Skip to main content

REST API reference

Inspect the contract.
Build against generated truth.

This reference reads the same typed endpoint registry that generates Booking Bible’s OpenAPI 3.1 document. Start with a group, then open only the parameters and examples you need.
  • Date-based versions
  • Scoped API keys
  • Consistent JSON envelopes
708
Documented operations
34
Capability groups
91
Documented scopes
692
REST route handlers

Route handlers are the complete deployed /api/v1 surface. Documented operations are the partner-facing contracts currently registered for OpenAPI.

Start with the contract

Authentication and versioning are explicit

Public discovery routes need no credentials. Protected routes accept a user bearer token, an organization-scoped API key, or the method documented for that operation.

Organization-scoped credentials

Create API keys under Admin → Settings → Developer. Each key is shown once, carries explicit scopes, and remains bound to its venue.

Version pinned by header

Send X-Api-Version to pin behavior. The current documented version is 2026-04-11.

bash
curl https://bookingbible.com/api/v1/admin/dashboard \
  -H "X-API-Key: bb_live_…" \
  -H "X-Api-Version: 2026-04-11"

Endpoint registry

Scan the surface. Expand the exact contract.

Groups and operations below are generated from the application registry. Download the OpenAPI JSON for code generation or machine-readable inspection.

Admin364 documented operations
POST/api/v1/admin/reports/brand-analyticsBearer token

Brand client and live engagement analytics

Read-only brand-scoped overview, paginated people/search, client details, classes and venue-admin crossover. Requires an active staff session, venue membership and reports.view; non-admin staff also require an explicit active brand reporting assignment. Supports staff JWT or a short-lived read-only delegated brand token. Search is in the request body, never the URL. Missing sources and capped totals are explicit. Small demographic cells are suppressed. No export or messaging authority.

Parameters, scopes and examples

Required scopes

reports.view

brandId, operation overview|people|person|classes|crossover, optional personId; filters include date range, paging, search, stage, membership/pass, attribution, geography, age band, consent, activity and watch ranges, played class/type/teacher, room/start-hour/day, and correlated device/platform/browser/operating-system/app-version filters. See docs/brand-analytics-contract.md.

Request body

{
  "brandId": "00000000-0000-4000-8000-000000000001",
  "operation": "overview",
  "filters": {
    "from": "2026-10-01",
    "to": "2026-10-08",
    "page": 1,
    "pageSize": 25,
    "inactiveDays": 30
  }
}
GET/api/v1/admin/reports/brand-sessionBearer token

Verify current staff reporting scope

Rechecks source-session revocation, MFA, current role, active venue membership, report permission and explicit brand assignment. Does not grant access by email or consumer cookie.

Parameters, scopes and examples

Required scopes

reports.view

Query parameters

brandIdstring · required
Brand UUID within the authenticated venue
GET/api/v1/admin/music/accessBearer token

Check music pilot staff access

Rechecks the current staff reporting session, brand scope, management permission and confirmed pilot identity. Returns enabled only for the pilot manager; other staff receive 403.

Parameters, scopes and examples

Required scopes

reports.viewsettings.business

Query parameters

brand_idstring · required
Brand UUID
GET/api/v1/admin/musicBearer token

List private brand music library

Staff report scope and confirmed pilot identity required. Returns brand mixes, versions, assignments, future classes and short-lived private preview URLs.

Parameters, scopes and examples

Required scopes

reports.view

Query parameters

brand_idstring · required
Brand UUID
POST/api/v1/admin/musicBearer token

Manage private brand music

Pilot staff manager/admin with settings.business may begin a path-specific signed resumable upload, finalize metadata, publish, assign, unpublish, activate/discard a replacement or delete. Every mutation is audited.

Parameters, scopes and examples

Required scopes

settings.business

Brand UUID and action-specific validated fields.

Request body

{
  "brand_id": "00000000-0000-4000-8000-000000000001",
  "action": "publish",
  "mix_id": "00000000-0000-4000-8000-000000000002"
}
GET/api/v1/admin/music/reportBearer token

Brand music engagement report

Pilot staff report scope required. Separates mix selection, attempts and bounded player-observed listening; small cells are suppressed and actual speaker or cast output is unknown.

Parameters, scopes and examples

Required scopes

reports.view

Query parameters

brand_idstring · required
Brand UUID
fromstring · required
UTC start date YYYY-MM-DD
tostring · required
UTC end date YYYY-MM-DD; at most 31 days
GET/api/v1/admin/failed-payment-policyBearer token

Read recurring policy editor context

Staff JWT only, with settings.membership for venue policy or passes.manage for recurring products. Original organization and actor are server-bound. No message or payment is sent by these controls. mode=venue returns defaults and the override list; mode=catalog returns recurring fixed and Flexible memberships only. offset pages 100 items; next_offset is null at the end. Includes venue currency, IANA time_zone, civil today, sparse stored values, server-resolved inheritance and venue reminder limit. No provider configuration is returned.

Parameters, scopes and examples

Required scopes

settings.membershippasses.manage
POST/api/v1/admin/failed-payment-policy/previewBearer token

Preview a failed-payment policy

Staff JWT only, with settings.membership for venue policy or passes.manage for recurring products. Original organization and actor are server-bound. No message or payment is sent by these controls. Read-only canonical resolver and reminder timeline, bounded to 30 venue-local days. Returns preview_hash bound to original actor, target, sparse value and current policy/draft context. It is not delivery or collection authority.

Parameters, scopes and examples

Required scopes

settings.membershippasses.manage

Sparse policy value; null clears a product override. Non-recurring products are refused.

Request body

{
  "organization_id": "11111111-1111-4111-8111-111111111111",
  "target": {
    "kind": "pass_type",
    "pass_type_id": "22222222-2222-4222-8222-222222222222"
  },
  "value": {
    "member_pay_now": false
  }
}
PATCH/api/v1/admin/failed-payment-policyBearer token

Save the reviewed recurring policy

Staff JWT only, with settings.membership for venue policy or passes.manage for recurring products. Original organization and actor are server-bound. No message or payment is sent by these controls. Requires original Idempotency-Key and preview_hash. Same original request replays before mutable catalog reads. SQL locks and compares the authoritative reviewed context; stale context yields immutable refused receipt. Flexible changes use draft CAS and canonical publisher; unrelated draft changes produce draft_review_required. Unknown or malformed outcomes retain original body and key.

Parameters, scopes and examples

Required scopes

settings.membershippasses.manage

Exact preview body with returned preview_hash. Per-key omission inherits for product overrides.

Request body

{
  "organization_id": "11111111-1111-4111-8111-111111111111",
  "target": {
    "kind": "pass_type",
    "pass_type_id": "22222222-2222-4222-8222-222222222222"
  },
  "value": {
    "member_pay_now": false
  },
  "preview_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
PATCH/api/v1/admin/failed-payment-policy/reminder-budgetBearer token

Set the venue reminder limit

Staff JWT with settings.business. Venue-only max_reminders integer 0..2147483647 and expected_max_reminders are required, with an original Idempotency-Key. Zero reminders does not disable independently configured lapse/cancellation. SQL248 checks dependent custom ladders. Missing invoice row retains daily fallback; existing invoice fields and cadence are preserved.

Parameters, scopes and examples

Required scopes

settings.business

Original venue and reviewed previous limit.

Request body

{
  "organization_id": "11111111-1111-4111-8111-111111111111",
  "max_reminders": 3,
  "expected_max_reminders": 3
}
GET/api/v1/admin/appointments/visits/{id}Bearer token

Read a venue appointment visit

Requires bookings.manage in the selected organization; owners and authorized staff share this route. Explicit notification_channels may select email, SMS or push only with notifications.send. Omission means no staff-triggered client notification. Staff bypass the member self-service cutoff; refund amounts still follow the accepted venue terms. Returns the same full-visit detail envelope as the member route, with staff-authorized cancellation preview.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
POST/api/v1/admin/appointments/visits/{id}/planBearer token

Plan a venue appointment visit move

Requires bookings.manage in the selected organization; owners and authorized staff share this route. Explicit notification_channels may select email, SMS or push only with notifications.send. Omission means no staff-triggered client notification. Staff bypass the member self-service cutoff; refund amounts still follow the accepted venue terms. Send date and optional provider_id/location_id. The route seeds the selected venue visit’s owned service composition and returns the shared candidate contract.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
DELETE/api/v1/admin/appointments/visits/{id}Bearer token

Cancel a venue appointment visit

Requires bookings.manage in the selected organization; owners and authorized staff share this route. Explicit notification_channels may select email, SMS or push only with notifications.send. Omission means no staff-triggered client notification. Staff bypass the member self-service cutoff; refund amounts still follow the accepted venue terms. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Send expected_updated_at exactly as returned by GET; stale snapshots return 409 STALE_TARGET. Optional reason and notification_channels. Returns visit fields plus cancellation_result under data. Every leg and the refund obligation are committed together.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
POST/api/v1/admin/appointments/visits/{id}/rescheduleBearer token

Move a venue appointment visit

Requires bookings.manage in the selected organization; owners and authorized staff share this route. Explicit notification_channels may select email, SMS or push only with notifications.send. Omission means no staff-triggered client notification. Staff bypass the member self-service cutoff; refund amounts still follow the accepted venue terms. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Send expected_updated_at exactly as returned by GET; stale snapshots return 409 STALE_TARGET. Uses the same provider/location/start/expected quote fields and atomic replacement contract as the member route. Optional notification_channels select one complete-visit reschedule notice.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
GET/api/v1/admin/reports/attendanceBearer token

Attendance history report

Class-date attendance (default) or action-date audit history with venue-local and UTC timestamps, actor/status/credit detail, and separate ordinary check-in, Undo, and correction receipts. Missing legacy history is explicit and never receives a fabricated timestamp.

Parameters, scopes and examples

Required scopes

reports.classesmembers.view_insights

Query parameters

viewstring
Whether the date range selects scheduled class dates or recorded action datesDefault: class_date
fromstring
Inclusive venue-local date (YYYY-MM-DD)
tostring
Inclusive venue-local date (YYYY-MM-DD)
location_idstring
Optional assigned venue location UUID
limitnumber
Cursor page size from 1 to 100Default: 20
afterstring
Opaque next-page cursor
GET/api/v1/admin/residence-commerce/pass-componentsBearer token

Read isolated draft pass component prices

Requires settings.business and a venue-owned pass_type_id query parameter. Returns the current pass source fingerprint, published choice/add-on identities and optional draft book from pass_component_drafts_v1. No payment, activation or country gate is exposed; responses are no-store.

PATCH/api/v1/admin/residence-commerce/pass-componentsBearer token

Save isolated draft pass component prices

Requires settings.business and Idempotency-Key. Accepts pass_type_id, expected_revision and a strict draft book with revision, source_pricing_version_id, source_fingerprint and component gross minor amounts by explicit ISO currency. Components cover registration fees, 30+ full pass totals, published add-on unit prices and a published allowance plus validated add-on quantities as a combined total. Source ownership, active published version, allowance rules and add-on eligibility are rechecked. Compare-and-set preserves other passes and active residence_commerce_v1; changed content requires a new revision. Exact replay is checked before changed source terms. Unknown writes retain the same body/key. Drafts never activate checkout, country gates, tax calculations or sends.

GET/api/v1/admin/residence-commerceBearer token

Read reviewed residence commerce price books

Venue-scoped versioned catalog price books and per-country checkout gates. Requires settings.business; a missing or invalid policy keeps checkout closed for configured products.

PATCH/api/v1/admin/residence-commerceBearer token

Revise a catalog price book and country gates

Optional country_drafts retain incomplete classification, place-of-supply and provider research separately from active rules; drafts do not approve checkout. Newly enabled or changed Stripe Tax markets are verified against seller Tax settings, active registration country/scheme coverage and charge-model-scoped product classification. Unchanged approvals retain their original reviewer and remain editable during provider outages. Requires settings.business and Idempotency-Key. Accepts a legacy pass_type_id or typed item identity and saves one owned catalog revision with compare-and-set, audits every country decision, and requires a new price book version when terms change. Fixed pass, product-package and service-bundle books use exact gross prices. Products, product packages, bundles, services and service_variant items support sparse exact gross currency drafts with all country gates closed; service variants are scoped through their owning service. Product packages verify both their own venue and their nonrecurring owning product venue. Known ISO prices retain exact currency minor precision. Reserved product_variant identities remain unsupported because the catalog has no product-variant entity. One-time foreign tax, configurable selections and unsupported checkout paths remain closed. Recurring foreign enabled countries require reviewed place-of-supply, registration and processor-account references.

GET/api/v1/admin/brands/{brandId}/rule-overridesBearer token

Read sub-brand booking and membership rule overrides

Requires settings.business. Missing overrides inherit venue rules; a corrupt brand policy fails closed.

PATCH/api/v1/admin/brands/{brandId}/rule-overridesBearer token

Revise sub-brand booking and membership rules

Requires settings.business, Idempotency-Key and the current brand updated_at value. Writes a validated whole versioned override with compare-and-set and audit.

GET/api/v1/admin/sales-snapshotBearer or API key

Sales snapshot

Venue-local sales report, gated by reports.view. Optional range=today|yesterday|7d|mtd|30d|custom, basis=cash|accrual, from/to, methods, categories, brand, location and refresh=1. Existing aggregates remain unchanged; cashHeadline adds separate major-unit gross/count totals by recorded payment or gift-card currency and unresolvedCount. CashHeadline is null for accrual; clients must not relabel legacy aggregates with a current venue currency.

Parameters, scopes and examples

Required scopes

read:reports
GET/api/v1/admin/dashboardBearer or API key

Dashboard stats

Today, weekly, and monthly KPIs: bookings, revenue, members, capacity, churn, MRR.

Parameters, scopes and examples

Required scopes

read:reports
GET/api/v1/admin/reports/brandsBearer or API key

Per-brand comparison report

Revenue, bookings, attendance, 30-day active clients, and average revenue per client — per brand for the given period (default last 30 days). Uses bookings.brand_id and payments.brand_id populated by HYC_2. Returns venue-wide (unbranded) totals alongside the brand rows.

Parameters, scopes and examples

Required scopes

read:reports
GET/api/v1/admin/dashboard/todayBearer or API key

Today at a glance

Today's class timeline with booking counts, check-in status, and room assignments.

Parameters, scopes and examples

Required scopes

read:schedule
GET/api/v1/admin/scheduleBearer or API key

Admin schedule

Full schedule view with internal data: per-status booking counts, notes, cancellation reasons, updated_at concurrency tokens, fail-closed historical capabilities, additive `course_identifiers` pills (`{course_id,label,tone,course_name}`, hidden courses included) for workshop dates and, for a bounded window (from and to at most 62 days apart) or include=notification_availability, at most 300 rows, notification_availability.{cancel,edit,substitute}.{clients,instructor}.{email,sms,push} (available when the venue gate passes and at least one recipient is reachable; reach counts and blocked reasons included). Otherwise the key is omitted: read one class with GET /api/v1/admin/schedule/{id} (which also carries restore). include_historical=true requires scheduling.manage_history.

Parameters, scopes and examples

Required scopes

read:schedule

Query parameters

start_datestring
Inclusive ISO date/time lower bound
end_datestring
Inclusive ISO date/time upper bound
include_historicalstring
Include protected historical class rows; requires scheduling.manage_historyDefault: false
GET/api/v1/admin/appointments/availabilityBearer token

Staff appointment availability

Staff JWT and bookings.manage required; API keys and mixed credentials are rejected. Calls the canonical availability engine with the authoritative active organization, purpose staff and strict read errors. Staff-only services and rescheduling during wind-down do not inherit public sale gates. Requires service_id and a valid YYYY-MM-DD date; accepts provider_id, variant_id, location_id and reschedule_id. A reschedule source is excluded from conflicts only after same-tenant and same-service ownership checks. Returns provider and opted-in facility slots with private no-store caching. Facility identity is provider_id null plus room_id/resource_kind room. Reschedule walks the tenant-verified original room through an internal staff-only override, including non-default rooms; caller room overrides are rejected. Read-only discovery never authorizes creation or changes engine/SQL mutation checks.

Parameters, scopes and examples

Required scopes

bookings.manage

Query parameters

service_idstring · required
Service UUID in the active organization
datestring · required
Venue-local calendar date YYYY-MM-DD
provider_idstring
Optional provider UUID
variant_idstring
Optional service variant UUID
location_idstring
Optional service location UUID
reschedule_idstring
Existing same-tenant, same-service appointment UUID to exclude from occupancy
GET/api/v1/admin/appointmentsBearer token

Venue appointment schedule

Business-app venue-wide appointment list with updated_at concurrency tokens and authoritative, fail-closed historical capabilities. Filters by ISO window, direction, status, provider, and location. Permission: bookings.manage.

Parameters, scopes and examples

Query parameters

fromstring
Inclusive ISO start time
tostring
Exclusive ISO end time
directionstring
upcoming | pastDefault: upcoming
statusstring
Appointment status
provider_idstring
Provider UUID
location_idstring
Location UUID
limitnumber
Maximum 200Default: 100
POST/api/v1/admin/appointmentsBearer token

Create an appointment for a client

Creates a tenant-bound current/future appointment for a known member or contact-complete guest through the canonical atomic appointment engine. Venue-local past dates and historical attestation fields fail closed until the dedicated executor is installed. Permission: bookings.manage. Idempotency-Key required. Client delivery is default-silent: only an explicit notification_channels selection of email, sms, and/or push can send. A non-empty selection requires notifications.send and an authoritative availability preflight before the mutation; an unavailable channel fails without creating the appointment. Omitted or empty channels and legacy notify booleans remain silent. notify.clients.channels is accepted as the per-audience equivalent; the 422 details carry {audience, channel, reason_code, unavailable_reason}; success adds notify_outcome.

Parameters, scopes and examples

Appointment booking

Request body

{
  "service_id": "uuid",
  "provider_id": "uuid",
  "start_time": "2026-08-01T12:00:00Z",
  "client_id": "uuid",
  "source": "phone",
  "notification_channels": []
}
GET/api/v1/admin/appointments/{id}Bearer token

Appointment operational detail

Tenant-bound client, provider, service, location, payment, notes, lifecycle state, updated_at concurrency token, and authoritative fail-closed historical capabilities for the Business app. Additive historical_correction_eligibility returns candidate/reason_code/reason from a bounded private tenant-scoped row read. Definite SQL dependencies suppress corrections without exposing payment or operation identities. A candidate still requires locked SQL checks; global capability is not record eligibility. Permission: bookings.manage.

Parameters, scopes and examples

Path parameters

idstring · required
Appointment UUID
PATCH/api/v1/admin/appointments/{id}Bearer token

Operate an appointment

Atomic ordinary check-in, start, complete, no-show, cancel, or reschedule with the exact expected_updated_at token returned by GET. A stale token returns 409 STALE_TARGET without mutation. Each action is checked against its canonical permission. Past/terminal appointments, past reschedule targets, and historical attestation fields fail closed until the dedicated executor is installed. Idempotency-Key required. No-show, cancel, and reschedule are default-silent and accept an explicit notification_channels selection of email, sms, and/or push; a non-empty selection requires notifications.send and an authoritative availability preflight before mutation. An unavailable channel fails without changing the appointment. Omitted or empty channels and legacy notify booleans remain silent. Check-in, start, and complete are non-client-contact actions and reject notification_channels. notify.clients.channels is accepted as the per-audience equivalent (mixing both shapes returns 422 MIXED_NOTIFY_SHAPES); an unavailable channel returns 422 APPOINTMENT_NOTIFICATION_CHANNEL_UNAVAILABLE with {audience, channel, reason_code, unavailable_reason}; success adds notify_outcome. GET responses add notification_availability_by_action {cancel, no_show, reschedule} in the staff availability contract.

Parameters, scopes and examples

Path parameters

idstring · required
Appointment UUID

Discriminated appointment action with the opaque updated_at token returned by the appointment detail

Request body

{
  "action": "cancel",
  "expected_updated_at": "2026-08-28T09:15:30.000Z",
  "reason": "Client requested",
  "notification_channels": []
}
POST/api/v1/admin/appointments/{id}/historical-correctionsBearer token

Correct a historical appointment record

Dedicated, atomic appointment-history command contract. Requires a UUID Idempotency-Key, appointments.manage_history plus the ordinary operation permission, REWRITE attestation, and a past effective_at. Existing-row operations require the exact expected_updated_at and expected_status returned by the appointment detail; retrocreate accepts only the closed non-financial appointment intent. Corrections are always silent and reject notification controls. The route returns 503 HISTORICAL_EXECUTOR_UNAVAILABLE without table-call fallback until the separately reviewed correct_appointment_historical database RPC is installed.

Parameters, scopes and examples

Required scopes

appointments.manage_history

Path parameters

idstring · required
Appointment UUID

Closed appointment correction; history_confirmation_token must be REWRITE

Request body

{
  "operation": "appointment.correct_attendance_state",
  "expected_updated_at": "2026-08-28T09:15:30.000Z",
  "expected_status": "confirmed",
  "history_reason": "Signed appointment record confirms this correction",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-27T11:00:00.000Z",
  "intent": {
    "status": "completed",
    "checkedInAt": "2026-08-27T10:00:00.000Z",
    "completedAt": "2026-08-27T11:00:00.000Z"
  }
}
GET/api/v1/admin/reviewsBearer token

Venue review inbox

Tenant-bound class and appointment reviews for the Business app. Supports source, visibility, rating, and pagination filters. Anonymous reviewer identity is never returned. Permission: feedback.view.

PATCH/api/v1/admin/reviews/{sourceType}/{id}Bearer token

Moderate a venue review

Publishes or unpublishes one tenant-bound class or appointment review. Permission: feedback.manage. Idempotency-Key required.

Parameters, scopes and examples

Path parameters

sourceTypestring · required
class | appointment
idstring · required
Review UUID
GET/api/v1/admin/feedback-settingsBearer or API key

Read internal feedback and review cadence settings

JWT permission feedback.configure, or a venue-bound API key with write:settings. Mixed credentials are rejected. Reads fail unavailable on settings query errors and do not seed configuration. Returns the public feedback settings DTO, including first_completed_visit, repeat_every_completed_visits, visit_scope and their legacy prompt-prefixed mirrors. No raw organization settings are returned.

Parameters, scopes and examples

Required scopes

write:settings
PATCH/api/v1/admin/feedback-settingsBearer token

Configure internal feedback and review cadence

JWT permission feedback.configure only; API keys and mixed credentials are rejected. Idempotency-Key is bound to the organization, actor, operation and validated body. This saves configuration only, never sends a review request. Unknown saves return 503 SAVE_OUTCOME_UNKNOWN with phase:write and saved:unknown and retain the claim. A confirmed save whose reload fails returns saved:true, phase:postcommit, reload_failed:true; refresh, do not treat this as rollback. First completed visit N and repeat interval M must be saved together. N and non-null M are integers from 1 to 50; null M means first only. Explicit prompt_channels: [] disables internal prompt channels. Omitted fields retain existing configuration.

Parameters, scopes and examples

Required scopes

feedback.configure

Validated feedback-settings patch; aliases first_completed_visit, repeat_every_completed_visits and visit_scope are accepted.

Request body

{
  "first_completed_visit": 2,
  "repeat_every_completed_visits": 2,
  "visit_scope": "appointments"
}
GET/api/v1/admin/external-review-settingsBearer or API key

Read public review-request configuration

JWT permission feedback.configure, or a venue-bound API key with write:settings. Mixed credentials are rejected. Reads fail unavailable on settings query errors and do not seed configuration. Returns public review sites, configured email/SMS channels and independent completed-visit cadence. A clicked public review link is not proof that a review was submitted.

Parameters, scopes and examples

Required scopes

write:settings
PATCH/api/v1/admin/external-review-settingsBearer token

Configure public review-request cadence

JWT permission feedback.configure only; API keys and mixed credentials are rejected. Idempotency-Key is bound to the organization, actor, operation and validated body. This saves configuration only, never sends a review request. Unknown saves return 503 SAVE_OUTCOME_UNKNOWN with phase:write and saved:unknown and retain the claim. A confirmed save whose reload fails returns saved:true, phase:postcommit, reload_failed:true; refresh, do not treat this as rollback. Public review requests use configured email/SMS channels and existing consent, suppression and delivery gates. N and M are independent from internal feedback. suppress_after_public_review means stop after the tracked public link was opened, not verified review completion.

Parameters, scopes and examples

Required scopes

feedback.configure

Public settings patch; first_completed_visit and repeat_every_completed_visits are paired. No automatic delivery or tenant-specific seeding.

Request body

{
  "first_completed_visit": 1,
  "repeat_every_completed_visits": null,
  "visit_scope": "appointments"
}
POST/api/v1/admin/review-cadence/previewBearer token

Preview review cadence without sending

JWT permission feedback.configure only. Read-only preview of saved configuration and recent completed-visit candidates. It uses the canonical visit counter and cadence decisions, but does not prove delivery reachability, consent, quiet hours or source overrides. preview_kind is cadence_configuration and delivery_verified is false. Legacy eligible means cadence qualification only. No messages, prompts or settings are created.

Parameters, scopes and examples

Required scopes

feedback.configure

Choose internal or public saved configuration; unsaved form drafts are not evaluated.

Request body

{
  "path": "internal"
}

Response example

{
  "data": {
    "path": "internal",
    "preview_kind": "cadence_configuration",
    "delivery_verified": false,
    "enabled": true,
    "description": "Start after visit 2, then every 2 visits",
    "examples": [
      2,
      4,
      6
    ],
    "channels": [
      "in_app"
    ],
    "unavailable_channels": [],
    "rows": []
  },
  "error": null
}
POST/api/v1/admin/services/{id}/archiveBearer token

Archive a venue-owned service

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Idempotency-Key is required and bound to organization, actor, operation, resource and validated body. Web and REST share the same allowlisted writer, tenant-reference checks, plan/franchise limits, audit and cache invalidation. Uses the existing web archive core: deactivates the service without deleting existing appointments, retains the franchise lock, and emits the canonical service.deleted audit/webhook event. Returns {ok:true,write_warnings?}, not a service row. Unknown writes return 409 UNKNOWN_WRITE_OUTCOME and retain the claim; replay the exact key or reload authoritative state. This endpoint does not notify customers.

Parameters, scopes and examples

Required scopes

bookings.manage

Required empty JSON object. Tenant, actor, service identity and notification flags cannot be supplied in the body.

Request body

{}
GET/api/v1/admin/servicesBearer token

List the venue service catalog

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Returns explicit catalog fields, excluding payout configuration. Read failure is unavailable, not a successful empty catalog. No customer appointments are returned.

Parameters, scopes and examples

Required scopes

bookings.manage

Query parameters

include_inactivestring
Include inactive services when exactly true.Default: false
POST/api/v1/admin/servicesBearer token

Create a service in the venue catalog

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Idempotency-Key is required and bound to organization, actor, operation, resource and validated body. Web and REST share the same allowlisted writer, tenant-reference checks, plan/franchise limits, audit and cache invalidation. Prices are decimal MAJOR currency units, not integer minor units. Currency must match authoritative venue currency. The route refuses body tenant IDs, unknown columns and arbitrary image URLs. Confirmed saves return the service DTO and optional write_warnings. Unknown transport/database outcomes return 409 UNKNOWN_WRITE_OUTCOME and retain the claim: refresh authoritative state or retry the exact operation/key, never blindly create again. Known prewrite refusals may release the claim. A failed acknowledgement cannot turn a known save into rollback.

Parameters, scopes and examples

Required scopes

bookings.manage

Validated catalog create input. Native service image, variants and provider-hours authoring are separate companion work, not implied by this endpoint.

Request body

{
  "name": "Manicure",
  "slug": "manicure",
  "duration_minutes": 45,
  "price_amount": 49.5,
  "currency": "EUR"
}
GET/api/v1/admin/services/{id}Bearer token

Read one venue-owned service

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Foreign or missing service IDs return not found; query failures remain unavailable. The catalog DTO does not contain provider payout configuration.

Parameters, scopes and examples

Required scopes

bookings.manage
PATCH/api/v1/admin/services/{id}Bearer token

Edit or deactivate a venue service

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Idempotency-Key is required and bound to organization, actor, operation, resource and validated body. Web and REST share the same allowlisted writer, tenant-reference checks, plan/franchise limits, audit and cache invalidation. Empty patches are rejected before claiming a key. Unrelated edits retain stored service currency; monetary edits validate the resulting stored-plus-patch deposit configuration. is_active:false deactivates, without deleting existing appointments. Reactivation rechecks the plan limit. Confirmed saves return the service DTO and optional write_warnings. Unknown transport/database outcomes return 409 UNKNOWN_WRITE_OUTCOME and retain the claim: refresh authoritative state or retry the exact operation/key, never blindly create again. Known prewrite refusals may release the claim. A failed acknowledgement cannot turn a known save into rollback.

Parameters, scopes and examples

Required scopes

bookings.manage

Only changed catalog fields; no tenant reassignment, arbitrary image URL or direct booking-mode grant.

Request body

{
  "duration_minutes": 50,
  "price_amount": 55
}
POST/api/v1/admin/services/{id}/image/upload-urlBearer token

Prepare a service image upload

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Service ownership is checked before storage access. Strict JPG, PNG or WebP metadata, positive integer file_size up to 5 MiB. Returns {uploadUrl,token,path,maxBytes}; PUT the file bytes to uploadUrl, then call finalize. Paths have an immutable venue/service/generation prefix. Existing lazy bucket provisioning remains part of this authorized source operation; this does not mean hosted storage has been provisioned. This request does not attach an image or contact clients.

Parameters, scopes and examples

Required scopes

bookings.manage

Only declared content type and byte size; no tenant IDs or arbitrary URL.

Request body

{
  "content_type": "image/jpeg",
  "file_size": 250000
}
POST/api/v1/admin/services/{id}/image/finalizeBearer token

Finalize an owned service image upload

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Service ownership is checked before storage access. Idempotency-Key is required and scoped to actor, venue, service, operation and body. A confirmed save may include write_warnings for secondary failures. Unknown outcomes return 409 UNKNOWN_WRITE_OUTCOME and retain the claim; reload authoritative state or retry the exact key, never blindly repeat with a new key. Accepts only the original path minted for this venue and service. Validates bytes and minimum 1200 by 675 dimensions, generates immutable 1600/800/400 WebP images, and compares both previous image URL and JSON media before attaching them. Returns {imageUrl,write_warnings?}. Cleanup never lists/deletes a service prefix or removes current-generation heroes. Storage conflicts are unknown, not proof that prior bytes match. No customer notifications.

Parameters, scopes and examples

Required scopes

bookings.manage

Exact owned original path returned by upload-url.

Request body

{
  "path": "00000000-0000-4000-8000-0000000000a1/00000000-0000-4000-8000-0000000000c1/original-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.jpg"
}
DELETE/api/v1/admin/services/{id}/imageBearer token

Remove a service image

Staff JWT with bookings.manage in the authorized venue. API keys and mixed credentials are rejected. Service ownership is checked before storage access. Idempotency-Key is required and scoped to actor, venue, service, operation and body. A confirmed save may include write_warnings for secondary failures. Unknown outcomes return 409 UNKNOWN_WRITE_OUTCOME and retain the claim; reload authoritative state or retry the exact key, never blindly repeat with a new key. No request body. Returns {ok:true,write_warnings?}. Clears only the image and hero media keys with a compare-and-set; preserves sibling media and current appointment history. Only exact owned former hero paths are eligible for cleanup. Arbitrary external images and peer uploads are never deleted. No customer notifications.

Parameters, scopes and examples

Required scopes

bookings.manage
GET/api/v1/admin/booking-offeringBearer token

Read booking products and eligible offering changes

User JWT and settings.business required; API keys and mixed credentials are rejected. Returns contract_version:1, organization_id, mode, product lifecycle/status/sales flags and both internal transition choices with availability, reason and requiredPlanKeys where relevant. Uses the same authority as web Business settings. Read failure is 503, not an empty or class-default configuration. No prices, raw settings or client identities are returned.

Parameters, scopes and examples

Required scopes

settings.business
PATCH/api/v1/admin/booking-offeringBearer token

Change an eligible venue booking offering

User JWT and settings.business required. Strict mode-only body; Idempotency-Key binds the selected organization, actor and validated body. Uses the existing atomic offering RPC, commercial access gates and wind-down/history retention; it never changes a paid plan, charges a customer or archives a product. Success returns contract_version:1, organization_id and mode, with refresh_failed:true if independent surface refresh failed after save. Errors add write_state:not_written or unknown; PLAN_QUOTE_REQUIRED includes required_plan_keys without inventing prices. Preserve the same logical key on unconfirmed outcomes and reload before a new intent. An acknowledgement failure never replaces a known save response. Separate paid-plan preview/acceptance and native authoring companions remain required before full parity is claimed.

Parameters, scopes and examples

Required scopes

settings.business

Required mode: classes, appointments or both. Internal classes preserves grandfathered configurations; public commerce still has two model names. No organization, actor, price, tier or raw settings fields.

Request body

{
  "mode": "appointments"
}
GET/api/v1/admin/appointment-readinessBearer token

Evaluate live appointment setup readiness

User JWT and bookings.manage required; API keys and mixed credentials are rejected. Evaluates the canonical appointment readiness facts for the selected organization without writing. Returns contract_version:1, organization_id, evaluated status ready|incomplete, missing labels, evaluated_at and current booking_mode. persisted_onboarding is separate onboarding provenance and is never the evaluated status; pending and seed_errors are not live facts. Read failure is 503 QUERY_FAILED, not fabricated ready/incomplete. business_type does not determine readiness. Native must bind this exact contract; this is not a claim that native screens are complete.

Parameters, scopes and examples

Required scopes

bookings.manage
GET/api/v1/admin/appointment-policiesBearer token

Read effective appointment booking policies

User JWT and settings.business required; API keys and mixed credentials are rejected. Lifecycle uses resolveAppointmentSettingsLifecycleGate, not canVenueManageAppointmentSettings: active, wind_down and read_only authorize; archived and unknown/malformed status refuse; a missing appointments product row may use booking_mode plus service count; a product-domain query failure is 503 QUERY_FAILED and never a legacy fallback. Returns contract_version:1, organization_id and the allowlisted effective configuration: slot_interval_minutes, payment_at_booking_mode with derived require_payment_at_booking, self_service_cutoff_hours plus malformed flag, grouped_visits venue settings, and row_present. Does not expose platform kill switches, raw settings, deposits, checkout or POS. Read failure is 503, not invented defaults presented as a query success. Lifecycle refusal is 403 APPOINTMENT_SETTINGS_UNAVAILABLE.

Parameters, scopes and examples

Required scopes

settings.business
PATCH/api/v1/admin/appointment-policiesBearer token

Update allowlisted appointment booking policies

User JWT and settings.business required, plus resolveAppointmentSettingsLifecycleGate (active/wind_down/read_only; archived and unknown/malformed refuse; product query failure is not a booking_mode fallback). Strict allowlisted body for slot_interval_minutes, payment_at_booking_mode, self_service_cutoff_hours and grouped_visits. grouped_visits refund_terms require confirmed:true; platform kill switches are not writable. Idempotency-Key binds selected organization, actor, operation appointment_policies.patch and the validated body. Writes use compare-and-set merge of the shared appointments settings row so unrelated keys survive; an absent-row unique conflict retries. Success returns the GET DTO, with refresh_failed:true if independent cache/audit follow-up failed after save. Errors add write_state:not_written or unknown. Preserve the same logical key on unconfirmed outcomes and reload before a new intent. An acknowledgement failure never replaces a known save response. Native authoring companions remain required before full parity is claimed.

Parameters, scopes and examples

Required scopes

settings.business

At least one allowlisted field. payment_at_booking_mode is venue, online or client_choice. self_service_cutoff_hours may be null to clear. grouped_visits requires enabled, max_services_per_visit, refund_terms and confirmed. No organization, actor, require_payment_at_booking, raw settings, deposit, checkout or plan fields.

Request body

{
  "slot_interval_minutes": 15,
  "payment_at_booking_mode": "venue",
  "self_service_cutoff_hours": 24
}
DELETE/api/v1/admin/services/{id}/variants/{variantId}Bearer token

Remove a service variant

Uses the same tenant-bound variant deletion core as the web admin. Prefer PATCH is_active:false to keep the variant record; deletion retains existing database reference restrictions and may be refused. Requires bookings.manage and JWT; API keys/mixed credentials are rejected. Idempotency-Key binds tenant, actor, parent service and variant. Unknown writes retain the request key: reload and review before a new intent. No appointment or payment mutation is performed by this handler.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID
variantIdstring · required
Variant UUID
GET/api/v1/admin/services/{id}/variantsBearer token

List service variants

Duration/price options for one tenant service. Empty array means none; 503 UNAVAILABLE is a failed read. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID
POST/api/v1/admin/services/{id}/variantsBearer token

Create a service variant

Creates a duration/price option on the parent service. Price inherits the parent service currency and is decimal major units (49.50 stays 49.50). No organization_id, payout, intake, or waiver fields. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID

Variant create. Parent currency is authoritative.

Request body

{
  "name": "Long",
  "duration_minutes": 90,
  "price_amount": 49.5,
  "is_active": true
}
PATCH/api/v1/admin/services/{id}/variants/{variantId}Bearer token

Update a service variant

Partial update of one variant that belongs to the parent service. Empty PATCH is 400. Prefer is_active false over delete. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID
variantIdstring · required
Variant UUID

Non-empty partial variant fields.

Request body

{
  "is_active": false,
  "price_amount": 49.5
}
GET/api/v1/admin/services/{id}/providersBearer token

List assigned providers

Optional eligible_only=true returns only active assignments with an active service-delivering membership of the authenticated venue; a failed eligibility read returns 503. Default reads retain inactive assignments for administration. Public assignment projection for one tenant service: names, photos, duration/price overrides, primary/sort/active. No bio, contact, compensation, or payout. Empty array means none; 503 is a failed read. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID

Query parameters

eligible_onlystring
true for eligible booking choices; false or omitted retains setup assignments
PUT/api/v1/admin/services/{id}/providers/{providerId}Bearer token

Assign or replace a service provider

Upserts an active delivering membership onto the service. Instructor/service_provider primary role or additive delivering capability required. Generic staff/reception is 403. custom_price_amount is decimal major units in the parent service currency; null inherits. Idempotency-Key required and bound to tenant, actor, target and validated body. Permission: bookings.manage. JWT only. Team capacity uses the canonical plan authority; its preflight and upsert are not an atomic capacity reservation.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID
providerIdstring · required
Staff profile UUID

Optional duration/price overrides, primary, sort, and active.

Request body

{
  "custom_duration_minutes": 45,
  "custom_price_amount": 49.5,
  "is_primary": true,
  "sort_order": 0,
  "is_active": true
}
PATCH/api/v1/admin/services/{id}/providers/{providerId}Bearer token

Update a service-provider assignment

Partial update of an existing assignment. Empty PATCH is 400. Missing assignment is 404. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID
providerIdstring · required
Staff profile UUID

Non-empty partial assignment fields.

Request body

{
  "is_active": false
}
DELETE/api/v1/admin/services/{id}/providers/{providerId}Bearer token

Unassign a service provider

Removes the future offering link. Historical appointments are preserved. Inactive memberships may be unassigned. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Service UUID
providerIdstring · required
Staff profile UUID
GET/api/v1/admin/staff/{staffId}/servicesBearer token

List services assigned to a staff member

Staff-detail assignment pane. staff.view plus venue membership; catalog edit is not required to read names. Empty array means none assigned. Permission: staff.view. JWT only.

Parameters, scopes and examples

Required scopes

staff.view

Path parameters

staffIdstring · required
Staff profile UUID
GET/api/v1/admin/roomsBearer token

List venue rooms and stations

Explicit room setup projection, including inactive rooms. Requires settings.business. JWT only; API keys and mixed credentials are refused. Empty list is distinct from a failed read.

Parameters, scopes and examples

Required scopes

settings.business
POST/api/v1/admin/roomsBearer token

Create a room or station

Shared web/native room core. Explicit location must belong to the venue; an omitted location is inferred only when exactly one active location exists. Floor-plan URLs require the separate upload/finalize flow. Does not create locations or change paid tiers. Idempotency-Key is required and binds actor, organization, target and validated intent. UNKNOWN_WRITE_OUTCOME retains that key; reload and review before creating a new intent.

Parameters, scopes and examples

Required scopes

settings.business

Room name, positive capacity and supported room configuration.

Request body

{
  "name": "Treatment station",
  "capacity": 1,
  "room_type": "station"
}
GET/api/v1/admin/rooms/{id}Bearer token

Read room setup

One room scoped to the authenticated venue. No client, treatment, payroll or booking records. Missing room is 404; failed read is not an empty room.

Parameters, scopes and examples

Required scopes

settings.business

Path parameters

idstring · required
Room UUID in the authenticated organization
PATCH/api/v1/admin/rooms/{id}Bearer token

Update room setup

Nonempty supported room fields, including is_active for retirement. Capacity reduction retains the existing future-class cap projection; ROOM_CAPACITY_PARTIAL includes saved:true and the saved room when that secondary projection fails. No class booking or checkout change. floor_plan_url accepts only null to clear its reference; new URLs require upload/finalize. Clearing does not delete storage objects. Idempotency-Key is required and binds actor, organization, target and validated intent. UNKNOWN_WRITE_OUTCOME retains that key; reload and review before creating a new intent.

Parameters, scopes and examples

Required scopes

settings.business

Path parameters

idstring · required
Room UUID in the authenticated organization

Supported partial room configuration.

Request body

{
  "is_active": false
}
GET/api/v1/admin/locationsBearer token

List location setup references

Limited authoring DTO for selecting a room location and editing existing media/hours. Either settings.business or locations.manage permits this read. Does not create/delete locations, set primary location or expose billing settings.

Parameters, scopes and examples

Required scopes

settings.businesslocations.manage
GET/api/v1/admin/locations/{id}Bearer token

Read location media and hours

Limited authoring DTO for one tenant location. Either settings.business or locations.manage permits this read. No location billing or client records.

Parameters, scopes and examples

Required scopes

settings.businesslocations.manage

Path parameters

idstring · required
Location UUID in the authenticated organization
PATCH/api/v1/admin/locations/{id}Bearer token

Update location media and opening hours

Requires locations.manage. A media/hours patch supports image_url, gallery_urls and opening_hours. Media permits deliberate external URLs or null removal; storage-object URLs must use finalize. Alternatively send only remove_gallery_url to remove one exact saved gallery reference, including finalized uploads, while preserving the other images. Removal uses a tenant-scoped JSONB compare-and-swap, is a no-op if already absent, returns GALLERY_CONFLICT on concurrent change, and never deletes storage objects. Do not combine removal with other fields. Hours contain mon through sun, each null (closed) or open/close HH:mm with open before close; null clears hours. Not a location provisioning or billing API. Idempotency-Key is required and binds actor, organization, target and validated intent. UNKNOWN_WRITE_OUTCOME retains that key; reload and review before creating a new intent.

Parameters, scopes and examples

Required scopes

locations.manage

Path parameters

idstring · required
Location UUID in the authenticated organization

Nonempty media/hours patch, or the single remove_gallery_url field.

Request body

{
  "image_url": null
}
POST/api/v1/admin/rooms/{id}/media/upload-urlBearer token

Prepare a room floor-plan upload

Checks tenant room ownership before minting a signed venue-assets upload. JPEG/PNG/WebP only, at most 5 MiB. Returns upload_url, token, path, public_url and max_bytes. Upload authorization alone does not store the URL on the room.

Parameters, scopes and examples

Required scopes

settings.business

Path parameters

idstring · required
Room UUID in the authenticated organization

Declared format and size; finalize independently checks bytes.

Request body

{
  "content_type": "image/jpeg",
  "file_size": 12000
}
POST/api/v1/admin/rooms/{id}/media/finalizeBearer token

Finalize a room floor-plan upload

Exact organization/room object path, ownership and downloaded size/magic-byte validation before storing floor_plan_url. Returns the explicit room DTO. No arbitrary file or foreign resource path. Idempotency-Key is required and binds actor, organization, target and validated intent. UNKNOWN_WRITE_OUTCOME retains that key; reload and review before creating a new intent.

Parameters, scopes and examples

Required scopes

settings.business

Path parameters

idstring · required
Room UUID in the authenticated organization
POST/api/v1/admin/locations/{id}/media/upload-urlBearer token

Prepare a location image upload

Requires locations.manage and tenant location ownership. kind is hero or gallery. JPEG/PNG/WebP up to 5 MiB. Returns signed upload fields, max_bytes and kind; does not yet change location media.

Parameters, scopes and examples

Required scopes

locations.manage

Path parameters

idstring · required
Location UUID in the authenticated organization

Image kind, declared format and size.

Request body

{
  "kind": "hero",
  "content_type": "image/png",
  "file_size": 12000
}
POST/api/v1/admin/locations/{id}/media/finalizeBearer token

Finalize a location image upload

Requires locations.manage. Exact owned path and downloaded byte checks precede persistence. Gallery append uses JSONB compare-and-swap: duplicate path is unchanged, capacity 20 refuses, malformed data refuses, concurrent change returns GALLERY_CONFLICT without dropping another image. Returns the limited location DTO. Idempotency-Key is required and binds actor, organization, target and validated intent. UNKNOWN_WRITE_OUTCOME retains that key; reload and review before creating a new intent.

Parameters, scopes and examples

Required scopes

locations.manage

Path parameters

idstring · required
Location UUID in the authenticated organization
GET/api/v1/admin/providers/{id}/hoursBearer token

List provider working hours

Tenant-local working-hours rows for one delivering staff member. Empty array means none; 503 UNAVAILABLE is a failed read. include_inactive=true includes deactivated rows. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Staff profile UUID

Query parameters

include_inactivestring
Include deactivated hours rows
POST/api/v1/admin/providers/{id}/hoursBearer token

Create provider working hours

Creates a venue-local hours row. Times are clock values; effective_from defaults to the venue calendar date. Overlap/duplicate rows are refused. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Staff profile UUID

Working-hours create. Location must belong to this venue when set.

Request body

{
  "day_of_week": 1,
  "start_time": "09:00",
  "end_time": "17:00",
  "break_start": "12:00",
  "break_end": "13:00",
  "location_id": null,
  "effective_from": "2026-09-13",
  "effective_until": null
}
PATCH/api/v1/admin/providers/{id}/hours/{scheduleId}Bearer token

Update provider working hours

Partial update of one hours row owned by the provider in this venue. Empty PATCH is 400. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Staff profile UUID
scheduleIdstring · required
Schedule UUID

Non-empty partial hours fields.

Request body

{
  "end_time": "18:00"
}
DELETE/api/v1/admin/providers/{id}/hours/{scheduleId}Bearer token

Deactivate provider working hours

Soft-deactivates the hours row. Historical appointments are preserved. Inactive memberships may be cleaned up. Idempotency-Key required. Permission: bookings.manage. JWT only.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Staff profile UUID
scheduleIdstring · required
Schedule UUID
GET/api/v1/admin/professional/partner/classes/{classId}/rosterBearer token

Read an assigned partner class roster

Business bearer-JWT read for an individual professional whose active organization remains their own workspace. The server re-proves the active practitioner partnership, active host membership, primary/substitute assignment and relationship-scoped roster access. It returns genuine class and booking updated_at CAS tokens, venue-local day_state, operation-filtered historical_capabilities, the authoritative client_contact_visibility result, the fail-closed client_pass_visibility (visible|hidden) set by the host venue per collaborator, and only operational booking/pass details including the frozen Flexible selection, remaining allowance and expiry warnings. When client_pass_visibility is hidden every attendee pass is null and pass-derived warnings are omitted. Contact values use the shared none/masked/full redactor. Terminal cancelled rows require include_historical_records=true plus the host scheduling.manage_history and matching ordinary action grant. No unrestricted profile, account credit, unrelated passes or financial history is exposed.

Parameters, scopes and examples

Required scopes

schedule.view_own

Path parameters

classIdstring · required
Assigned host class UUID
include_historical_recordsboolean
Include supported terminal roster records when host history access permits
POST/api/v1/admin/professional/partner/classes/{classId}/roster/{bookingId}/historical-correctionsBearer token

Correct an assigned partner class roster record

Business bearer-JWT contract for an individual professional whose active organization remains their own workspace. The server derives the host venue and proves the exact active practitioner partnership, active host membership, primary/substitute class assignment, a past scheduled/completed class, and scheduling.manage_history plus the operation's ordinary host permission. Only existing-booking attendance, no-show, cancellation, and invalidation corrections are accepted; home-tenant and generic admin-booking execution are never reused. A UUID Idempotency-Key, typed REWRITE confirmation, reason, effective_at, genuine expected_updated_at booking CAS, and genuine expected_class_updated_at class CAS are mandatory. Success returns the database-read updated_at and class_updated_at tokens; stale state returns 409, while failed post-commit token readback returns 503 and requires an exact retry with the same key. Delivery defaults silent; an explicit Email/SMS/Push selection first requires the host's notifications.send permission and then fails 422 before mutation because historical delivery is unsupported. Executor: 20261020000009.

Parameters, scopes and examples

Required scopes

scheduling.manage_history

Path parameters

classIdstring · required
Assigned host class UUID
bookingIdstring · required
Host class booking UUID

One existing partner-roster correction. The host organization is relationship-derived and cannot be selected in the body.

Request body

{
  "operation": "class_booking.correct_attendance_state",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_class_updated_at": "2026-08-20T08:55:00.000Z",
  "history_reason": "Signed host roster confirms this historical correction",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-20T10:00:00.000Z",
  "intent": {
    "status": "checked_in"
  }
}
GET/api/v1/admin/marketing/eligibility-optionsBearer token

Load venue campaign filter selections

Returns venue-owned purchase and promotion catalog selections, including retired items used by historical rules, plus the categories supported by the venue capabilities. Appointment-only and mixed venues use the same catalog authority. The authenticated venue supplies the scope. No customer history or targeting authority is returned. Permission: marketing.flash_sales.

Parameters, scopes and examples

Required scopes

marketing.flash_sales
POST/api/v1/admin/marketing/audience-previewBearer token

Preview a mobile flash-sale audience

Returns consent, preference, contact and suppression-aware email/SMS reach for a venue-scoped marketing audience. audience_filter.eligibility accepts shared V1 commerce include/exclude rules for current pass state, completed purchases and prior promotion use, including inclusive custom dates in the venue timezone. Valid rules return policy code COMMERCE_AUDIENCE and are evaluated server-side; unresolved facts produce an unavailable matched count. Unknown or malformed rules are refused without being dropped. Health, attendance and fitness-behaviour sources are not accepted. Permission: marketing.flash_sales.

GET/api/v1/admin/payments/{id}/refund/reviewBearer token

Load refund dialog context

Returns canonical refundable headroom, currency_decimal_places, payer receipt contacts, receipt_channels availability for Email/SMS/Push with explicit disabled reasons, original card brand/last4, venue refund destinations, and eligibility. Notification availability uses the same proven recipient and preference checks as the refund workflow. Native clients must use this response instead of deriving refund options locally. Staff refund notification choices start empty; only explicit channels request contact.

Parameters, scopes and examples

Required scopes

billing.refunds.same_daybilling.refunds.full

Path parameters

idstring · required
Payment UUID
POST/api/v1/admin/payments/{id}/refund/reviewBearer token

Create immutable refund review

Idempotency-Key required. Body uses major units: { amount, reason? (staff-only), client_receipt_comment? (client-visible), destination_id? (original or venue method id), method_reference?, guest_booking_id? }. Returns the immutable review fields and full-refund confirmation phrase. While the payment has an open critical refund recovery case (processor_result_ambiguous, refund_state_mismatch or venue_funds_restore_required) the review is refused with 409 REFUND_RECOVERY_CASE_OPEN; resolve or re-issue on the web payment detail first.

Parameters, scopes and examples

Required scopes

billing.refunds.same_daybilling.refunds.full

Path parameters

idstring · required
Payment UUID
GET/api/v1/admin/marketing/flash-salesBearer token

Venue flash-sale operations

Recent published flash sales with promo code, lifecycle_status, linked campaign, and delivery outcomes for the Business app. Includes email_cta_destination {type:"offer"} or {type:"custom",url:string}; legacy rows default to offer. Private draft_input is never returned; drafts use /admin/marketing/flash-sales/drafts. Permission: marketing.flash_sales.

POST/api/v1/admin/marketing/flash-salesBearer token

Create and announce a flash sale

Creates a venue-scoped promo and public offer, optionally queuing a consent-gated email/SMS campaign. Idempotency-Key required. Permission: marketing.flash_sales; non-empty notification_channels also requires notifications.send before business mutation and X-BookingBible-Confirm-Delivery: QUEUE. Optional X-BookingBible-Reviewed-Recipient-Count compares reviewed reach with the fresh consent-safe audience before mutation. Optional email_cta_destination is {type:"offer"} (also the omission default) or {type:"custom",url:string}: an absolute HTTPS URL of at most 2048 characters without credentials, control characters or backslashes. Invalid input returns 400 VALIDATION_ERROR. The destination changes only the email button; SMS and public purchase links remain canonical venue offer links. Generated announcement copy does not reveal the internal coupon. This setting never selects a delivery channel or authorizes sending.

Parameters, scopes and examples

Flash-sale fields plus optional email-only button destination. Existing notification and confirmation controls still apply.

Request body

{
  "name": "September offer",
  "notification_channels": "none",
  "email_cta_destination": {
    "type": "custom",
    "url": "https://venue.example/offer-details"
  }
}
PATCH/api/v1/admin/marketing/flash-sales/{id}Bearer token

Deactivate a flash sale

Deactivates the sale and its linked promo code. Idempotency-Key required. Permission: marketing.flash_sales.

Parameters, scopes and examples

Path parameters

idstring · required
Flash sale UUID
GET/api/v1/admin/marketing/campaignsBearer token

Recent campaign outcomes

Venue-scoped campaign lifecycle and delivery metrics for the Business app. Campaign type is email, sms, both (email + SMS only), or push. Push uses subject as title and sent_count as recipient acceptance count, not device count or proof of delivery. Reading history and saving unscheduled drafts require marketing.campaigns, not notifications.send. Scheduled creation, queueing, sending, and actual test delivery additionally require notifications.send in the canonical campaign engine; no destination or logo setting grants delivery authority.

GET/api/v1/admin/marketing/review-promptsBearer token

Review-request delivery activity

Explains current automatic review-request rules and returns anonymized recent delivery outcomes. Permission: marketing.reviews.

POST/api/v1/admin/marketing/flash-sales/{id}/duplicateBearer token

Duplicate a flash sale into a private draft

Idempotency-Key required and used as the operation key. Resolves {id} in the authenticated organization, runs UTC flashSaleDuplicatePrefill, and inserts a deterministic draft UUID derived from org/actor/source/action/key. Unique conflict replays the matching org/actor/draft row without overwrite. Never mutates the source, creates a promo, publishes, or sends. The copy is a standalone sale; web admin "+ Add run" (a draft in the same run series, migration 109) is not part of this endpoint and its series fields are not returned. Permission: marketing.flash_sales.

Parameters, scopes and examples

Required scopes

marketing.flash_sales

Path parameters

idstring · required
Source flash sale UUID
GET/api/v1/admin/marketing/flash-sales/draftsBearer token

List resumable private flash-sale drafts

Returns at most 50 organization-scoped draft rows (id, name, window, products, edit_revision). Raw draft_input, promo codes and campaigns are omitted. Permission: marketing.flash_sales.

Parameters, scopes and examples

Required scopes

marketing.flash_sales
GET/api/v1/admin/marketing/flash-sales/drafts/{draftId}Bearer token

Load sanitized flash-sale draft editor input

Returns validated FlashSaleDraftWire plus edit_revision for a private draft in the authenticated organization. Invalid stored input fails closed. Permission: marketing.flash_sales.

Parameters, scopes and examples

Required scopes

marketing.flash_sales

Path parameters

draftIdstring · required
Draft UUID
PATCH/api/v1/admin/marketing/flash-sales/drafts/{draftId}Bearer token

Save a private flash-sale draft

Idempotency-Key required. Save-only full FlashSaleInput round-trip including presentation and mixed family. Requires expected_revision compare-and-set; stale revisions return 409 DRAFT_CHANGED. Not publication. Permission: marketing.flash_sales.

Parameters, scopes and examples

Required scopes

marketing.flash_sales

Path parameters

draftIdstring · required
Draft UUID
POST/api/v1/admin/marketing/flash-sales/drafts/{draftId}/publishBearer token

Publish a reviewed flash-sale draft

Idempotency-Key required. Converts the same draft row to published through publish_flash_sale_v1. Body is FlashSaleDraftWire plus expected_revision. Returns draft_transition {status:published, draft_id, published_sale_id}. Same key/fingerprint replays the durable receipt. A new key against the same draft returns 409 DRAFT_ALREADY_PUBLISHED. Non-none notification_channels require notifications.send and X-BookingBible-Confirm-Delivery: QUEUE before the RPC. Optional replace-the-running-sale: a 422 FLASH_SALE_OVERLAP may include error.details.replaceable_sale; repeating the request with X-BookingBible-Replace-Sale-Id and X-BookingBible-Replace-Sale-Revision (both or neither) ends that sale and publishes in one transaction through publish_flash_sale_v2, carrying its promo claims into max_redemptions and adding replacement to the response; other overlaps still fail. Permission: marketing.flash_sales.

Parameters, scopes and examples

Required scopes

marketing.flash_sales

Path parameters

draftIdstring · required
Draft UUID
GET/api/v1/admin/gift-cardsBearer token

List venue gift cards

Returns the venue gift-card ledger for the Business app, including remaining balance and module-off historical rights. Optional code query looks up one card. New issue/options remain module-gated. Permission: gift_cards.sell.

Parameters, scopes and examples

Required scopes

gift_cards.sell

Query parameters

codestring
Exact gift-card code for a balance lookup
POST/api/v1/admin/gift-cardsBearer token

Issue a desk gift card

Creates or recovers one original venue gift card. Delivery is default-silent: omitted send_notification never delivers. send_notification=true requires notifications.send plus a recipient. Idempotency-Key required; NEW issues require bb-gift-issue-v1-<UUID v4>. Already-bound originals recover with their exact original key/body; unbound legacy keys require review and never mint. Success adds purchase_preserved=true and operation_id to id/code/amount/notification_sent/warning. 409 IDEMPOTENCY_KEY_REUSE_MISMATCH, GIFT_CARD_ISSUE_PROCESSING or GIFT_CARD_ISSUE_REVIEW_REQUIRED retain the original operation; never retry with a fresh key. HTTP cache TTL is not issuance authority. Permission: gift_cards.sell.

Parameters, scopes and examples

Required scopes

gift_cards.sell

Desk gift-card issue request

Request body

{
  "gift_type": "custom_amount",
  "amount": 500,
  "mark_as_paid": true,
  "send_notification": false
}
GET/api/v1/admin/gift-cards/optionsBearer token

Gift-card issue options

Minimum amount and giftable pass types for desk issue. Module gift_cards. Permission: gift_cards.sell.

Parameters, scopes and examples

Required scopes

gift_cards.sell
GET/api/v1/admin/gift-cards/{id}Bearer token

Gift-card detail and balance

One venue gift card including remaining_amount. Permission: gift_cards.sell.

Parameters, scopes and examples

Required scopes

gift_cards.sell

Path parameters

idstring · required
Gift card UUID
POST/api/v1/admin/gift-cards/{id}/sendBearer token

Send a paid gift card

Explicit, default-silent staff delivery of an already-paid gift card. channels chooses Email/SMS/Push; omitted or empty sends nothing. Recipient overrides freeze with the delivery and do not change purchase fields. Optional predecessors email/sms UUIDs identify an explicitly confirmed successor to a known terminal delivery; omitted means the one initial channel delivery. Changed Idempotency-Key values cannot fork either identity. Per-channel accepted, delivered, pending, needs_review, unavailable or failed results preserve the completed purchase; accepted is not a delivery receipt. Ambiguous provider outcomes remain held and HTTP claims are not released. Push reports unavailable without a durable recipient binding. Requires gift_cards.sell, members.contact, and notifications.send. Idempotency-Key required.

Parameters, scopes and examples

Required scopes

gift_cards.sellmembers.contactnotifications.send

Path parameters

idstring · required
Gift card UUID

Explicit post-sale gift delivery; optional predecessors map email/sms to terminal delivery UUIDs for a separately confirmed resend

Request body

{
  "channels": {
    "email": true,
    "sms": true
  },
  "recipient_email": "recipient@example.com",
  "recipient_phone": "+4512345678"
}
GET/api/v1/admin/appointments/inquiriesBearer token

List booking inquiries

Phone-useful booking-inquiry inbox. Returns enabled=false and items=[] when the venue has no inquiry-enabled service. Permission: bookings.manage.

Parameters, scopes and examples

Required scopes

bookings.manage
GET/api/v1/admin/appointments/inquiries/{id}Bearer token

Booking inquiry detail

Contact, preferred time, message, and conversation for one booking inquiry. Permission: bookings.manage.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Inquiry UUID
POST/api/v1/admin/appointments/inquiries/{id}/respondBearer token

Respond to a booking inquiry

Delivery is default-silent. send_notification=true sends the inbox reply and requires notifications.send; otherwise the body is stored as an internal staff note. Idempotency-Key required. Permission: bookings.manage.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

idstring · required
Inquiry UUID

Inquiry response. send_notification defaults false.

Request body

{
  "body": "We can do Thursday at 10.",
  "send_notification": false
}
GET/api/v1/admin/services/bundlesBearer token

List combo packages

Phone-useful combo-package list with price and item count. Full bundle builder stays on web admin. Permission: bookings.manage.

Parameters, scopes and examples

Required scopes

bookings.manage
GET/api/v1/admin/staffBearer token

List staff

Venue-membership staff directory: name, role, membership_status and capabilities; is_active is true only for active membership. Includes additive service-delivering collaborators, but returns email/phone as null when the profile belongs to another home venue. Assignment selectors must use active status and actual delivery capabilities, never the staff label alone. No payroll or commission data. Permission: staff.view.

Parameters, scopes and examples

Required scopes

staff.view
GET/api/v1/admin/sites/statusBearer token

Website status snapshot

Returns the org-scoped venue website summary for the Business app: site identity (incl. brand/location coverage), draft/published versions, preview URL, publish-readiness blockers/warnings, connected custom-domain verification/SSL state, and the additive per-site `entitlement` block (kind paid|trial|expired|legacy_preview|none, can_write_draft, can_publish, trial_expires_at, trial_remaining_ms — S12b). Permission: sites.view.

Parameters, scopes and examples

Required scopes

sites.view
POST/api/v1/admin/sites/publishBearer token

Publish the venue website

Publishes one tenant-bound website through the canonical publish core. Requires sites.publish plus a stable Idempotency-Key. Returns readiness blockers when the draft is not yet publishable; older mobile builds may ignore additive warnings.

Parameters, scopes and examples

Required scopes

sites.publish

Tenant-scoped website publish request

Request body

{
  "site_id": "00000000-0000-4000-8000-000000000001",
  "note": "Published after mobile review"
}
POST/api/v1/admin/sites/ai/chatBearer token

Talk to the website builder AI

Business-app SSE transport for the org-scoped website builder assistant. Requires sites.manage. Returns the mobile AI event vocabulary over Server-Sent Events and adds `site_patch` events so clients can refresh status mid-turn.

Parameters, scopes and examples

Required scopes

sites.manage

Tenant-bound site-builder message

Request body

{
  "site_id": "00000000-0000-4000-8000-000000000001",
  "conversation_id": null,
  "message": "Make the homepage warmer and highlight workshops.",
  "attachment_ids": [
    "00000000-0000-4000-8000-000000000002"
  ]
}
POST/api/v1/admin/sites/attachmentsBearer token

Upload a website-builder attachment

Multipart upload for the Business website builder chat. Requires sites.manage. The file is stored through the shared AI attachment pipeline and later referenced by `attachment_ids` on the site chat route.

Parameters, scopes and examples

Required scopes

sites.manage
DELETE/api/v1/admin/sites/attachmentsBearer token

Delete a pending website-builder attachment

Deletes one tenant-owned, not-yet-bound website-builder attachment before it is sent in chat. Requires sites.manage.

Parameters, scopes and examples

Required scopes

sites.manage
GET/api/v1/admin/passes/{passId}/configurationBearer token

Get a client Flexible membership configuration

Returns the frozen current allowance, published recurring choices, pricing version, and pending renewal change for an organization-owned issued pass. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Issued pass UUID
GET/api/v1/admin/gift-cards/{id}/sendBearer token

Gift-card delivery availability

Read-only per-channel availability and current deliveries (delivery_id, status, retryable) for a paid gift card, including module-off historical rights. Provider accepted is distinct from delivered; needs_review cannot be blindly retried. Push is unavailable until a gift has a durable recipient-account/device binding; the purchaser is never substituted. Requires gift_cards.sell, members.contact, and notifications.send.

Parameters, scopes and examples

Required scopes

gift_cards.sellmembers.contactnotifications.send

Path parameters

idstring · required
Gift card UUID
POST/api/v1/admin/passes/{passId}/configurationBearer token

Preview or schedule a client Flexible membership change

Previews or schedules a quantity/unlimited change for renewal 1–24 against an unexpired quote. Organization ownership is enforced and the new entitlement applies only after the target renewal invoice is paid. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Issued pass UUID
DELETE/api/v1/admin/passes/{passId}/configurationBearer token

Cancel a client pending Flexible membership change

Cancels the organization-owned pass’s pending future allowance change without altering its current frozen entitlement. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Issued pass UUID
GET/api/v1/admin/pass-types/clips-empty-offersBearer token

List extra-class offer configuration

Returns active limited pass types, eligible target pass types (including hidden staff-only items), and the current exhausted-credit offer mappings. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage
PATCH/api/v1/admin/pass-types/clips-empty-offersBearer token

Configure an extra-class offer

Creates, replaces, or removes the one automatic clips-empty offer for a limited recurring pass or still-valid class pack. The target may be hidden from public catalogs. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Source pass and nullable offer target

Request body

{
  "source_pass_type_id": "00000000-0000-4000-8000-000000000001",
  "target_pass_type_id": "00000000-0000-4000-8000-000000000002"
}
GET/api/v1/admin/pos/flash-salesBearer token

List currently redeemable POS flash sales

Read-only desk catalog of published, active, in-window flash sales with at least one available promo. Returns organization_id plus id, name, applicable_pass_type_ids, ends_at, rule_label. Promo codes are never returned. Permission: pos.access with venue-wide location access. Does not require marketing.flash_sales. Final eligibility stays on pass-preview/sale.

Parameters, scopes and examples

Required scopes

pos.access
GET/api/v1/admin/pos/flash-sales/endedBearer token

List recently ended flash sales for a staff exception

STAFF-PROMO-WINDOW-EXCEPTION-01. Same { organization_id, sales[] } shape as GET /admin/pos/flash-sales (id, name, applicable_pass_type_ids, ends_at, rule_label; never a promo code), listing published, still-active sales that ended by time in the last 90 days whose code is active and below its total caps. Deactivated sales are never listed. Permission: pos.access with venue-wide location access AND marketing.flash_sales in the same venue (403 otherwise). An ended sale is sold only with promo_window_exception { kind: flash_sale_after_end, reason } on POST /admin/pos/sale or /admin/memberships; final eligibility and caps stay on those calls.

Parameters, scopes and examples

Required scopes

pos.accessmarketing.flash_sales
POST/api/v1/admin/pos/expired-code-redemptionsBearer token

Redeem one expired partner/batch code for a client (staff exception)

STAFF-PROMO-WINDOW-EXCEPTION-01. Staff JWT with pos.access OR pos.sell, venue-wide location access AND marketing.campaigns in the same venue. Body: { member_id, code, reason } (reason 5-500 characters, kept in the audit log). Waives ONLY the code’s own expiry and its campaign’s redeem-by deadline (expired in the last 90 days); a voided, redeemed, reserved, staff-expired or inactive code, a cancelled/paused campaign, per-client and total limits and customer eligibility still refuse (422). A pass-granting code is redeemed through the canonical atomic grant claim and returns { outcome: granted, pass_id, code_label }; replays for the same code and client converge. A code that unlocks a flash sale returns { outcome: sale_required, code_label, pass_type_ids } and is sold with promo_window_exception { kind: late_code_redemption, reason } on POST /admin/pos/sale or /admin/memberships. Expired discount campaign codes (422 LATE_DISCOUNT_CODE_UNSUPPORTED) and plain venue promo codes without a campaign (422 LATE_CODE_NOT_CAMPAIGN) are refused. Records a 30-minute database exception and an audit_log row; customer self-service stays refused.

Parameters, scopes and examples

Required scopes

pos.accessmarketing.campaigns

The client, the code as typed, and the staff reason

Request body

{
  "member_id": "uuid",
  "code": "DOWNTOWN-ABC123",
  "reason": "Client was away when the code expired"
}

Response example

{
  "data": {
    "outcome": "granted",
    "pass_id": "uuid",
    "code_label": "DOWNTOWN-ABC123"
  }
}
GET/api/v1/admin/pos/transactions/{transactionId}/returnsBearer token

Preview physical stock return evidence

Staff JWT only. Requires pos.access, products.manage_inventory and venue-wide location access. Returns transaction_id, receipt line_names, and evidence-backed items with product_id, current catalog product_name, sold_quantity, returned_quantity and returnable_quantity. Bundle constituents come from original sold movements, never current bundle definitions. Foreign/missing sales return the same 404. No monetary refund or sale-status dependency.

Parameters, scopes and examples

Required scopes

pos.accessproducts.manage_inventory
POST/api/v1/admin/pos/transactions/{transactionId}/returnsBearer token

Record an explicit physical inventory return

Staff JWT, pos.access plus products.manage_inventory, venue-wide location access. Required Idempotency-Key (8-160 ASCII letters/digits/colon/underscore/hyphen). Body {items:[{product_id,quantity}],reason}; unique products, positive integer quantities, 1-500 character reason. Atomically persists physical return, stock batch and audit. The full reason is private to the canonical return; stock movements use a fixed operational label and audit records only reason_present plus operation identifiers. Replays bind organization, actor, transaction, normalized items and reason. Returns return_id, transaction_id, stock_batch_id, items, reason, created_at, replayed; clients must match that receipt to the submitted transaction/items/reason before clearing the attempt. POS_RETURN_STALE (409) requires refreshing quantities; POS_RETURN_KEY_CONFLICT (409) rejects changed intent; POS_RETURN_UNAVAILABLE (503) retries the identical key/body. Inventory only: no refund, credit, receipt total, commission, entitlement or notification changes.

Parameters, scopes and examples

Required scopes

pos.accessproducts.manage_inventory
GET/api/v1/admin/pos/favoritesBearer token

List ranked quick-sale favorites

Returns ranked quick-sale tiles (admin pins first, then rolling-90-day volume, cap 8), the saved pin list, and the active pass/product catalog used to rank them. Uses getQuickSaleFavoritesForOrg. Permission: pos.access or pos.sell with venue-wide staff location access. Does not require settings.business.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/favoritesBearer token

Save admin/manager quick-sale pins

Replaces organizations.settings.pos.favorites through savePosFavoritePinsForOrg and updateOrgSettingsWithCas. A failed or missing settings read does not write. Unrelated settings and other pos keys are preserved. Body is { pinned: [{ kind: pass_type|product, id }] } (max 24). Returns the same ranked payload as GET. Permission: pos.manage (admin/manager). Does not rewrite settings.business.

Parameters, scopes and examples

Required scopes

pos.manage
GET/api/v1/admin/pos/pass-typesBearer token

List the staff POS pass catalog

Returns every active venue pass type for authenticated POS staff, including pass types intentionally hidden from public consumer catalogs, plus pricing_mode, published flexible_pricing_config, binding_tiers, vat_config/effective_vat, is_recurring, shop_visibility, category and price_amount. Permission: pos.access with venue-wide staff location access.

Parameters, scopes and examples

Required scopes

pos.access
GET/api/v1/admin/pos/servicesBearer token

List the staff POS service catalog

Returns active appointment services, their variants and active linked providers for authenticated POS staff. Class-only venues receive an empty list. Permission: pos.access.

Parameters, scopes and examples

Required scopes

pos.access
GET/api/v1/admin/pos/perksBearer token

List verified course perks for a POS client

Returns the selected venue member’s currently available product perk entitlements. Tenant membership is revalidated and entitlement reads fail closed. Claims are supported only on product/bundle-only sales; pass, service and mixed service carts are refused before collection, regardless of tender. Direct product payments commit perks atomically with sale effects. Permission: pos.access or pos.sell.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/pass-previewBearer token

Preview an authoritative standard pass total

Runs the canonical pass sale pricing, fee, discount and VAT engine without collection or writes. Optional additive flash_sale_id is resolved by the existing sale core; do not send promo_code with it. Backdated windows additionally require passes.manage. Permission: pos.access or pos.sell.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/product-sale-recoveryBearer token

Recover the original selected-currency product sale

Financial recovery by { operation_key }. Requires pos.access or pos.sell and venue-wide location access; the original immutable sold_by_staff_id must match the authenticated actor. Returns unresolved for missing, ambiguous, malformed, canceled, quarantined or inconsistent evidence; absence never permits a replacement collection. Pending replies include the frozen original currency, amount_minor, total, subtotal, vat_amount, member_id, items and product_price_book_versions. Completed replies additionally include validated transaction_id, payment_id and payment_status after matching both scoped ledger records to the original operation. For a bound or succeeded operation, retrieves the exact frozen Stripe intent on its original account. Only verified provider success can atomically create or adopt the original payment and POS transaction and complete the frozen items and stock through the canonical completion RPC. Never creates or confirms a provider intent or sends a receipt. Quarantined operations require manual reconciliation. Only completed recovery may release a browser marker. POST keeps original keys out of URLs; responses are no-store.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/product-currenciesBearer token

Discover qualified currencies for an ordinary product basket

Read-only discovery by { member_id, items } for ordinary product quantities, without an initial currency or payment key. Requires pos.access or pos.sell, venue-wide location access and a scoped member. Returns { available_currencies: string[] }, the exact-price intersection of active product books for verified home-country catalog tax, qualified precision, owned nonrecurring products, quantity limits and available SQL recovery. Unsupported variants, benefits and commissions remain refused. Empty choices never authorize venue-currency fallback. Responses are no-store. Each chosen currency still requires its own signed catalog-preview and exact original sale/finalize body. Discovery creates no payment operation.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/catalog-previewBearer token

Preview an authoritative POS basket total

Quotes any multi-line register basket that POST /admin/pos/sale completes as one sale: products and bundles (product engine) or services with products (service engine). Runs canonical catalog repricing, course-perk resolution (product/bundle baskets), discount composition and per-line VAT without collection or writes, and returns { subtotal, vatAmount, total, discountAmount, currency }. Optional product_currency selects exact gross product books for an authenticated member with home-country evidence and ordinary product-only lines; the response additionally returns product_price_book_versions and product_quote, a signed proof of actor, member, residence evidence, exact items and inclusive tax provenance. Send the exact product_quote, those versions, that currency and total as expected_total to the sale endpoint; preserve them across retries and SCA finalize. Commission/benefit/foreign-country refusals are 422 PRODUCT_PRICE_BOOK_UNAVAILABLE. Pass lines return 422 PRODUCTS_REQUIRED (use pass-preview); bundles with services return 422 MIXED_CART_NOT_SUPPORTED; products.max_quantity_per_order returns 422 PRODUCT_QUANTITY_LIMIT with details; a register-access denial returns 403. Permission: pos.access or pos.sell.

Parameters, scopes and examples

Required scopes

pos.access
GET/api/v1/admin/pos/bundlesBearer token

List the staff POS product-bundle catalog

Returns active product bundles for authenticated POS staff. Permission: pos.access.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/gift-cardsBearer token

Sell a gift card through the register

Creates a POS gift-card sale through posSellGiftCard (payment + pos_transactions). Silent by default. Permission: pos.sell. Idempotency-Key required.

Parameters, scopes and examples

Required scopes

pos.sell
POST/api/v1/admin/pos/flexible-pass/previewBearer token

Preview a Flexible pass quote at the register

Returns the same short-lived server quote used by desk Flexible sales. Selection uses passSelectionSchema ({kind:quantity|unlimited} or {kind:option, optionId}). Permission: pos.access or pos.sell.

Parameters, scopes and examples

Required scopes

pos.access
GET/api/v1/admin/pos/eodBearer token

Load end-of-day reconciliation inputs

Returns original tender legs, recorded refund allocations, account-scoped Stripe settlements and the close record for a venue-local ledger date. Unverified methods cannot establish a balanced day. Refunds use their recorded creation date; this is not a fiscal Z report. Permission: pos.access.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/eod/closeBearer token

Close the register for a business date

Recalculates and stores the current end-of-day reconciliation per organization and business date. A later close replaces that date’s stored record; historical close revisions are not yet retained. Unverified lines remain unbalanced. Permission: pos.sell.

Parameters, scopes and examples

Required scopes

pos.sell
GET/api/v1/admin/pos/kioskBearer token

Read kiosk register configuration

Returns kiosk enablement, receipt mode and idle timeout without staff PIN secrets. Permission: pos.access.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/kiosk/pinBearer token

Validate a kiosk staff PIN

Checks a staff PIN against the venue kiosk configuration. Permission: pos.access.

Parameters, scopes and examples

Required scopes

pos.access
GET/api/v1/admin/passes/{passId}/flexible-switchBearer token

Get the Move-to-Flexible options for a client membership

FLEX-RETIRE-01 — ADMIN-ONLY. Returns the source fixed-price membership, its next renewal date (the only possible effective date), the sellable Flexible pass types with their published allowance ranges, and any pending switch. Never offered to a member. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Issued pass UUID (the fixed-price membership being retired)
POST/api/v1/admin/passes/{passId}/flexible-switchBearer token

Preview or schedule a Move to Flexible at next renewal

action=preview returns the ordinary desk-mint review (pricing, timeline, saved card, review fingerprint, Flexible quote) anchored on the old renewal date with the registration fee waived. action=schedule echoes the quote + review fingerprint + a caller-owned attempt id: it mints the Flexible membership pending_activation on that date, schedules the old subscription to end at its current period end, and links the two on the membership operation. Idempotent per attempt; a second pending switch is refused 409 SWITCH_PENDING. Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Issued pass UUID (the fixed-price membership being retired)

Preview or schedule body

Request body

{
  "action": "preview",
  "target_pass_type_id": "00000000-0000-4000-8000-000000000002",
  "selection": {
    "kind": "quantity",
    "quantity": 4
  }
}
DELETE/api/v1/admin/passes/{passId}/flexible-switchBearer token

Cancel a scheduled Move to Flexible

Reversible until the effective date: undoes the old subscription’s scheduled cancellation first, then voids the pending Flexible pass (nothing was charged). Permission: passes.manage.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Issued pass UUID (the fixed-price membership being retired)
GET/api/v1/admin/checkin/{classInstanceId}/streamBearer token

Class stream controls

Business-app stream-control DTO for one tenant-bound class instance. Returns source options without provider live-stream ids, stream keys, RTMP URLs, SRT URLs, or playback URLs; action availability includes exact disabled reason codes. Visible to venue schedule/check-in readers and assigned staff roster readers. Source/go-live writes repeat module/settings gates; end remains available for safe live shutdown.

Parameters, scopes and examples

Required scopes

booking.checkinschedule.view_allclass.createstaff_portal.roster.view

Path parameters

classInstanceIdstring · required
Class instance UUID
PATCH/api/v1/admin/checkin/{classInstanceId}/stream/sourceBearer token

Select class stream source

Sets or clears the occurrence-level stream source before the class goes live. Requires class.create, active streaming module/entitlement, a streamable class, an active RTMP/SRT source with an attached provider stream, tenant binding, lifecycle CAS status=scheduled, UUID Idempotency-Key, and no assigned-instructor owner-toggle block. The operation is silent; notification fields are rejected.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID

Occurrence-level source selection

Request body

{
  "stream_source_id": "00000000-0000-4000-8000-000000000001"
}
POST/api/v1/admin/checkin/{classInstanceId}/stream/go-liveBearer token

Start class livestream

Starts a class stream from an existing class provider stream or an active selected RTMP/SRT source. Requires class.create, active streaming entitlement/module, streaming settings enabled, streamable class, venue-local class date, tenant binding, lifecycle CAS status=scheduled, provider-adapter enablement, UUID Idempotency-Key, and no assigned-instructor owner-toggle block. Provider enablement is compensated when the DB transition fails or loses a race unless the winner uses the same stream. The route does not mint new one-time provider streams or expose source credentials; it is operationally silent and rejects notification fields.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID

Optional explicit source for this go-live attempt

Request body

{
  "stream_source_id": "00000000-0000-4000-8000-000000000001"
}
POST/api/v1/admin/checkin/{classInstanceId}/stream/endBearer token

End class livestream

Ends a live class stream. Requires class.create, tenant binding, lifecycle CAS status=live, and UUID Idempotency-Key. The class completion commits before the shared-stream provider disable guard runs; the response reports provider_stop as disabled, skipped_shared, failed, or not_applicable. The route records streaming usage after the class completion commit. It is operationally silent and rejects notification fields.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/stream/retry-provider-shutdownBearer token

Retry completed class provider shutdown

Retries only the provider disable step after a class has already completed; it never re-completes the class or repeats lifecycle effects. Requires class.create, tenant binding, UUID Idempotency-Key, and an attached provider stream. The shared-stream guard is organization-scoped and no blocking occurrence identifier is returned. This route is operationally silent and rejects notification fields.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisherBearer token

Prepare phone publisher session

PHONE-PUBLISH-01: mints an ephemeral, class-scoped provider live stream plus a hashed single-use claim token, bound to one organization, one class occurrence, and the preparing user, with a short server-enforced TTL. Requires class.create OR the assigned instructor when the venue enables instructor go-live, active streaming module/entitlement, streamable class, venue-local class date, the phone_publisher_sessions kill switch, tenant binding, and a UUID Idempotency-Key. Returns non-secret session state and the one-time claim token; NEVER ingest URLs, stream keys, or provider resource ids. One active session per class; supersession refuses while another device is actively publishing with a fresh heartbeat. On a live class paused by the revoke of the previous phone session (or a session prepared to resume it), prepare is allowed and REUSES that provider stream instead of minting one; a live class that is not paused, paused by the operator, or paused on a venue encoder stream answers 409 ALREADY_LIVE.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID

Optional device label shown to staff

Request body

{
  "device_label": "Front-desk iPhone"
}
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisher/claimBearer token

Claim phone publisher credential (one-time)

PHONE-PUBLISH-01: atomically consumes the single-use claim token and returns short-lived RTMPS ingest material exactly once (Cache-Control: private, no-store). Only the session creator may claim. Deliberately NOT idempotency-cached — a duplicate claim returns CLAIM_ALREADY_USED and the recovery path is revoke + prepare a new session; the old secret is never re-displayed. No ingest material is ever stored server-side.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID

One-time claim token from prepare

Request body

{
  "claim_token": "base64url-opaque-token",
  "device_label": "Front-desk iPhone"
}
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisher/startBearer token

Report phone publisher started

PHONE-PUBLISH-01: transitions the claimed session to publishing and returns the authoritative session/provider/class status snapshot. Never marks the class live — only the provider active webhook does. Owner-bound, tenant-bound, UUID Idempotency-Key required.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisher/heartbeatBearer token

Phone publisher heartbeat/status

PHONE-PUBLISH-01: periodic liveness touch returning session state, provider connection status (ingest fields stripped), and class lifecycle status. When the provider confirms an active input and the class is still scheduled inside the phone-publisher window, the snapshot reconciles the class to live (CAS; the cron sweep is the backstop). Owner-bound and naturally idempotent, so no Idempotency-Key is required. A stale heartbeat makes a publishing session eligible for takeover by another authorized device.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisher/endBearer token

End phone publisher session

PHONE-PUBLISH-01: ends the active phone publisher session, completes the class when this session took it live (usage recorded after the completion commit), and records a DURABLE provider-cleanup outcome — a failed teardown is surfaced in GET state and retried, never hidden. Requires class.create or the permitted assigned instructor, tenant binding, and a UUID Idempotency-Key.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisher/revokeBearer token

Revoke phone publisher session

PHONE-PUBLISH-01: revokes a session whose device was lost or reinstalled or should no longer publish; the old secret is never re-displayed and a fresh session must be prepared and claimed. When the stream of the revoked session is the one that took the class live, the class is PAUSED (`stream_paused_at`, outcome `class_paused: true`) rather than completed, so a following prepare resumes on the same provider stream (viewers keep their playback id). A self-revoke keeps that stream enabled (`provider_stop.status: skipped_paused`); a take-over/admin revoke disables it (kicks the publisher) and the resume re-enables it. Only `…/end` completes the class. Requires class.create or the permitted assigned instructor, tenant binding, and a UUID Idempotency-Key.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/stream/phone-publisher/retry-cleanupBearer token

Retry phone publisher provider cleanup

PHONE-PUBLISH-01: explicitly retries a failed or pending provider teardown for a terminal publisher session. Requires class.create or the permitted assigned instructor, tenant binding, and a UUID Idempotency-Key.

Parameters, scopes and examples

Required scopes

class.create

Path parameters

classInstanceIdstring · required
Class instance UUID
PATCH/api/v1/admin/schedule/{id}Bearer or API key

Edit class instance

Update start/end time, instructor, class type, capacity, or room on a single class instance. Physical and online capacity use a class-row lock and current counts; optional expected_updated_at rejects a stale edit. Concurrent count/version conflicts return 409; preliminary capacity validation may return 422 CAPACITY_BELOW_BOOKED. Class-type changes recheck retained teachers against the new type and brand. Rooms must belong to the occurrence venue/location and fit the resulting capacity. Notifications default silent: omitted controls and legacy notify_attendees never send. notify.{clients,instructor}.channels (or the older notify.{audience,channels}) sends the branded schedule-change email/SMS/push on time/instructor/room changes: clients get class_schedule_changed, instructors class_schedule_changed_instructor. A chosen plan requires notifications.send; a channel that reaches nobody returns 422 NOTIFICATION_CHANNEL_UNAVAILABLE {audience, channel, reason_code, unavailable_reason} before the edit; mixing shapes returns 422 MIXED_NOTIFY_SHAPES. Success adds notify_outcome (per audience × chosen channel {sent, skipped, failed, pending, reasons}) or null. Idempotency-Key honored; audit_log carries per-field from/to diffs.

Parameters, scopes and examples

Required scopes

write:schedule

Path parameters

idstring · required
Class instance ID

Class instance patch

Request body

{
  "start_time": "2026-07-10T08:00:00+02:00",
  "end_time": "2026-07-10T09:00:00+02:00",
  "instructor_id": "uuid",
  "capacity": 24,
  "notify": {
    "clients": {
      "channels": [
        "email"
      ]
    },
    "instructor": {
      "channels": [
        "email",
        "push"
      ]
    }
  }
}
POST/api/v1/admin/schedule/{id}/historical-correctionsBearer token

Correct a past class instance with an audit record

Atomic, immutable-ledger correction for a past class. Requires a UUID Idempotency-Key, scheduling.manage_history plus scheduling.manage, expected_updated_at and expected_status for an existing row, a past effective_at, and typed REWRITE attestation. Assignment-only corrections preserve linked roster, financial, course, workshop, import and streaming records; all other corrections on linked classes require specialist review. Existing tenant, location and instructor erasure guards remain enforced. Cancellation-state corrections additionally require class.cancel. Notifications are always silent.

Parameters, scopes and examples

Required scopes

scheduling.manage_history

Path parameters

idstring · required
Class instance ID

Bounded historical class-instance correction

Request body

{
  "operation": "class_instance.correct_timing",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_status": "completed",
  "history_reason": "Signed instructor log confirms the recorded class time",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-20T10:00:00.000Z",
  "intent": {
    "startTime": "2026-08-20T08:00:00.000Z",
    "endTime": "2026-08-20T09:00:00.000Z"
  }
}
POST/api/v1/admin/staff/{staffId}/availability/windows/{id}/historical-correctionsBearer token

Correct a staff availability history window

Atomic, immutable-ledger correction for a past recurring availability window. Requires a UUID Idempotency-Key, availability.manage_history, staff_portal.availability, staff.edit for another staff member, expected_updated_at plus expected_is_active for existing rows, a past effective_at, and typed REWRITE attestation. The staff path and tenant-owned row pin ownership. Notifications are always silent.

Parameters, scopes and examples

Required scopes

availability.manage_history

Path parameters

staffIdstring · required
Availability owner user ID
idstring · required
Availability window ID

Bounded historical availability correction

Request body

{
  "operation": "availability_window.correct_effective_period",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_is_active": true,
  "history_reason": "Approved rota shows this earlier effective period",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-10T10:00:00.000Z",
  "intent": {
    "effectiveFrom": "2026-08-01",
    "effectiveUntil": "2026-08-15"
  }
}
POST/api/v1/admin/schedule/bulk-editBearer token

Edit selected class instances

Applies explicit venue-local start/end time, room, instructor, capacity, or class type to 1–50 tenant-owned classes with per-row conflict/failure results. Notifications default silent; notify.{clients,instructor}.channels (or the older notify.{audience,channels}) requires notifications.send, is checked against the selection before any write (422 NOTIFICATION_CHANNEL_UNAVAILABLE {audience, channel, reason_code, unavailable_reason}) and sends only after each successful write. Success adds notify_outcome merged across classes, or null. Idempotency-Key required.

Parameters, scopes and examples

Required scopes

write:schedule

Selected class edits

Request body

{
  "instance_ids": [
    "uuid"
  ],
  "set": {
    "room_id": "uuid",
    "capacity": 24
  },
  "time": {
    "start": "09:00",
    "end": "10:00",
    "timezone": "Europe/Copenhagen"
  },
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": true
    }
  }
}
POST/api/v1/admin/schedule/bulk-cancelBearer token

Cancel selected class instances

Cancels 1–50 tenant-owned classes in one transaction: statuses, consumed credit returns, counters, audit and durable effects commit together. A refusal returns 409 CANCELLATION_CONFLICT with no partial business mutation. Success retains succeeded/succeeded_count/failed and adds committed, operation_id and effects_status (completed, pending or held). Optional expected_versions maps IDs to row versions. Every target requires current accessible-location scope. Parsed intent and active organization scope the key; changed intent or venue returns 409 IDEMPOTENCY_KEY_REUSE_MISMATCH. A positive cache entry resumes durable effects. An unconfirmed commit returns 503 CANCELLATION_COMMIT_UNCONFIRMED with operation_id, committed=unknown and retry_same_operation=true in error.details; preserve the reviewed body and Idempotency-Key. Notifications default silent: omission and legacy notify_attendees never send. Explicit notify audience plus channel requires notifications.send; clients and instructors support Email/SMS/Push. notify.{clients,instructor}.channels gives each audience its own channels (an instructor-only notice needs no client channel); the legacy notify.{audience,channels} shape gives the instructor the client channels minus Push, and its Push needs the client audience (422). Mixing both shapes returns 422. A chosen channel that reaches nobody in the selection returns 422 NOTIFICATION_CHANNEL_UNAVAILABLE with {audience, channel, reason_code, gate_code, unavailable_reason} before any class changes. Success adds notify_outcome: per audience × chosen channel {sent, skipped, failed, pending, reasons[{code, count, label}]}. Idempotency-Key required.

Parameters, scopes and examples

Required scopes

write:schedule

Selected class cancellations

Request body

{
  "instance_ids": [
    "uuid"
  ],
  "reason": "Instructor unavailable",
  "notify": {
    "audience": {
      "clients": true,
      "instructor": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": true
    }
  }
}
POST/api/v1/admin/schedule/{id}/cancelBearer or API key

Cancel class

Atomically cancel a class and its bookings, return only actually consumed eligible credits, reset counters, and record audit plus durable delivery obligations. Optional expected_updated_at protects the reviewed version. A committed result adds operation_id and effects_status; pending/held delivery is not a failed cancellation. Retrying the same Idempotency-Key replays the operation. Notifications default silent: omission and legacy notify_attendees never send. Explicit notify.{audience,channels} or notify.{clients,instructor}.channels requires notifications.send; channel choices AND with recipient preferences. Clients and instructors support Email/SMS/Push. The per-audience shape gives the instructor its own channels (instructor-only is valid); the legacy shape gives the instructor the client channels minus Push, and its Push needs the client audience (422). Mixing both shapes returns 422. A chosen channel that reaches nobody returns 422 NOTIFICATION_CHANNEL_UNAVAILABLE with {audience, channel, reason_code, gate_code, unavailable_reason} before the class changes; success adds notify_outcome (per audience × chosen channel {sent, skipped, failed, pending, reasons}). Past classes return 409 HISTORICAL_CORRECTION_REQUIRED and must use the dedicated /api/v1/admin/schedule/{id}/historical-corrections endpoint with reason, REWRITE attestation, history permission and compare-and-set evidence. Idempotency-Key required. Unconfirmed commit returns 503 CANCELLATION_COMMIT_UNCONFIRMED with operation_id, committed=unknown and retry_same_operation=true in error.details. Keep the same reviewed body and key; never replace them after an uncertain outcome. Current notification and location authority are checked before replay. Keys are bound to parsed intent and active organization; mismatches return 409 IDEMPOTENCY_KEY_REUSE_MISMATCH. Durable replay resumes effects.

Parameters, scopes and examples

Required scopes

write:schedule

Path parameters

idstring · required
Class instance ID
POST/api/v1/admin/schedule/{id}/restoreBearer token

Review and restore a future cancelled class

Requires scheduling.manage and Idempotency-Key. Accepts expected_updated_at, restore_booking_ids, confirm_conflicts and optional reason. auto_rebook_clients defaults false; when explicitly true it selects eligible cancellation-snapshot IDs for review. Selected rebooking also requires bookings.manage. Old cancelled rows remain immutable; canonical eligibility creates new linked bookings with confirmed, waitlisted, ineligible or pending outcomes. One rejected client does not roll back activation. Notifications start silent. notify.{clients,instructor}.channels (or the older notify.{clients,instructor} booleans with notify.channels) requires notifications.send and is checked before mutation: a channel that reaches nobody returns 422 NOTIFICATION_CHANNEL_UNAVAILABLE {audience, channel, reason_code, unavailable_reason}. The instructor (class_restored_instructor) is told with the restore; clients (class_uncancelled) are told when their rebooking resolves — rebooked, waitlist place restored, or "book again" — through the durable staff notification outbox. Success includes committed, operationId, bookings, effectsStatus and notify_outcome (per audience × chosen channel {sent, skipped, failed, pending, reasons}); stale or historical rows return 409. Lost atomic acknowledgements return 503 REACTIVATION_COMMIT_UNCONFIRMED with operation_id, committed=unknown and retry_same_operation=true in error.details. Explicit selected booking IDs replay with the default auto_rebook_clients=false. Keep the same reviewed body and Idempotency-Key until the receipt resolves. Current location, selected-client booking and notification authority are checked before replay; a replay with a different notification plan is refused. Parsed intent and active organization scope the key; mismatches return 409 IDEMPOTENCY_KEY_REUSE_MISMATCH. Positive cached success resumes the durable restoration operation.

Parameters, scopes and examples

Required scopes

write:schedule

Path parameters

idstring · required
Class instance ID

Reviewed restoration with an optional per-audience notification plan

Request body

{
  "expected_updated_at": "2026-10-05T10:00:00.000Z",
  "restore_booking_ids": [
    "uuid"
  ],
  "confirm_conflicts": false,
  "notify": {
    "clients": {
      "channels": [
        "email",
        "push"
      ]
    },
    "instructor": {
      "channels": []
    }
  }
}
POST/api/v1/admin/schedule/{id}/substituteBearer or API key

Assign substitute

Replace instructor for a class. Validates no scheduling conflicts across locations. Notifications default silent. notify.{clients,instructor}.channels requires notifications.send: clients get class_schedule_changed and the instructor audience gets staff_assignment (the new substitute, and the teacher they replace) on every chosen channel (Email/SMS/Push). The older notify.{audience,channels} shape keeps its meaning (clients Push, substitute Email; other combinations 422). A channel that reaches nobody returns 422 NOTIFICATION_CHANNEL_UNAVAILABLE before the change. Success adds notify_outcome or null. Idempotency-Key is required and binds the caller, venue, class location and exact body. Replay rechecks current scheduling, location and selected notification authority. Concurrent requests are held while the scoped claim is retained; mismatched reuse returns 409. Unconfirmed outcomes return 503 SUBSTITUTE_COMMIT_UNCONFIRMED and require review without a new key. Definite scheduling conflicts remain 409 SCHEDULE_CONFLICT and permit a reviewed override. Expiring claims do not guarantee permanent deduplication.

Parameters, scopes and examples

Required scopes

write:schedule

Path parameters

idstring · required
Class instance ID
GET/api/v1/admin/checkinBearer or API key

Venue-local check-in day strip

The venue-local day's classes for the native staff check-in screen (Business app): per-class check-in/waitlist counts, room/instructor, and the day-navigation gates (today vs. read-only past/future). Defaults `date` to the venue-local today when omitted; optional `location_id` (query param or X-Location-ID header) narrows to one location. Each class carries additive `course_identifiers` (`{course_id, label, tone, course_name}`, hidden courses included; `[]` when none).

Parameters, scopes and examples

Required scopes

read:bookings

Query parameters

datestring
Venue-local date, YYYY-MM-DD. Defaults to the venue-local today.
location_idstring
Restrict results to one location. Also accepted as the X-Location-ID header.
GET/api/v1/admin/checkin/{classInstanceId}Bearer or API key

Attendee list

Class roster with member details, pass info, native course_access covering this booking, add-ons, included services, check-in status, and class/booking updated_at concurrency tokens. Whole-class cancellations retain the preserved roster and each attendee’s previous status; ordinary client cancellations remain excluded. Streaming bookers (attendance_type online) carry online_attendance { state: watching | attended | not_yet | null, playback_state, first_played_at, last_heartbeat_at } and are attended automatically when their stream plays; summary adds whole-class in_studio { total, confirmed, checked_in, no_show, waitlisted } and online { total, not_yet, watching, attended, no_show, waitlisted } counts. Every response returns a cursor (the database read time): pass it back as since= for a delta read that returns only attendees whose booking or stream session changed after since minus a 2-second overlap (merge by id; re-sends are idempotent), removed_ids for bookings that left the roster, delta: true and the whole-class summary. Poll the delta every few seconds while the screen is visible and do a full read on open, pull-to-refresh and periodically. Each row carries additive notification_availability.{booking_removal, waitlist_promote}.clients — per channel {available, reason_code, reason, recipient_id, reachable, total, blocked, reach_label}.

Parameters, scopes and examples

Required scopes

read:bookings

Path parameters

classInstanceIdstring · required
Class instance ID

Query parameters

include_historical_recordsstring
Include protected cancelled/late-cancelled roster rows; requires scheduling.manage_historyDefault: false
sincestring
The previous response cursor (ISO timestamp). Returns only changed attendees plus removed_ids; omit for the full roster.

Response example

{
  "data": {
    "attendees": [
      {
        "id": "uuid",
        "status": "checked_in",
        "attendance_type": "online",
        "online_attendance": {
          "state": "watching",
          "playback_state": "playing",
          "first_played_at": "2026-10-05T07:01:10Z",
          "last_heartbeat_at": "2026-10-05T07:20:40Z"
        }
      }
    ],
    "summary": {
      "total": 12,
      "checked_in": 7,
      "confirmed": 5,
      "waitlisted": 0,
      "no_show": 0,
      "in_studio": {
        "total": 9,
        "confirmed": 3,
        "checked_in": 6,
        "no_show": 0,
        "waitlisted": 0
      },
      "online": {
        "total": 3,
        "not_yet": 2,
        "watching": 1,
        "attended": 0,
        "no_show": 0,
        "waitlisted": 0
      }
    },
    "delta": true,
    "cursor": "2026-10-05T07:20:40.512Z",
    "removed_ids": []
  }
}
POST/api/v1/admin/checkin/{classInstanceId}/contactBearer token

Bulk email/SMS a class roster

Send a bulk email or SMS to a selected subset of one class instance. Submitted booking_ids are intersected server-side with the organization's active or whole-class-preserved roster; ordinary cancellations and stale/foreign ids are dropped and counted as skipped. Caller-supplied contact data is never accepted. Uses the canonical consent/suppression-aware bulk senders and requires the can_view_client_contact_info membership toggle. Idempotency-Key is honored.

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

classInstanceIdstring · required
Class instance UUID

Channel, roster booking ids, and message

Request body

{
  "channel": "email",
  "booking_ids": [
    "00000000-0000-4000-8000-0000000000b1"
  ],
  "subject": "Class update",
  "message": "Hi {{first_name}} — here is an update about your class."
}
POST/api/v1/admin/checkin/{classInstanceId}/{bookingId}Bearer or API key

Check in member

Check a member into class through the canonical attendance core. Validates current tenant/location scope, class timing and late-arrival authority; after-cutoff requires booking.checkin.after_cutoff and explicit confirmation. Idempotency-Key is required for the reviewed attempt, and the committed receipt reports operation identity/effects. Notification is silent unless an explicit supported channel selection is authorized and preflighted. A streaming (online) booking is attended automatically when its stream plays: a staff check-in of one is refused with 409 ONLINE_ATTENDANCE_AUTO unless the body sets mark_attended_override: true (the audited "Mark attended" override, not subject to the late-arrival cutoff). The receipt adds attendance_type and online_attendance_override.

Parameters, scopes and examples

Required scopes

write:checkin

Path parameters

classInstanceIdstring · required
Class instance ID
bookingIdstring · required
Booking ID

Optional: ended_class_confirmed, mark_attended_override (streaming bookers only), canonical notify selection (omit to stay silent)

Request body

{
  "ended_class_confirmed": false,
  "mark_attended_override": true
}
POST/api/v1/admin/checkin/{classInstanceId}/{bookingId}/noshowBearer or API key

Mark no-show

Mark member as no-show through the canonical attendance core. Validates current tenant/location scope and timing; no-show fee/clip effects are derived server-side and require the specific no-show grant. Idempotency-Key and the reviewed booking version protect retries. Notification is silent unless explicit channels pass notifications.send and recipient/event preflight.

Parameters, scopes and examples

Required scopes

write:checkin

Path parameters

classInstanceIdstring · required
Class instance ID
bookingIdstring · required
Booking ID
GET/api/v1/admin/membersBearer or API key

List members

Membership-driven client list with exact pre-pagination status and pass filtering. Native course grants count as covering entitlement: those clients are status=active (not no_pass) and each row may carry additive active_course_access { course_name } | null. Search by name, email, phone or venue client ID; optionally filter by tag, active pass type, or canonical pass family. pass_type_id and pass_family combine with AND semantics. Every successful response, including zero-match pages, includes meta.pass_type_options and meta.pass_family_options. Options expose distinct active-client counts across the full authenticated venue before pagination; pass types include current active templates plus archived templates still held by active clients, including types hidden from public pricing and course/workshop-managed types.

Parameters, scopes and examples

Required scopes

read:members

Query parameters

searchstring
Search by name, email, or phone
statusstring
Client status: active, inactive, new, or no_pass
tagstring
Filter by member tag
pass_type_idstring
Filter by active pass type
pass_familystring
Filter by pass family: recurring, class_pack, time_based, or intro_offer
pageinteger
Page numberDefault: 1
limitinteger
Items per pageDefault: 20

Response example

{
  "data": [],
  "error": null,
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 0,
    "has_more": false,
    "counts": {
      "all": 0,
      "active": 0,
      "inactive": 0,
      "new": 0,
      "no_pass": 0
    },
    "pass_type_options": [
      {
        "id": "00000000-0000-4000-8000-000000000101",
        "name": "Unlimited membership",
        "family": "recurring",
        "active_client_count": 24
      },
      {
        "id": "00000000-0000-4000-8000-000000000102",
        "name": "Course preparation pass",
        "family": "class_pack",
        "active_client_count": 3
      }
    ],
    "pass_family_options": [
      {
        "key": "recurring",
        "active_client_count": 24
      },
      {
        "key": "class_pack",
        "active_client_count": 3
      },
      {
        "key": "time_based",
        "active_client_count": 0
      },
      {
        "key": "intro_offer",
        "active_client_count": 0
      }
    ]
  }
}
POST/api/v1/admin/membersBearer token

Create or invite a client

Register an active client or send a pending client invitation. Enforces the venue plan limit, requires members.edit and an Idempotency-Key, and records a PII-safe audit event.

Parameters, scopes and examples

Required scopes

write:members

Discriminated client create request

Request body

{
  "mode": "register",
  "first_name": "Mia",
  "last_name": "Member",
  "email": "mia@example.com",
  "phone": "+4512345678"
}
GET/api/v1/admin/members/{id}Bearer or API key

Member detail

Full member profile: passes with course-fulfillment provenance and an additive, optional `provenance` field per pass (who sold it, how, when, plus the sale/activation/money facts of the one sale-provenance rule — gated on the caller's own payments.view, null otherwise, same rule as stripe_subscription_id/recent_payments), canonical native course_access, recent bookings/payments, tags, scores, credits, referrals, client_display_id, plus server-authoritative total_bookings and last_visit_at. The additive root-level date_format is the venue's display preference (DD/MM/YYYY, YYYY-MM-DD, DD.MM.YYYY or DD-MM-YYYY); profile.date_of_birth remains a canonical YYYY-MM-DD civil date for writes. Also returns per-channel notification_availability (email/SMS/push with exact unavailable reasons), contact_details_visibility {email,phone} (independent member.view_email / member.view_phone AND the membership contact toggle; contact_details_visible remains email AND phone for older app builds), and payments_visible. A 409 PROFILE_MERGED is returned when this profile was merged away in this venue, with primary_user_id of the survivor. Contact disclosure is fail-closed on PII_AUDIT_FAILED.

Parameters, scopes and examples

Required scopes

read:members

Path parameters

idstring · required
Member user ID
POST/api/v1/admin/members/{id}/deactivateBearer token

Deactivate a client in this venue

Soft-deactivates only the active membership at the selected venue; it never deletes the shared profile or changes memberships at other venues. Delivery is silent by default. An explicit notify object may select Email, SMS, and/or Push, which requires notifications.send and an availability preflight before the deactivation commits. A post-commit delivery failure is returned separately as notification_failure and never restores access. Protected Admin/Finance memberships retain their shared lifecycle authorization checks. Idempotency-Key required. Permission: members.delete.

Parameters, scopes and examples

Required scopes

members.delete

Path parameters

idstring · required
Member user ID

Deactivation reason and optional explicit client delivery channels

Request body

{
  "reason": "Client requested account closure",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "push": false
    }
  }
}
GET/api/v1/admin/members/{id}/passesBearer token

Paginated member pass history

Tenant-scoped pass history with an exact total and opaque keyset cursor. Returns up to 100 records per page and never exposes processor subscription identifiers.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID

Query parameters

limitinteger
Items per page (1–100)Default: 25
afterstring
Opaque next_cursor from the previous page
GET/api/v1/admin/members/{id}/bookingsBearer token

Paginated member booking and visit history

Deterministically merges live bookings and imported historical visits. Historical rows carry record_source=migration_history and read_only=true. The total is exact across both stores. Live rows with a recorded course consumption carry payment_status=course and course_coverage { course_name, state }; released course usage retains its returned state. Course coverage describes the recorded booking entitlement, not tuition payment. Every row also carries source, payment_status, class_instance.local_date and a permission-agnostic actions block (cancel, remove_waitlist, change_pass, correct_attendance, check_in, mark_no_show — all false on an imported row). meta.venue_today is the venue-local date the today gates were measured against; meta.stats (first page only, absent when after is sent) holds exact counts over the entire filtered set. Live rows carry additive notification_availability.{booking_removal, waitlist_promote}.clients in the staff availability contract.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID

Query parameters

limitinteger
Items per page (1–100)Default: 25
afterstring
Opaque next_cursor from the previous page
pass_idstring
Only bookings funded by this pass
cycle_startstring
Venue-local YYYY-MM-DD start of a pass usage cycle (inclusive)
cycle_endstring
Venue-local YYYY-MM-DD end of a pass usage cycle (exclusive)
scopestring
upcoming = class start after now and the seat still held, ordered soonest first, no imported history; past = the exact complement, newest first. Omitted returns the merged view.
fromstring
Venue-local YYYY-MM-DD, inclusive, on the class start (imported rows compare on visit_date)
tostring
Venue-local YYYY-MM-DD, inclusive; converted to the next local midnight so evening classes stay in range
statusstring
Exact match on live rows; imported rows match on their normalised status, so confirmed, waitlisted and pending_payment never match one
class_type_idstring
Only classes of this class type (excludes imported history)
instructor_idstring
Only classes whose PRIMARY instructor is this person — a substitute does not match (excludes imported history)
location_idstring
Only classes at this location (excludes imported history)
brand_idstring
Only classes whose class type belongs to this brand (excludes imported history)
GET/api/v1/admin/members/{id}/paymentsBearer token

Paginated member payment history

Tenant-scoped payments with exact total, refunds, safe card display, invoice linkage, and explicit receipt capabilities. Each refund has additive receipt_available=true only for a succeeded operation claim on a venue payment; pending, legacy non-claim and Apple merchant-of-record rows cannot offer the venue refund PDF. Additive receipt.bulk_email {available,reason_code,reason} describes eligibility for the existing bulk email route, including non-POS payments, independently of POS-only single-send capabilities. Bulk execution still requires members.contact and notifications.send and rechecks delivery policy. Older clients may ignore this field; newer clients fail closed on a malformed present field and retain the conservative single-email fallback when it is absent. Processor IDs, client secrets, and raw receipt URLs are never returned.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID

Query parameters

limitinteger
Items per page (1–100)Default: 25
afterstring
Opaque next_cursor from the previous page
statusstring
Only payments with this status. Anything outside the list is a 400. Omitted = every status.
fromstring
Venue-local YYYY-MM-DD, inclusive. Applied to the page AND the total, so meta.total is the filtered total.
tostring
Venue-local YYYY-MM-DD, inclusive. from after to, or a day no calendar has, is a 400.
GET/api/v1/admin/members/{id}/payments/{paymentId}/receiptBearer token

Download a member payment receipt

Streams the canonical venue-branded PDF for a receipt-bearing payment after proving active membership and same-venue payment ownership.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID
paymentIdstring · required
Same-venue payment ID
POST/api/v1/admin/members/{id}/payments/{paymentId}/receipt/sendBearer token

Re-send a POS payment receipt

Re-sends through the canonical POS sender to the member contact stored on the server. Only same-venue POS-backed receipt payments are eligible; arbitrary recipients and payment retry are not supported. Idempotency-Key is required and atomically bound to the actor, venue, member, payment and selected channel. Retry the same request after response loss. A concurrent request returns IDEMPOTENCY_IN_PROGRESS; changed intent returns IDEMPOTENCY_KEY_REUSE_MISMATCH. RECEIPT_DELIVERY_UNCONFIRMED retains the uncertain outcome without another provider send. sent:true confirms provider acceptance, not delivery to the client.

Parameters, scopes and examples

Required scopes

notifications.send

Path parameters

idstring · required
Active member ID
paymentIdstring · required
Same-venue payment ID

Receipt delivery channel advertised by the payment receipt capability

Request body

{
  "method": "email"
}
GET/api/v1/admin/paymentsBearer token

Org-wide recent sales

Business-app contract C6: the venue's recent payments (all statuses), newest first, mirroring the web sales drawer rows — plain-language method label, card label, money bucket (captured | recorded | internal), and the drawer's refund-offer rule (`refundable` = settled non-guest rows with money remaining). Amounts are integer minor units (øre). Each item also carries `sale_provenance` {sale, activation, money}: who made the sale and when (staff at the desk, the client online, …), when its pass starts, and how THIS payment was collected (at the desk, online, charged automatically when a scheduled pass started, a renewal, …) — `sold_by`/`channel` read the sale, never the payment. Cursor-paginated (opaque keyset cursor), limit ≤ 50. Range resolves in the venue's timezone.

Parameters, scopes and examples

Required scopes

reports.view

Query parameters

rangestring
Venue-local window: 'today' | '7d' | '30d'Default: today
cursorstring
Opaque cursor from a previous page (next_cursor)
limitinteger
Page size (1–50)Default: 25
POST/api/v1/admin/payments/{id}/refundBearer token

Refund a payment

Business-app contract C7: executes a claimed review through canonical processRefund. Body { review_intent_id, confirmation?, amount?, reason?, client_receipt_comment?, destination_id?, method_reference?, notify_client?, receipt_channels?: ('email'|'sms'|'push')[] }. Destination and notes must match the immutable review. Omitted or empty channels and a legacy boolean alone remain silent. Explicit channels require notifications.send before new refund admission; an unavailable channel returns 422 REFUND_CHANNEL_UNAVAILABLE with channel and reason_code. Recover an admitted operation with its original review and Idempotency-Key. A new operation is refused with 409 REFUND_RECOVERY_CASE_OPEN while the payment has an open critical refund recovery case (the same rule as the review). Success includes refund_id, refund_status, a printable bearer-authenticated PDF URL, and per-channel outcomes; pending approval or provider processing is not a settled refund or proof of notification delivery.

Parameters, scopes and examples

Required scopes

billing.refunds.same_daybilling.refunds.full

Path parameters

idstring · required
Payment UUID

Refund details (amount in MAJOR units)

Request body

{
  "review_intent_id": "00000000-0000-4000-8000-000000000000",
  "amount": 199,
  "reason": "Client requested the refund",
  "client_receipt_comment": "We hope to see you again soon.",
  "destination_id": "original",
  "notify_client": true,
  "receipt_channels": [
    "email",
    "push"
  ]
}
GET/api/v1/admin/refunds/{id}/receiptBearer token

Download canonical refund receipt PDF

Bearer-authenticated, tenant-scoped, no-store PDF used by native print/share. Contains only the optional client receipt comment; the staff-only internal reason is never rendered.

Parameters, scopes and examples

Required scopes

billing.refunds.same_daybilling.refunds.full

Path parameters

idstring · required
Refund UUID
POST/api/v1/admin/checkin/{classInstanceId}/contactBearer token

Bulk email/SMS a class roster

Business-app contract C8: send a bulk email or SMS to participants of one class instance. Recipients are resolved server-side — the submitted booking_ids are intersected with the class's ACTIVE roster (confirmed/waitlisted/checked_in); stale ids are dropped and counted as skipped, and caller-supplied contact info is never accepted. Delegates to the same senders/consent semantics as the web check-in bulk bar (templates admin_bulk_email / admin_bulk_sms). Requires the TV-D can_view_client_contact_info membership toggle. Returns { sent, skipped }.

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

classInstanceIdstring · required
Class instance UUID

Channel, roster booking ids, and the message

Request body

{
  "channel": "email",
  "booking_ids": [
    "00000000-0000-4000-8000-0000000000b1"
  ],
  "subject": "Tonight’s class moves to Room 2",
  "message": "Hi {{first_name}} — we moved tonight’s class to Room 2. See you there!"
}
GET/api/v1/admin/checkin/{classInstanceId}/guest-optionsBearer token

Guest payment options

Business-app parity: the eligible one-class products for paid guest spots. Uses the same guest visitor permission and catalog core as the web check-in screen.

Parameters, scopes and examples

Required scopes

booking.checkin

Path parameters

classInstanceIdstring · required
Class instance UUID
POST/api/v1/admin/checkin/{classInstanceId}/walk-inBearer token

Add a guest walk-in to a class

Requires booking.checkin, current can_add_client_to_class and assigned-location access. Idempotency-Key (maximum 200 characters) and immutable full guest intent are required. Every retry reads the canonical receipt after current authorization, including after midnight; cached HTTP success never overrides current booking status. Silent: no payment or client notification.

Parameters, scopes and examples

Required scopes

booking.checkin

Path parameters

classInstanceIdstring · required
Class instance UUID

Freeze these guest fields together with Idempotency-Key until the operation is completed or definitely refused.

Request body

{
  "first_name": "Nora",
  "last_name": "Guest",
  "email": "nora@example.com",
  "phone": "+4512345678"
}

Response example

{
  "data": {
    "success": true,
    "outcome": "completed",
    "bookingId": "uuid",
    "bookingStatus": "checked_in",
    "replayed": false
  },
  "error": null
}
POST/api/v1/admin/checkin/{classInstanceId}/guestsBearer token

Add guest spots

Business-app parity: add 1–20 guest spots as payment-link, paid-at-desk, or comp/free bookings. Payment-link delivery supports email, SMS, or both. Uses the same context-free core as web; Idempotency-Key required.

Parameters, scopes and examples

Required scopes

booking.checkin

Path parameters

classInstanceIdstring · required
Class instance UUID

Guest contact, count, payment mode, product, and delivery channels

Request body

{
  "first_name": "Nora",
  "last_name": "Guest",
  "email": "nora@example.com",
  "number_of_guests": 2,
  "payment_mode": "send_payment_link",
  "payment_option_id": "00000000-0000-4000-8000-000000000001",
  "delivery_channels": [
    "email"
  ]
}
GET/api/v1/admin/billing/failed-paymentsBearer token

Org-wide failed payments

Business-app contract C1: the venue's failed payments (payments.status='failed') in the trailing window (default 30 days), newest first, cap 100. Each row carries client + linked pass context, a plain-language method label, and `retryable` per the same pure decider the web Retry button uses. Amounts are integer minor units (øre).

Parameters, scopes and examples

Required scopes

members.view_insights

Query parameters

daysinteger
Trailing window in days (1–365)Default: 30
member_idstring
Client-account parity P3 — only this client’s failed payments (public display id or UUID). `count` is then that client’s count. An id that is not an active client of this venue answers 404.
POST/api/v1/admin/billing/failed-payments/{paymentId}/retryBearer token

Retry a failed payment

Business-app contract C2: re-collect a failed payment through the canonical retry core (PaymentIntent confirm or off-session invoice pay on the SC2-resolved Connect account, driving handleInvoicePaid). The body may be empty for the provider default, or contain payment_method_id selected from the exact failed invoice/PaymentIntent customer wallet. A provider-proven legacy card_ default is accepted and retried as the customer default without an unsupported override. Idempotency-Key is required, atomically claimed before the provider charge, and bound to the venue, payment, and selected payment_method_id; simultaneous reuse cannot double-charge and reuse with another card returns 409. Returns status succeeded | requires_action | failed with a plain-language message.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

paymentIdstring · required
Failed payment UUID

Optional exact saved card override; omit the body to use the provider default.

Request body

{
  "payment_method_id": "pm_…"
}
POST/api/v1/admin/billing/failed-payments/{paymentId}/settle-externalBearer token

Record an external settlement for a failed renewal

Business-app contract C3: the failed recurring-renewal invoice was paid through another channel (cash, bank transfer, MobilePay, external card terminal, other). Settles the Stripe invoice out-of-band so the canonical recovery reactivates the pass, attributing the recovered payments row to the real method. amount is integer minor units (øre). Idempotency-Key required.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

paymentIdstring · required
Failed payment UUID

Settlement details

Request body

{
  "method": "bank_transfer",
  "amount": 79900,
  "paid_at": "2026-07-31",
  "note": "Paid via bank transfer, ref 1234",
  "notify_client": true
}
POST/api/v1/admin/billing/failed-payments/{paymentId}/waiveBearer token

Waive a failed renewal cycle

Business-app contract C4: comp the failed recurring-renewal cycle — the client keeps the period, 0 revenue is recorded (the recovered payments row is forced to 'comped' amount 0). Reason required. Idempotency-Key required.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

paymentIdstring · required
Failed payment UUID

Waive details

Request body

{
  "reason": "Goodwill — studio closure week",
  "notify_client": true
}
POST/api/v1/admin/passes/{passId}/suspendBearer token

Suspend (deactivate) a pass

Business-app contract C5: venue-imposed suspension (distinct from the member freeze) — blocks bookings until unsuspended. Client delivery is silent by default and accepts only notify.audience.clients=true plus an explicit Email/SMS/Push selection; legacy notify_client/notify_channels inputs remain silent. A non-empty selection requires notifications.send, and an unavailable selected channel returns PASS_NOTIFICATION_CHANNEL_UNAVAILABLE before the pass changes. Idempotency-Key (UUID) required; replay returns the stored response and the stable delivery reference prevents re-sending. Delivery failure after the pass write is reported as notification.sent=false and never rolls the suspension back.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Optional reason and explicit, default-silent client notification choice

Request body

{
  "reason": "Unpaid balance",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/unsuspendBearer token

Unsuspend a pass

Business-app contract C5: lift a venue-imposed suspension. Body is optional and silent by default. Client delivery requires notify.audience.clients=true, an explicit Email/SMS/Push selection, and notifications.send; legacy booleans/arrays remain silent. An unavailable selected channel returns PASS_NOTIFICATION_CHANNEL_UNAVAILABLE before the pass changes. Idempotency-Key (UUID) required; replay never re-sends. A post-write delivery failure returns notification.sent=false without rolling the pass change back. 422 NOT_SUSPENDED for a pass that is not suspended.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Optional explicit client notification choice

Request body

{
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}
POST/api/v1/admin/passes/{passId}/reactivateBearer token

Reactivate a terminal pass

Business-app contract C5 (PASS-REACTIVATE-01): flip an expired/cancelled NON-recurring pass back to active; a run-out window requires new_end_date ≥ venue-local today. Client delivery is silent by default and requires notify.audience.clients=true, explicit Email/SMS/Push channels, and notifications.send; legacy notification inputs remain silent. Unavailable channels reject before mutation. Idempotency-Key (UUID) required and replay never re-sends; delivery failure after the write returns notification.sent=false without rollback. Recurring memberships are refused (422 RECURRING_UNSUPPORTED) — restart via a real re-mint. Audit pass_reactivated + reverse_payload.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Optional new end date for a run-out window

Request body

{
  "new_end_date": "2026-09-30",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": true,
      "push": false
    }
  }
}
POST/api/v1/admin/bookings/{bookingId}/reassign-pass/conflict-planBearer token

Review or recover the original later-booking cancellation plan

Requires bookings.manage and booking.cancel_member. Idempotency-Key is the original pass-change key; request is its exact version:1 body including reviewed expected_old_pass_id and expected_updated_at. mode:read returns null only if no plan or parent receipt exists. mode:prepare freezes the server-selected children without cancelling anything. The strict plan retains each original child key, reviewed class time/location, quiet cancellation terms and confirmed partial results in server order. A terminal original parent is returned before new planning. 503 BOOKING_PASS_CONFLICT_UNRESOLVED retains the original; it never proves absence.

Parameters, scopes and examples

Required scopes

bookings.managebooking.cancel_member

Path parameters

bookingIdstring · required
Original reassigned booking

Read or prepare with the unchanged parent request and original Idempotency-Key.

Request body

{
  "mode": "read",
  "request": {
    "version": 1,
    "new_pass_id": "00000000-0000-4000-8000-0000000000c1",
    "expected_old_pass_id": null,
    "expected_updated_at": "2026-09-26T10:00:00.000Z"
  }
}
POST/api/v1/admin/booking-pass-conflicts/{planId}/children/{childId}Bearer token

Recover, apply or close one reviewed later-booking cancellation

Requires bookings.manage, booking.cancel_member and canonical location/attendance authority. Send mode:read|apply|close and the exact server-frozen child request, with its original Idempotency-Key. Apply follows the original server order and returns only a correlated cancelled receipt. Close returns that committed receipt or closed_without_cancel; it never reverses a cancellation. Null is permitted only on read and is not closure. Changed facts, ALREADY_CANCELLED and 503 BOOKING_PASS_CONFLICT_UNRESOLVED retain the original. Notifications stay empty; shared usage remains retained. Retry the original parent only after every child has its own cancelled receipt.

Parameters, scopes and examples

Required scopes

bookings.managebooking.cancel_member

Path parameters

planIdstring · required
Frozen plan ID
childIdstring · required
Frozen child ID

mode and the exact request returned for this child; preserve all timestamps, status, class location and quiet terms.

Request body

{
  "mode": "read",
  "request": {
    "version": 1,
    "booking_id": "00000000-0000-4000-8000-000000000008",
    "class_instance_id": "00000000-0000-4000-8000-000000000009",
    "pass_id": "00000000-0000-4000-8000-000000000004",
    "user_id": "00000000-0000-4000-8000-000000000010",
    "expected_updated_at": "2026-09-27T00:00:00.123456Z",
    "expected_class_start_time": "2026-10-30T10:00:00Z",
    "expected_class_end_time": "2026-10-30T11:00:00Z",
    "expected_location_id": null,
    "expected_status": "confirmed",
    "target": "removed",
    "restore_clip": true,
    "charge_no_show_fee": false,
    "no_show_forfeit_clip": false,
    "no_show_fee_amount": 0,
    "notify_channels": [],
    "allow_after_cutoff": false,
    "ended_class_confirmed": false
  }
}
POST/api/v1/admin/bookings/{bookingId}/shared-usage-reviewBearer token

Find the shared usage attached to a reviewed booking

Read-only. Requires passes.manage, bookings.manage and booking location access. The exact original expected_pass_id and expected_updated_at must still match. Returns null for funding with no share, or booking_id, pass_id, share_id and booking_updated_at. This only opens the explicit usage review; it never allocates usage, cancels a booking or releases an unresolved pass-change request.

Parameters, scopes and examples

Required scopes

passes.managebookings.manage

Path parameters

bookingIdstring · required
Reviewed booking ID

The original reviewed funding and booking version.

Request body

{
  "expected_pass_id": "00000000-0000-4000-8000-0000000000c1",
  "expected_updated_at": "2026-09-26T10:00:00.000Z"
}
GET/api/v1/admin/checkin/{classInstanceId}/{bookingId}/attendanceBearer token

Attendance correction options

Current attendance state for one booking plus what a correction would do: current_status, any attendance fee and its refund payment, the venue no-show fee, whether a clip was consumed, and per-channel notification_availability (email/SMS/push with the exact unavailable reason) so the client-notify picker can disable what cannot be delivered. The read also returns booking_updated_at, can_check_in_after_cutoff, ended_class_confirmation_required, credit_applicable and no_show_forfeits_clip. can_refund_fee is always false here — refunds live in Billing.

Parameters, scopes and examples

Required scopes

booking.checkin

Path parameters

classInstanceIdstring · required
Class instance ID
bookingIdstring · required
Booking ID
POST/api/v1/admin/checkin/{classInstanceId}/{bookingId}/attendanceBearer token

Correct attendance

Flip a booking between checked in, no-show, booked and removed for today or a past day (a future class returns 422 FUTURE_ATTENDANCE, a cancelled class 422 CLASS_CANCELLED). Marking a no-show additionally requires bookings.mark_no_show and removing a visit requires booking.cancel_member. Client delivery is silent by default: only an explicit notify object with a non-empty channel set sends, which requires notifications.send and a per-channel availability preflight before the correction (422 ATTENDANCE_NOTIFICATION_CHANNEL_UNAVAILABLE, 502 ATTENDANCE_NOTIFICATION_PREFLIGHT_FAILED). The legacy notify_client boolean is accepted but never delivers. refund_fee returns 409 REFUND_REVIEW_REQUIRED — refund the charged fee from its protected payment detail first. Idempotency-Key required and is bound to the actor, venue and parsed body; expected_updated_at and ended_class_confirmed are forwarded to the canonical compare-and-swap core. A changed retry returns 409 IDEMPOTENCY_KEY_REUSE_MISMATCH. Success is committed when returned with operationId, bookingUpdatedAt, creditDelta, seatDelta and effectsStatus; pending/held delivery remains a committed correction.

Parameters, scopes and examples

Required scopes

booking.checkin

Path parameters

classInstanceIdstring · required
Class instance ID
bookingIdstring · required
Booking ID

Target attendance state, fee/clip choices, expected_updated_at, ended_class_confirmed when required, and explicit client delivery channels

Request body

{
  "to": "no_show",
  "charge_no_show_fee": true,
  "expected_updated_at": "2026-09-22T09:00:00.000Z",
  "ended_class_confirmed": false,
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}
GET/api/v1/admin/bookings/{bookingId}/pass-optionsBearer token

Passes a booking can move to

The client's passes that could fund this booking, each with why it is or is not eligible (clips remaining, end date, who shared it, whether it is the current pass, and whether using it would shift the pass start date). Includes booking:{booking_id,pass_id,updated_at}; retain that reviewed snapshot with the chosen pass and original request key. Eligibility comes from the booking engine itself.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

bookingIdstring · required
Booking ID
POST/api/v1/admin/bookings/{bookingId}/reassign-passBearer token

Change the pass funding a booking

Atomically change the pass funding a booking. Idempotency-Key (1–200 characters, no control characters) and the exact original body are retained for every retry. Reviewed expected_old_pass_id (including null) and expected_updated_at are required for protected POS passes; both reviewed fields must be supplied together or both omitted; omission remains distinct from null. Success includes success:true, operation_id, event_id, booking_id, previous_pass_id, new_pass_id, pass_id (legacy alias), booking_updated_at, old_credit_returned, new_credit_consumed, replayed, idempotency_key and the original version:1 request. Internal activation proof is not exposed. Shift conflicts require separate explicit resolution. Busy or malformed/unknown outcomes return 503 booking_reassign_unresolved; no error or lookup miss proves the original did not commit. Use the close endpoint before replacing a retained request.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

bookingIdstring · required
Booking ID

The chosen pass and exact reviewed booking snapshot; retain omitted fields as omitted on legacy retries.

Request body

{
  "new_pass_id": "00000000-0000-4000-8000-0000000000c1",
  "expected_old_pass_id": null,
  "expected_updated_at": "2026-09-26T10:00:00.000Z"
}
POST/api/v1/admin/bookings/{bookingId}/reassign-pass/closeBearer token

Resolve an original pass-change attempt before replacing it

Requires bookings.manage and the same original Idempotency-Key and body as reassignment. Returns the original committed success receipt, or a strictly correlated success:false,error:booking_reassign_closed receipt with operation_id,booking_id,new_pass_id,request,replayed,idempotency_key. Only this closed receipt allows replacement; 409 conflicts, 422 refusals, missing results and 503 uncertainty retain the original. Closing never cancels or reverses a committed change and sends no notification.

Parameters, scopes and examples

Required scopes

bookings.manage

Path parameters

bookingIdstring · required
Original booking ID

Exact original reassignment body, with no refreshed expectations.

Request body

{
  "new_pass_id": "00000000-0000-4000-8000-0000000000c1",
  "expected_old_pass_id": null,
  "expected_updated_at": "2026-09-26T10:00:00.000Z"
}
GET/api/v1/admin/members/{id}/bookings/filtersBearer token

Filter choices for a member booking list

The venue catalogue the Business app builds the Visits filter sheet from: active class types (with their brand), this venue’s instructors, active locations and active brands. Served under the same grant as the list these choices filter, so a staff member who can see every booking can always load the sheet. Every list is org-scoped.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID
GET/api/v1/admin/schedule/{id}Bearer or API key

Admin schedule item

One class instance in the GET /api/v1/admin/schedule item shape (per-status counts, concurrency token, instructor/substitute, historical_capabilities) plus notification_availability.{cancel,edit,substitute,restore}.{clients,instructor}.{email,sms,push}. Use this for a class detail instead of reading the whole schedule. Each channel entry is {available, reason_code, gate_code, reason, recipient_id, reachable, total, blocked, reach_label}: reason_code is the app vocabulary (venue_capability, unsupported, no_contact, invalid_contact, user_opted_out, no_registered_device, no_recipients, availability_error) and gate_code the precise gate. A channel is available when the venue gate passes and at least one recipient is reachable. Callers without class.cancel or notifications.send get every channel unavailable (availability_error). A protected historical class is 404 without historical read access. Requires schedule.view_all and current location access.

Parameters, scopes and examples

Required scopes

read:schedule

Path parameters

idstring · required
Class instance ID
POST/api/v1/admin/schedule/notification-availabilityBearer or API key

Notification channels for a class selection

Read-only. Returns {action, can_notify, clients, instructor} for 1–200 tenant-owned classes and one action (cancel, edit, substitute, restore). A channel is available when the venue gate passes and at least one recipient across the selection is reachable; counts and blocked reasons add up across classes. Clients start silent; the instructor picker may pre-select Email and Push when available. Restore reports unsupported until restore notifications ship. Requires schedule.view_all; can_notify reflects notifications.send, and callers with neither class.cancel nor notifications.send get every channel unavailable (availability_error). Foreign classes return 422 CLASS_SCOPE_MISMATCH.

Parameters, scopes and examples

Required scopes

read:schedule

Selected classes and the action to preview

Request body

{
  "instance_ids": [
    "uuid"
  ],
  "action": "cancel"
}

Response example

{
  "action": "cancel",
  "can_notify": true,
  "clients": {
    "email": {
      "available": true,
      "reason_code": null,
      "gate_code": null,
      "reason": null,
      "recipient_id": null,
      "reachable": 9,
      "total": 12,
      "blocked": {
        "no_contact": 2,
        "recipient_opted_out": 1
      },
      "reach_label": "Email reaches 11 of 12 (1 no email)"
    },
    "sms": {
      "available": true,
      "reason_code": null,
      "gate_code": null,
      "reason": null,
      "recipient_id": null,
      "reachable": 9,
      "total": 12,
      "blocked": {
        "no_contact": 2,
        "recipient_opted_out": 1
      },
      "reach_label": "SMS reaches 9 of 12 (2 no phone, 1 turned SMS off)"
    },
    "push": {
      "available": false,
      "reason_code": "no_registered_device",
      "gate_code": "no_registered_device",
      "reason": "None of the 12 clients can receive Push (12 not on the app).",
      "recipient_id": null,
      "reachable": 0,
      "total": 12,
      "blocked": {
        "no_registered_device": 12
      },
      "reach_label": null
    }
  },
  "instructor": {
    "email": {
      "available": true,
      "reason_code": null,
      "gate_code": null,
      "reason": null,
      "recipient_id": null,
      "reachable": 1,
      "total": 1,
      "blocked": {},
      "reach_label": "Email reaches the instructor"
    },
    "sms": {
      "available": true,
      "reason_code": null,
      "gate_code": null,
      "reason": null,
      "recipient_id": null,
      "reachable": 1,
      "total": 1,
      "blocked": {},
      "reach_label": "SMS reaches the instructor"
    },
    "push": {
      "available": false,
      "reason_code": "no_registered_device",
      "gate_code": "no_registered_device",
      "reason": "The instructor cannot receive Push (not on the app).",
      "recipient_id": null,
      "reachable": 0,
      "total": 1,
      "blocked": {
        "no_registered_device": 1
      },
      "reach_label": null
    }
  }
}
POST/api/v1/admin/members/{id}/profile-requestBearer token

Ask a client to add or update a profile detail

Requires members.contact. Sends one templated request (profile_update_request) on the explicitly chosen channel asking the client to add a missing detail or confirm/update an existing one. The message is written as the client's acquisition brand of this venue (else the venue): branded email layout with a button, brand-named SMS and push. It links to the member profile opened at that detail (`/profile?field=<field>` on the brand/venue host; push data carries `type`, `field`, `url` path and `organization_id`). An address request to a member the age-based VAT exemption applies to also asks them to confirm it as their home address there. Venue-scoped, audited (member_profile_update_requested), one request per client and field per 24 hours. 404 CLIENT_NOT_FOUND outside the venue; 422 PROFILE_REQUEST_FAILED with a staff-readable reason (no email/phone on file, email blocked or not sent, no app device, already asked).

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

idstring · required
Client profile id

The detail to ask about and the one channel to send on.

Request body

{
  "field": "phone",
  "channel": "email"
}

Response example

{
  "data": {
    "sent": true,
    "field": "phone",
    "channel": "email"
  }
}
GET/api/v1/admin/members/{id}/payments/historyBearer token

Read a client’s archived payment history

Read-only payment snapshots retained during a brand split or transfer. These records are separate from the live payment ledger and do not support refunds, receipt resends, invoice actions or revenue totals. Each row retains its original amount in major currency units, ISO currency, source status, occurrence time and snapshot time. Archive IDs are not payment IDs. Pagination sorts by original occurred_at and archive ID; reuse the exact opaque cursor with the same venue and member. Invalid pagination returns 400; unavailable or unverified history returns 503 rather than an empty history. Requires members.view_insights and active membership in the authenticated venue.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active client ID in the current venue.

Query parameters

limitnumber
Page size: 1–100, default 25.
afterstring
Opaque next_cursor from the same archive collection and scope.
DELETE/api/v1/admin/members/{id}/payment-methods/{pmId}Bearer token

Remove a client’s saved card

Removes one saved card from an active client of this venue. Requires members.contact and an Idempotency-Key (≤128 chars, bound to the venue, the operator, this client and this card; reuse for another card returns 409). The card must be on the client’s provider-proven wallet — an unknown id and another client’s id answer the same 404 PAYMENT_METHOD_NOT_FOUND, never an existence oracle. A shared platform-wallet card owned by another home venue answers 403 WALLET_FORBIDDEN. If the card was the default, the profile column and the exact Stripe customer default are cleared first (rolled back if the processor refuses) and only then is the card detached; a processor refusal answers 502 STRIPE_DETACH_FAILED. Audited; the client is never contacted.

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

idstring · required
Active member ID
pmIdstring · required
Saved payment-method ID

Response example

{
  "removed": true,
  "payment_method_id": "pm_example"
}
POST/api/v1/admin/members/{id}/payment-methods/{pmId}/defaultBearer token

Set a client’s default card

Makes the named saved card the client’s default for future off-session charges. Requires members.contact and an Idempotency-Key. Only a card can be a default — a non-card method answers 422 NOT_A_CARD. 404 PAYMENT_METHOD_NOT_FOUND for an id not on the proven wallet, 403 WALLET_FORBIDDEN for another home venue’s shared wallet, 502 STRIPE_UPDATE_FAILED when the processor refuses (the local column write is rolled back). Setting the card that is already the default is a 200 no-op with no audit row. The client is never contacted.

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

idstring · required
Active member ID
pmIdstring · required
Saved payment-method ID

No fields; send an empty object

Request body

{}

Response example

{
  "default_payment_method_id": "pm_example"
}
DELETE/api/v1/admin/members/{id}/payment-methods/{pmId}/defaultBearer token

Clear a client’s default card

Clears the client’s default card so nothing is charged off-session without a fresh choice. Requires members.contact and an Idempotency-Key. The path names the card the operator believes is current: if it is NOT the client’s default any more the request is refused with 409 NOT_DEFAULT (carrying the real default) rather than clearing a different card. 403 WALLET_FORBIDDEN and 502 STRIPE_UPDATE_FAILED as above. The client is never contacted.

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

idstring · required
Active member ID
pmIdstring · required
The card the operator believes is the current default

Response example

{
  "default_payment_method_id": null
}
POST/api/v1/admin/members/{id}/receipts/bulk-emailBearer token

Email receipts for selected client payments

Emails a payment receipt for each selected payment of this venue’s active client, including non-POS rows the single-payment receipt/send route skips. Requires members.contact plus notifications.send on this request, and an Idempotency-Key. Body is { payment_ids: uuid[] } (1…100). Each item reports sent, deduped, ineligible, or failed. Preserve the original member, payment set and key when recovering an interrupted request; do not start another batch merely because its response was lost. The client is contacted by email only; omission of the key is 400, a foreign client is 404.

Parameters, scopes and examples

Required scopes

members.contactnotifications.send

Path parameters

idstring · required
Active member ID

Selected payment ids

Request body

{
  "payment_ids": [
    "00000000-0000-4000-8000-0000000000e1"
  ]
}

Response example

{
  "results": [
    {
      "paymentId": "00000000-0000-4000-8000-0000000000e1",
      "outcome": "sent",
      "message": "Accepted by the email provider."
    }
  ],
  "summary": {
    "sent": 1,
    "failed": 0,
    "ineligible": 0,
    "deduped": 0
  }
}
GET/api/v1/admin/members/{id}/payment-methods/chargeBearer token

Context for charging a saved card

What the desk charge form needs before it can be shown: the venue currency, the display VAT rate (decimal) and its label, and the venue’s accounting categories. Requires pos.sell. An empty categories list means the venue has not set any up — the app disables the form and points at Settings rather than charging into a required column.

Parameters, scopes and examples

Required scopes

pos.sell

Path parameters

idstring · required
Active member ID

Response example

{
  "currency": "EUR",
  "vat_rate": 0.25,
  "vat_label": "VAT",
  "categories": [
    {
      "id": "00000000-0000-4000-8000-0000000000c1",
      "name": "Retail"
    }
  ]
}
POST/api/v1/admin/members/{id}/payment-methods/chargeBearer token

Charge a client’s saved card

Charges an ad-hoc amount to the client’s saved card off-session (a phone payment, a fee) and records the sale. Requires pos.sell and an Idempotency-Key. amount_minor is an integer in minor units and is VAT-inclusive — exactly what the card is charged. The card’s proven Stripe customer/account is what the PaymentIntent is created on and confirmed on. 201 on success with the new payment id, the receipt reference and the card label. 422 CHARGE_NOT_CHARGEABLE with reason payments_disabled | not_on_wallet | below_minimum | connect_not_active means nothing was attempted and the same key may be reused. 402 CHARGE_REQUIRES_ACTION means the card needs the client’s own authentication — the app then offers the secure card link. 402 CHARGE_DECLINED and 500 CHARGE_FAILED keep the key. set_as_default applies the card as the default afterwards, best effort; default_updated is null when it was not requested. No receipt is sent — the app offers the receipt route afterwards with the returned payment_id.

Parameters, scopes and examples

Required scopes

pos.sell

Path parameters

idstring · required
Active member ID

The card, the VAT-inclusive amount in minor units, and the bookkeeping fields

Request body

{
  "payment_method_id": "pm_example",
  "amount_minor": 25000,
  "description": "Workshop materials",
  "revenue_category_id": "00000000-0000-4000-8000-0000000000c1",
  "set_as_default": false
}

Response example

{
  "payment_id": "00000000-0000-4000-8000-0000000000p1",
  "payment_intent_id": "pi_example",
  "invoice_number": "2026-0042",
  "amount_minor": 25000,
  "currency": "EUR",
  "card": {
    "brand": "visa",
    "last4": "4242"
  },
  "default_updated": null
}
GET/api/v1/admin/members/{id}/feesBearer token

A client’s unpaid fees

The client’s open no-show and late-cancel debt, newest first. A declined charge stays on the list — pending and failed both mean money is still owed. Requires passes.manage, the same grant the web read uses; collecting or waiving needs billing.refunds.full. Amounts are integer minor units beside their currency, and each row carries the class name and start time when the linked booking still has them.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

idstring · required
Active member ID

Response example

{
  "fees": [
    {
      "id": "00000000-0000-4000-8000-0000000000f1",
      "booking_id": "00000000-0000-4000-8000-0000000000b1",
      "amount_minor": 5000,
      "currency": "EUR",
      "reason": "no_show",
      "status": "pending",
      "created_at": "2026-09-01T08:00:00.000Z",
      "class_name": "Hot Yoga",
      "class_start": "2026-09-01T07:00:00.000Z"
    }
  ],
  "count": 1
}
POST/api/v1/admin/fees/{feeId}/chargeBearer token

Charge an unpaid fee to the card

Collects an open no-show or late-cancel fee from the client’s card. Requires billing.refunds.full and an Idempotency-Key. The fee is loaded scoped to this venue (404 otherwise) and must still be open — a charged, waived or refunded fee answers 422 FEE_NOT_UNPAID with its status before anything is reserved. A declined card answers 422 FEE_CHARGE_DECLINED and the debt stays on the client. Every other non-charged outcome answers 422 FEE_NOT_CHARGEABLE with a plain-language message and details.collection: not_chargeable (no card on file, or card payments not ready — nothing attempted), in_progress (another charge of this fee is running), ambiguous (the provider result is not confirmed; the fee stays protected and is replayed with the same provider key), captured_unrecorded (the card was charged and the receipt is still being recorded — never charged again) or reconciliation_required (an earlier charge needs payment review). The fee is claimed in the database before the card is charged, so the same key may be reused safely. The client is never contacted.

Parameters, scopes and examples

Required scopes

billing.refunds.full

Path parameters

feeIdstring · required
Cancellation fee ID

No fields; send an empty object

Request body

{}

Response example

{
  "fee": {
    "id": "00000000-0000-4000-8000-0000000000f1",
    "status": "charged",
    "amount_minor": 5000,
    "currency": "EUR",
    "reason": "no_show"
  },
  "payment_id": "00000000-0000-4000-8000-0000000000p1"
}
POST/api/v1/admin/fees/{feeId}/settleBearer token

Record an unpaid fee as collected

Records that an open fee was collected outside the card processor — cash, bank transfer, MobilePay, an external card terminal, or other. Requires billing.refunds.full and an Idempotency-Key. There is no amount: a fee is always settled for its own amount. The venue-scoped load and the 422 FEE_NOT_UNPAID check run before anything is reserved. The underlying write is atomic and idempotent on a re-run. A refusal answers 422 FEE_SETTLE_REJECTED with the reason. The client is never contacted.

Parameters, scopes and examples

Required scopes

billing.refunds.full

Path parameters

feeIdstring · required
Cancellation fee ID

How the money was collected, and an optional internal note

Request body

{
  "method": "bank_transfer",
  "note": "Paid at the desk, ref 1234"
}

Response example

{
  "fee": {
    "id": "00000000-0000-4000-8000-0000000000f1",
    "status": "charged",
    "amount_minor": 5000,
    "currency": "EUR",
    "reason": "late_cancel"
  },
  "payment_id": "00000000-0000-4000-8000-0000000000p1"
}
POST/api/v1/admin/fees/{feeId}/waiveBearer token

Waive an unpaid fee

Forgives an open no-show or late-cancel fee: no money is collected and no revenue is recorded. Requires billing.refunds.full and an Idempotency-Key. The venue-scoped load and the 422 FEE_NOT_UNPAID check run before anything is reserved. reason is optional and is recorded in the audit trail only. While a card charge of this fee is running or awaits payment review, the waiver is refused with 409 FEE_PAYMENT_REVIEW_REQUIRED; a fee charged or settled meanwhile answers 422 FEE_NOT_UNPAID; a write that could not be confirmed answers 503 OPERATION_FAILED. In all three nothing is written and the same key may be retried. The client is never contacted.

Parameters, scopes and examples

Required scopes

billing.refunds.full

Path parameters

feeIdstring · required
Cancellation fee ID

Optional internal reason recorded in the audit trail

Request body

{
  "reason": "Goodwill — first missed class"
}

Response example

{
  "fee": {
    "id": "00000000-0000-4000-8000-0000000000f1",
    "status": "waived",
    "amount_minor": 5000,
    "currency": "EUR",
    "reason": "no_show"
  }
}
GET/api/v1/admin/billing/failed-payments/{paymentId}/retry-optionsBearer token

Cards a failed payment can be retried with

The saved cards a retry of this exact failed payment may use, default first. The wallet is read live from the failed invoice or PaymentIntent’s own Stripe customer on the account that payment was made on, so the picker can never offer a card the retry would then fail to charge. Requires passes.manage. 400 INVALID_PAYMENT_ID for a malformed id, 404 NOT_FOUND for another venue’s payment, and 422 RETRY_OPTIONS_UNAVAILABLE when the payment is not failed, has no client wallet, or has no processor customer.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

paymentIdstring · required
Failed payment UUID

Response example

{
  "cards": [
    {
      "id": "pm_example",
      "brand": "visa",
      "last4": "4242",
      "exp_month": 12,
      "exp_year": 2030,
      "is_default": true
    }
  ]
}
POST/api/v1/admin/refunds/{id}/repayment-linkBearer token

Create a repayment link for a refund

Mints a one-time, seven-day link the customer can use to pay back a refund that already completed and should not have. Authority is the same one the web action uses: a venue owner or finance staff member who also holds billing.refunds.full (403 FORBIDDEN otherwise). Idempotency-Key required. The refund is loaded scoped to this venue (404 otherwise) and must be succeeded; 422 REPAYMENT_LINK_UNAVAILABLE carries reason not_succeeded | already_repaid | activated_link_exists | attribution_review_required | payer_unresolved | no_email. Nothing is sent: the platform never contacts the customer here — the operator shares the returned link themselves, exactly as on the web.

Parameters, scopes and examples

Required scopes

billing.refunds.full

Path parameters

idstring · required
Refund UUID

No fields; send an empty object

Request body

{}

Response example

{
  "url": "https://studio.example/repay/8f3c…",
  "expires_at": "2026-09-10T08:00:00.000Z",
  "recipient_email": "client@example.com",
  "amount_minor": 25000,
  "currency": "EUR"
}
GET/api/v1/admin/members/{id}/billing-summaryBearer token

A client’s billing summary

The stat tiles and revenue breakdown for a client’s Billing tab: what they owe, how much of that is overdue, their account credit (which may be negative), their gift-card balance, their lifetime spend and their spend so far this venue-local month, plus their spend split by accounting category. Requires payments.view. Every amount is an integer in minor units. Lifetime and month-to-date are exact server-side sums, not a sample of recent rows; totals_truncated is true only for the rare client whose succeeded-payment history exceeds the 50,000-row walk, and the two sums are then the newest 50,000 payments rather than the exact figure. Saved cards are deliberately not included here — read them from the payment-methods route, which resolves the account the cards actually live on.

Parameters, scopes and examples

Required scopes

payments.view

Path parameters

idstring · required
Active member ID

Response example

{
  "currency": "EUR",
  "venue_today": "2026-09-03",
  "outstanding_minor": 12000,
  "overdue_minor": 5000,
  "credit_balance_minor": -2500,
  "gift_card_balance_minor": 0,
  "lifetime_minor": 480000,
  "this_month_minor": 25000,
  "totals_truncated": false,
  "revenue_breakdown": [
    {
      "category": "Memberships",
      "total_minor": 400000,
      "percentage": 83.3
    }
  ]
}
GET/api/v1/admin/members/{id}/product-subscriptionsBearer token

A client’s product subscriptions

A client’s recurring product subscriptions (lockers, rentals), newest first, with the price in minor units, the billing interval, the next billing date and any scheduled cancellation date. actions.cancel is true only while the subscription is active, pending or past due — the same rule the web section applies. Requires members.view_insights. The app shows the section only when the list is non-empty.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID

Response example

{
  "subscriptions": [
    {
      "id": "00000000-0000-4000-8000-0000000000s1",
      "product_id": "00000000-0000-4000-8000-0000000000d1",
      "product_name": "Locker — large",
      "status": "active",
      "price_minor": 9900,
      "currency": "EUR",
      "billing_interval": "month",
      "billing_interval_count": 1,
      "start_date": "2026-01-01",
      "next_billing_date": "2026-10-01",
      "end_date": null,
      "scheduled_cancel_date": null,
      "auto_renew_enabled": true,
      "created_at": "2026-01-01T08:00:00.000Z",
      "actions": {
        "cancel": true
      }
    }
  ]
}
POST/api/v1/admin/members/{id}/product-subscriptions/{subscriptionId}/cancelBearer token

Cancel a client’s product subscription

Stops a recurring product subscription. mode period_end lets the client keep what they already paid for; mode now ends it immediately. Requires products.manage and an Idempotency-Key. The subscription is loaded bound to BOTH this venue and this client (404 otherwise) and must still be cancellable — anything else answers 422 SUBSCRIPTION_NOT_CANCELLABLE with its status before anything is reserved. A processor refusal answers 502 STRIPE_UPDATE_FAILED and the local row is unchanged. Cancelling cannot be undone. The client is never contacted.

Parameters, scopes and examples

Required scopes

products.manage

Path parameters

idstring · required
Active member ID
subscriptionIdstring · required
Product subscription UUID owned by this client

When the cancellation takes effect

Request body

{
  "mode": "period_end"
}

Response example

{
  "subscription": {
    "id": "00000000-0000-4000-8000-0000000000s1",
    "status": "active",
    "scheduled_cancel_date": "2026-10-01",
    "auto_renew_enabled": false,
    "actions": {
      "cancel": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/extend-graceBearer token

Extend failed renewal booking grace

Permission passes.manage. Requires an Idempotency-Key and a recurring past_due or suspended pass with an outstanding failed subscription payment. A future venue-local grace day up to 90 days ahead restores a suspended pass to past_due while keeping its debt and original failure anchor. Silent; no client notification is sent.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Venue-local inclusive grace day and staff reason

Request body

{
  "grace_until": "2026-10-15",
  "reason": "Approved by the venue manager"
}

Response example

{
  "pass": {
    "id": "00000000-0000-4000-8000-000000000004",
    "status": "past_due",
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
    "clips_remaining": 8,
    "auto_renew": false,
    "pause_start": null,
    "pause_end": null,
    "archived_at": null,
    "billing_mode": "auto_charge",
    "updated_at": "2026-09-03T10:00:00.000Z",
    "failed_payment_grace_until": "2026-10-15"
  },
  "management": {
    "extend": true,
    "adjust_clips": true,
    "transfer": true
  },
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
POST/api/v1/admin/passes/{passId}/extendBearer token

Extend a pass's end date

Client-account parity P1 (A9): push a pass's validity end date out, running the same core the web pass card, the client-list bulk extend and the AI assistant use (audit pass_extended carries the previous end date and the client, so the change stays manually reversible). A staff extension keeps original_end_date from the first extension and never adds to extension_count, which counts only the client's own self-extensions. Permission: passes.manage. Idempotency-Key required; a replay returns the stored response. Client delivery is silent by default and requires notify.audience.clients=true plus an explicit Email/SMS/Push selection AND notifications.send on the same request; an unavailable selected channel returns PASS_NOTIFICATION_CHANNEL_UNAVAILABLE before the pass changes. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'extend' } when the pass cannot be extended (a pass needs an end date); 422 INVALID_RANGE for an end before the start and HARD_END_EXCEEDED for one past the pass type's hard end; 422 NO_END_DATE / AT_HARD_END when the pass has no end date or already runs to (or past) its hard end; 409 PASS_CHANGED when the pass changed while it was being extended (nothing written — retry).

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

New end date and the explicit, default-silent client notification choice

Request body

{
  "new_end_date": "2026-12-31",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/clipsBearer token

Adjust the clips on a pass

Client-account parity P1 (A8): add or remove clips on a clip card. A zero delta is refused with 422 INVALID_DELTA, and the resulting balance floors at 0 (the web rule — staff can zero a card, never owe it). Permission: passes.manage. Idempotency-Key required; a replay returns the stored response. Client delivery is silent by default and requires an explicit channel selection plus notifications.send. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'adjust_clips' } when the pass has no finite clip balance.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Signed clip delta, optional reason, and the client notification choice

Request body

{
  "delta": -2,
  "reason": "Correction after a mis-scan",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/validityBearer token

Adjust a pass's validity window or length

Client-account parity P1 (A10): either move the start and/or end of an activated pass's validity window — the "transfer the activation date" case for manually sold or mis-dated passes — OR, for a pass that has not activated yet (on_first_use/client_chooses_start, no start_date), set how long it stays valid once it does (PASS-VALIDITY-LENGTH-01). The two modes are mutually exclusive on one request: provide at least one of new_start_date, new_end_date or new_duration_days for the DATE mode (an exact new_end_date always wins over a duration), or provide BOTH new_validity_value and new_validity_unit ('days'|'weeks'|'months', bounds 1-1,095 days/1-156 weeks/1-36 months) for the LENGTH mode — never mix the two families on the same call. 422 HARD_END_EXCEEDED when a date-mode end is past the pass type's absolute end date, 422 INVALID_RANGE for every other rejected date-mode window, 422 INVALID_LENGTH for a rejected length (an already-activated pass, a non-on_first_use/client_chooses_start type, an out-of-bounds value, a Flexible-pricing-option pass whose length was frozen at purchase, or a race where the pass activated between read and write) — each with the plain-language message the app shows verbatim. The response reports already_expired when a date-mode window ends before venue-local today (allowed — backdating a correction is legitimate); length mode never sets dates directly, so already_expired is always false for it. This never touches bookings. Permission: passes.manage. Idempotency-Key required. Client delivery is silent by default. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'adjust_dates' }.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Date mode: new window (dates and/or duration). Length mode (unactivated passes only): new_validity_value + new_validity_unit. Optional reason, notification choice.

Request body

{
  "new_start_date": "2026-02-01",
  "new_duration_days": 90,
  "reason": "Sold in January, first class in February",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "already_expired": false,
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/auto-renewBearer token

Turn a membership's auto-renew on or off

Client-account parity P1 (A11): flip auto_renew, syncing Stripe cancel_at_period_end on the subscription's OWN account (a connected-account subscription is always scoped to passes.stripe_account_id), then write the admin-attributed audit row pass.auto_renew_toggled_by_admin. Permission: passes.manage. Idempotency-Key required. Client delivery is silent by default and requires an explicit channel selection plus notifications.send. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'toggle_auto_renew' }.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Desired auto-renew state and the client notification choice

Request body

{
  "auto_renew": false,
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
GET/api/v1/admin/passes/{passId}/convertBearer token

List the conversion targets for a pass

Client-account parity P1 (A21): the venue's non-recurring pass types in name order, minus this pass's current type and minus retired native-managed course/workshop passes — the exact list the web pass editor offers. Read-only: no Idempotency-Key, 30 requests / 60 s per operator. Permission: passes.manage. 404 for a pass that is not this venue's. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'convert_type' } for a recurring or already-ended pass.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "data": {
    "options": [
      {
        "id": "00000000-0000-4000-8000-000000000010",
        "name": "10 Classes",
        "price_amount": 900,
        "currency": "DKK"
      }
    ]
  }
}
POST/api/v1/admin/passes/{passId}/convertBearer token

Convert a pass to another type

Client-account parity P1 (A21): convert a NON-recurring pass (clip card / time pass) to another of the venue's non-recurring types. Credit-balance aware — the unused share of a price drop is issued as account credit and reported as credit_issued_major, so an upgrade never surprise-charges mid-pass. 422 TARGET_RECURRING when the source or the target is a recurring membership (convert those from the subscription page), 422 TARGET_NOT_FOUND for an unknown target or a retired course/workshop pass. Permission: passes.manage. Idempotency-Key required. Client delivery is silent by default. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'convert_type' }.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Target pass type and the client notification choice

Request body

{
  "new_pass_type_id": "00000000-0000-4000-8000-000000000010",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "credit_issued_major": 200,
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/transferBearer token

Transfer a pass to another client

Client-account parity P1 (A22): move a pass to another client of the SAME venue, with both-ends safety — the pass must belong to this venue and the recipient must hold an ACTIVE membership here (422 RECIPIENT_NOT_CLIENT); a recipient who already owns the pass is refused with 422 RECIPIENT_IS_OWNER, and a recurring membership with 422 RECURRING_UNSUPPORTED (manage those from the subscription actions). A reason is required and recorded on the audit row. Both ends are notified through the selected channels — the new owner AND the previous one — so BOTH clear the per-channel preflight before the write. Find recipient_id with GET /admin/members?search=. Permission: passes.manage. Idempotency-Key required. Client delivery is silent by default and requires notifications.send. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'transfer' } for anything but an active non-recurring pass.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Recipient profile id, required reason, and the client notification choice

Request body

{
  "recipient_id": "00000000-0000-4000-8000-000000000003",
  "reason": "Couple sharing — agreed at the desk",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/archiveBearer token

Archive an ended pass

Client-account parity P1 (A20): hide an ENDED pass from the client profile without rewriting its lifecycle — history is preserved, the profile is decluttered. Only a terminated, expired or cancelled pass may be archived (422 ARCHIVE_NOT_TERMINAL: end or terminate it first); a pass that is already archived is an idempotent success no-op. Permission: passes.manage. Idempotency-Key required. This route NEVER notifies the client (the web never does): a notify body is accepted and ignored, and notification_summary always reports no channels. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'archive' }.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Empty body

Request body

{}

Response example

{
  "data": {
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [],
      "sent": false
    }
  }
}
POST/api/v1/admin/passes/{passId}/unarchiveBearer token

Restore an archived pass

Client-account parity P1 (A20): bring an archived pass back into the client profile. A pass that is not archived is an idempotent success no-op. Permission: passes.manage. Idempotency-Key required. This route NEVER notifies the client (the web never does): a notify body is accepted and ignored. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'unarchive' }.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Empty body

Request body

{}

Response example

{
  "data": {
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [],
      "sent": false
    }
  }
}
POST/api/v1/admin/passes/{passId}/cancelBearer token

Cancel a pass now

Client-account parity P1 (A23): cancel a non-recurring pass immediately, with no refund — the pass editor's "Cancel now", server-side. Cancellation is TERMINAL by design: there is deliberately no undo, and the audit row carries everything needed to reconstruct state. Refunds are NOT part of this call — review the exact payment from the client's Billing tab. early_termination_fee reports the fee this cancellation determined applies (null once the binding period is over); this route never charges it. Permission: passes.manage. Idempotency-Key required. The branded cancellation notice is silent by default and requires an explicit channel selection plus notifications.send. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'cancel_now' }; 422 RECURRING_UNSUPPORTED if a recurring membership reaches the core (terminate those from the subscription actions).

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Optional reason and the explicit, default-silent client notification choice

Request body

{
  "reason": "Cancelled by staff",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "early_termination_fee": null,
    "pass": {
      "id": "00000000-0000-4000-8000-000000000004",
      "status": "active",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31",
      "clips_remaining": 8,
      "auto_renew": false,
      "pause_start": null,
      "pause_end": null,
      "archived_at": null,
      "billing_mode": "auto_charge",
      "updated_at": "2026-09-03T10:00:00.000Z"
    },
    "management": {
      "extend": true,
      "adjust_clips": true,
      "transfer": true
    },
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/freeze-previewBearer token

Preview the billing impact of a freeze

Client-account parity P1 (A4): the authoritative Stripe-cycle preview behind the freeze panel. POST carries the two dates but the call is READ-ONLY — it changes nothing, so it takes NO Idempotency-Key and is rate-limited as a read (30 requests / 60 s per operator). preview is null when the pass has no Stripe subscription and there is nothing financial to review. The freeze itself recalculates inside the idempotent financial engine, so this review is advisory: invalidate it whenever either date changes, and echo preview.previewToken as expected_preview_token on POST /admin/members/{id}/membership/pause (422 PREVIEW_STALE when it no longer matches). Permission: passes.manage. 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'freeze' } when the pass cannot be frozen.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Proposed freeze window (pause_end must be after pause_start)

Request body

{
  "pause_start": "2026-10-01",
  "pause_end": "2026-11-01"
}

Response example

{
  "data": {
    "preview": {
      "previewToken": "a1b2c3d4",
      "creditDays": 31,
      "creditAmountMinor": 32300,
      "currency": "DKK"
    }
  }
}
GET/api/v1/admin/passes/{passId}/stripe-pauseBearer token

Inspect a membership's Stripe pause state

Client-account parity P1 (A5): whether the pass's Stripe subscription is ACTUALLY paused, and whether that can be proven against the processor. Drives the "acknowledge unproven pause" confirmation on POST /admin/members/{id}/membership/resume, which accepts resume_from and acknowledge_unproven_pause. Read-only: no Idempotency-Key, 30 requests / 60 s per operator. A pass with no Stripe subscription answers hasStripePause=false, proven=true. Permission: passes.manage. 404 for a pass that is not this venue's; 422 PASS_ACTION_NOT_ELIGIBLE { capability: 'resume' } when the pass is not in a resumable state.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "data": {
    "inspection": {
      "hasStripePause": true,
      "proven": true,
      "stripe": {
        "behavior": "void",
        "resumesAt": "2026-11-01"
      },
      "stored": {
        "freezeStart": "2026-10-01",
        "freezeEnd": "2026-11-01"
      }
    }
  }
}
GET/api/v1/admin/passes/{passId}/billing-modeBearer token

Read how a membership collects renewals

Client-account parity P1 (A13): returns the pass's current renewal collection method. Capability gate: billing_mode (422 PASS_ACTION_NOT_ELIGIBLE otherwise). 404 NOT_FOUND for an unknown or other-venue pass.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "data": {
    "billing_mode": "auto_charge"
  }
}
POST/api/v1/admin/passes/{passId}/billing-modeBearer token

Switch a membership between auto-charge and venue-collected

Client-account parity P1 (A13): choose how FUTURE renewals are collected. 'external' flips the Stripe subscription to collection_method='send_invoice' (Stripe stops auto-charging but keeps raising cycle invoices, and dunning skips the pass); 'auto_charge' restores automatic card collection. The Stripe update is scoped to the pass's own connected account. Capability: billing_mode. Idempotency-Key required. Client delivery is silent by default and accepts only notify.audience.clients=true plus an explicit Email/SMS/Push selection, preflighted on the membership_admin_changed event before the pass changes. Errors: 422 BILLING_MODE_UNCHANGED when the pass already uses that method, 422 NO_CLIENT, 422 STRIPE_NOT_CONFIGURED, 500 STRIPE_UPDATE_FAILED / UPDATE_FAILED. Audit: pass.billing_mode_changed.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Target billing mode and an explicit, default-silent client notification choice

Request body

{
  "mode": "external",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}

Response example

{
  "data": {
    "billing_mode": "external",
    "notification_summary": {
      "requested_channels": [
        "email"
      ],
      "sent": true
    }
  }
}
POST/api/v1/admin/passes/{passId}/renewal-paymentBearer token

Record a venue-collected renewal payment

Client-account parity P1 (A14, EXTERNAL-MINT-01): record that this cycle of an externally billed membership was collected at the venue. Settles the subscription's OLDEST OPEN Stripe invoice through the canonical rails — recordManualSettlement for the attribution intent, then paid_out_of_band — so the invoice.paid recovery writes the payments row, rolls the period, resets clips and sends the receipt. Scoped to the pass's own Stripe account. Capability: billing_mode, and the pass must be billing_mode='external' (422 NOT_EXTERNALLY_BILLED). Idempotency-Key required. Never notifies. Errors: 422 NO_OPEN_INVOICE, 422 NO_SUBSCRIPTION, 422 STRIPE_NOT_CONFIGURED, 502 SETTLEMENT_FAILED / OPERATION_FAILED. Audit: payment.settled_externally (irreversible).

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

How the venue collected the cycle

Request body

{
  "method": "bank_transfer",
  "note": "Paid at the desk, ref 4471"
}

Response example

{
  "data": {
    "invoice_id": "in_1234",
    "amount_minor": 79900,
    "currency": "DKK"
  }
}
GET/api/v1/admin/passes/{passId}/renewal-recoveryBearer token

Read the failed-renewal recovery snapshot

Client-account parity P1 (A15): what is outstanding on a failed recurring renewal — amount, when it failed, how overdue it is, whether booking is suspended, any pending late fee, original payment identity/status, bank failure reason, individual grace date, subscription-pinned card and exact payment-wallet choices. Amounts remain major units; unknown provider/wallet reads are explicit. Deliberately NOT capability-gated: a pass with nothing outstanding returns recovery=null so the card can hide itself, and 404 NOT_FOUND (unknown or other-venue pass) is the only refusal.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "data": {
    "recovery": {
      "amount": 799,
      "currency": "DKK",
      "failed_at": "2026-08-28T06:12:00.000Z",
      "suspended": true,
      "days_overdue": 6,
      "pending_late_fee": {
        "amount": 50,
        "currency": "DKK"
      },
      "payment_id": "11111111-2222-4333-8444-555555555555",
      "payment_status": "failed",
      "failure_reason": "Insufficient funds",
      "grace_until": "2026-09-05",
      "subscription_card_id": "pm_original",
      "subscription_card_known": true,
      "cards_known": true,
      "cards": [
        {
          "id": "pm_original",
          "brand": "visa",
          "last4": "4242",
          "exp_month": 12,
          "exp_year": 2030,
          "is_default": false
        }
      ]
    }
  }
}
POST/api/v1/admin/passes/{passId}/renewal-recovery/chargeBearer token

Charge the outstanding renewal on the saved card

Client-account parity P1 (A15): settle the outstanding renewal off-session on the client's saved card, as the venue. Success uses the canonical invoice.paid finaliser and original payment receipt; deliberate termination prevents reactivation and older invoices cannot roll the current cycle backwards. Capability: renewal_recovery. Idempotency-Key required. Optional payment_id pins the original local debt; optional payment_method_id selects an owned saved card. Legacy empty body remains accepted. Returns additive receipt {operation_id,payment_id,status,settled}; 409 PAYMENT_PENDING retains an uncertain original operation. Retry the same charge endpoint with its original key/body to reconcile; durable SQL ownership survives the outer HTTP reservation. Keep the original key/body and reconcile that payment; never substitute a later debt. Never notifies directly. Errors: 402 RENEWAL_REQUIRES_ACTION when the card needs the CLIENT to confirm (the app then offers the payment link — the client secret is never on this wire), 422 RENEWAL_CHARGE_FAILED with the processor's plain-language reason and additive details.reason (the machine RenewalPayFailureCode); a receipt-less 422 whose reason is PAYMENT_NOT_COLLECTABLE, CARD_PAYMENTS_UNAVAILABLE, CARD_NOT_AVAILABLE or NO_PAYMENT_METHOD was refused before any provider call (sent only while the request owns no durable operation, else 409 PAYMENT_PENDING) and releases the original attempt, while any other or missing reason stays unresolved. Replaying the original key/body after the exact payment settled returns the ordinary paid shape (receipt omitted when no operation was admitted) instead of NOTHING_OUTSTANDING.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

No fields

Request body

{}

Response example

{
  "data": {
    "status": "paid"
  }
}
POST/api/v1/admin/passes/{passId}/renewal-recovery/payment-linkBearer token

Send the client a secure renewal payment link

Client-account parity P1 (A15): email and/or SMS the no-login pay link for the outstanding renewal (deduped once per pass, failure and day). The notification IS the mechanism, so notify is REQUIRED: notify.audience.clients=true plus at least one of channels.email / channels.sms, else 400 CHANNEL_REQUIRED. Push is ignored. notifications.send is re-checked on this request and each channel is preflighted on the renewal_payment_link event before anything is sent (422 PASS_NOTIFICATION_CHANNEL_UNAVAILABLE / 502 PASS_NOTIFICATION_PREFLIGHT_FAILED). Capability: renewal_recovery. Idempotency-Key required. 422 RENEWAL_PAYMENT_LINK_FAILED when the client has no reachable address or the send fails. Audit: membership.renewal_payment_link_sent.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Required client channel selection (email and/or SMS)

Request body

{
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": true
    }
  }
}

Response example

{
  "data": {
    "sent_email": true,
    "sent_sms": false
  }
}
POST/api/v1/admin/passes/{passId}/renewal-recovery/waive-late-feeBearer token

Waive the pending renewal late fee

Client-account parity P1 (A15): delete the late-fee invoice item dunning attached to this failure, while it is still pending. Once the fee lands on a finalized invoice there is nothing to delete and the venue resolves it through the normal refund path — that case answers 422 NO_PENDING_LATE_FEE, checked before the write. Capability: renewal_recovery. Idempotency-Key required; empty body. Never notifies. 422 WAIVE_LATE_FEE_FAILED when Stripe refuses. Audit: membership.late_fee_waived.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

No fields

Request body

{}

Response example

{
  "data": {
    "waived": true,
    "amount": 50,
    "currency": "DKK"
  }
}
POST/api/v1/admin/passes/{passId}/cleanup-setupBearer token

Remove a failed membership setup row

Client-account parity P1 (A19): delete a membership SETUP row that never became a membership — only when Stripe proves the subscription is dead and nothing links to it. Real history is never deleted. expected_updated_at is the optimistic-concurrency token (409 STALE when the row moved). The Idempotency-Key IS the cleanup operation key and MUST be a UUID (400 IDEMPOTENCY_KEY_INVALID); the atomic RPC records it, so a retry whose response was lost replays the committed removal instead of a false 404. Capability: cleanup_failed_setup. Never notifies. Errors: 404 NOT_FOUND, 422 HAS_HISTORY / HAS_LIFECYCLE_HISTORY / PROCESSOR_UNKNOWN / NOT_RECURRING / NOT_SAFE / NOT_REMOVABLE with the web's messages. Audit: membership.setup_artifact_removed, or membership.setup_artifact_removal_denied on a refusal.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

The updated_at the operator reviewed

Request body

{
  "expected_updated_at": "2026-09-01T09:31:22.000Z"
}

Response example

{
  "data": {
    "removed": true
  }
}
GET/api/v1/admin/passes/{passId}/sharesBearer token

Read a pass sharing state

Client-account parity P1 (A16): the whole Share-on-the-pass state for one pass — its shares (with the recipient's display name, monthly cap and classes used this month), its pending invites, and the sharer-slot budget from pass_types.max_sharers. Capability: share (a pass type that allows no sharers is 422 PASS_ACTION_NOT_ELIGIBLE). 404 NOT_FOUND for an unknown or other-venue pass.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "data": {
    "state": {
      "pass_id": "00000000-0000-4000-8000-000000000004",
      "owner_user_id": "00000000-0000-4000-8000-000000000003",
      "pass_type_name": "Unlimited",
      "max_sharers": 2,
      "slots_used": 1,
      "shares": [
        {
          "id": "00000000-0000-4000-8000-00000000051d",
          "shared_with_user_id": "00000000-0000-4000-8000-0000000000c2",
          "name": "Mia Holm",
          "max_classes_per_month": 4,
          "classes_used_this_month": 1,
          "status": "active",
          "shared_at": "2026-08-01T10:00:00.000Z"
        }
      ],
      "invites": []
    }
  }
}
POST/api/v1/admin/passes/{passId}/sharesBearer token

Share a pass with a client or invite one by email

Client-account parity P1 (A16): add one sharer on the owner's behalf. Provide exactly one of recipient_user_id or email; both or neither is 400 VALIDATION_ERROR. An email resolves to a unique active venue member or a pending invitation. One SQL transaction commits and retains that original decision, even if the email or membership later changes. Original Idempotency-Key and body are required. Existing unexpired pre236 HTTP responses are preserved first; new requests never acquire a second HTTP mutation claim. SQL validates staff authority, venue, active pass, sharing rights and slots. Results include a strict shared or invited receipt before optional current pass projection. No token or notification send is returned. 403 PASS_SHARE_FORBIDDEN, 409 PASS_SHARE_REQUEST_CONFLICT and 503 PASS_SHARE_UNRESOLVED retain the original request; never replace its key after an uncertain response.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Exactly one of recipient_user_id or email. Optional monthly cap applies to recipient UUID grants; the legacy email branch ignores it.

Request body

{
  "recipient_user_id": "00000000-0000-4000-8000-0000000000c2",
  "max_classes_per_month": 4
}

Response example

{
  "data": {
    "share_id": "00000000-0000-4000-8000-00000000051d",
    "invite_id": null
  }
}
DELETE/api/v1/admin/passes/{passId}/shares/{shareId}Bearer token

Revoke a pass share

Client-account parity P1 (A16): revoke access using the original pass, share and Idempotency-Key. An empty body resolves the retained share binding before mutable lookup; an explicit recipient_user_id and optional expected_usage_revision preserve the reviewed request. SQL validates staff authority and venue. Existing unexpired pre236 HTTP outcomes are preserved first. Strict revoked or closed proof precedes optional pass projection; a closed result does not claim revocation. 403 forbidden, 409 original-request conflict and 503 unknown outcomes retain the same request. Never notifies.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID
shareIdstring · required
Pass share UUID

Response example

{
  "data": {
    "revoked": true
  }
}
DELETE/api/v1/admin/passes/{passId}/shares/invites/{inviteId}Bearer token

Cancel a pending pass share invite

Client-account parity P1 (A16): cancel a share invite before its token is redeemed, freeing the sharer slot it was holding. Only a still-pending invite can be cancelled — one already accepted or revoked answers 422 INVITE_NOT_PENDING. Pass-scoped: an invite on another pass or in another venue is the same 404 NOT_FOUND. Capability: share. Idempotency-Key required (no body; the invite id rides the request fingerprint). Never notifies. Audit: pass_share_invite_cancelled.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID
inviteIdstring · required
Pass share invite UUID

Response example

{
  "data": {
    "cancelled": true
  }
}
GET/api/v1/admin/passes/{passId}/shares/{shareId}/usage-review/previewBearer token

Review unresolved shared-pass usage

Requires passes.manage AND bookings.manage. Service SQL validates the pass/share tenant and canonical booking location permissions. Returns a bounded page with explicit evidence, generation, revision, aggregate counter, attributed amount and unresolved remainder. after_booking_id UUID cursor; limit 1–200, default 100. Preview is read-only and never changes a counter or sends notifications. Reject an inconsistent snapshot; do not infer unlisted history. HTTP 503 means evidence is unavailable.

Parameters, scopes and examples

Required scopes

passes.managebookings.manage

Path parameters

passIdstring · required
Original pass UUID
shareIdstring · required
Original share UUID belonging to this pass
after_booking_idstring
Previous next_cursor UUID
limitinteger
Page size, 1–200; default 100

Response example

{
  "data": {
    "version": 1,
    "pass_id": "00000000-0000-4000-8000-000000000003",
    "share_id": "00000000-0000-4000-8000-000000000004",
    "generation_id": "00000000-0000-4000-8000-000000000005",
    "usage_revision": 2,
    "counter": 3,
    "attributed": 0,
    "remainder": 3,
    "bookings": [],
    "next_cursor": null
  }
}
POST/api/v1/admin/passes/{passId}/shares/{shareId}/usage-review/readBearer token

Read an original shared-usage review

Requires passes.manage AND bookings.manage and canonical SQL booking location authority. Idempotency-Key (trimmed, control-free, 1–200 characters) and exact original version 1 body are required. One key cannot change actor, organization, pass/share or body. Allocations contain 1–200 unique booking UUIDs, exact reviewed timestamps/pass IDs and contribution 0 or 1; reason is trimmed, 1–1000 characters. Original replay precedes mutable review facts. Applied allocates retained remainder without increasing the aggregate; closed seals that original request without allocating. Both terminal receipts echo the original key/request and immutable pass/share. A null read only means no receipt was found, never permission to discard an uncertain request. 409 conflict, 403 refusal and 503 busy/stale/unknown do not authorize replacement; retain original body/key and read or explicitly close it. Nothing is sent.

Parameters, scopes and examples

Required scopes

passes.managebookings.manage

Path parameters

passIdstring · required
Original pass UUID
shareIdstring · required
Original share UUID belonging to this pass

Exact reviewed version 1 request, retained before the first submission

Request body

{
  "version": 1,
  "expected_generation_id": "00000000-0000-4000-8000-000000000005",
  "expected_usage_revision": 2,
  "expected_counter": 3,
  "allocations": [
    {
      "booking_id": "00000000-0000-4000-8000-000000000006",
      "expected_updated_at": "2026-09-26T10:00:00Z",
      "expected_pass_id": "00000000-0000-4000-8000-000000000003",
      "contribution": 1
    }
  ],
  "reason": "Original attendance records reviewed"
}

Response example

{
  "data": {
    "kind": "applied",
    "operation_id": "00000000-0000-4000-8000-000000000009",
    "pass_id": "00000000-0000-4000-8000-000000000003",
    "share_id": "00000000-0000-4000-8000-000000000004",
    "request": {
      "version": 1,
      "expected_generation_id": "00000000-0000-4000-8000-000000000005",
      "expected_usage_revision": 2,
      "expected_counter": 3,
      "allocations": [
        {
          "booking_id": "00000000-0000-4000-8000-000000000006",
          "expected_updated_at": "2026-09-26T10:00:00Z",
          "expected_pass_id": "00000000-0000-4000-8000-000000000003",
          "contribution": 1
        }
      ],
      "reason": "Original attendance records reviewed"
    },
    "generation_id": "00000000-0000-4000-8000-000000000005",
    "usage_revision": 3,
    "counter": 3,
    "attributed": 1,
    "remainder": 2,
    "replayed": false,
    "idempotency_key": "original-review-key"
  }
}
POST/api/v1/admin/passes/{passId}/shares/{shareId}/usage-review/applyBearer token

Apply an original shared-usage review

Requires passes.manage AND bookings.manage and canonical SQL booking location authority. Idempotency-Key (trimmed, control-free, 1–200 characters) and exact original version 1 body are required. One key cannot change actor, organization, pass/share or body. Allocations contain 1–200 unique booking UUIDs, exact reviewed timestamps/pass IDs and contribution 0 or 1; reason is trimmed, 1–1000 characters. Original replay precedes mutable review facts. Applied allocates retained remainder without increasing the aggregate; closed seals that original request without allocating. Both terminal receipts echo the original key/request and immutable pass/share. A null read only means no receipt was found, never permission to discard an uncertain request. 409 conflict, 403 refusal and 503 busy/stale/unknown do not authorize replacement; retain original body/key and read or explicitly close it. Nothing is sent.

Parameters, scopes and examples

Required scopes

passes.managebookings.manage

Path parameters

passIdstring · required
Original pass UUID
shareIdstring · required
Original share UUID belonging to this pass

Exact reviewed version 1 request, retained before the first submission

Request body

{
  "version": 1,
  "expected_generation_id": "00000000-0000-4000-8000-000000000005",
  "expected_usage_revision": 2,
  "expected_counter": 3,
  "allocations": [
    {
      "booking_id": "00000000-0000-4000-8000-000000000006",
      "expected_updated_at": "2026-09-26T10:00:00Z",
      "expected_pass_id": "00000000-0000-4000-8000-000000000003",
      "contribution": 1
    }
  ],
  "reason": "Original attendance records reviewed"
}

Response example

{
  "data": {
    "kind": "applied",
    "operation_id": "00000000-0000-4000-8000-000000000009",
    "pass_id": "00000000-0000-4000-8000-000000000003",
    "share_id": "00000000-0000-4000-8000-000000000004",
    "request": {
      "version": 1,
      "expected_generation_id": "00000000-0000-4000-8000-000000000005",
      "expected_usage_revision": 2,
      "expected_counter": 3,
      "allocations": [
        {
          "booking_id": "00000000-0000-4000-8000-000000000006",
          "expected_updated_at": "2026-09-26T10:00:00Z",
          "expected_pass_id": "00000000-0000-4000-8000-000000000003",
          "contribution": 1
        }
      ],
      "reason": "Original attendance records reviewed"
    },
    "generation_id": "00000000-0000-4000-8000-000000000005",
    "usage_revision": 3,
    "counter": 3,
    "attributed": 1,
    "remainder": 2,
    "replayed": false,
    "idempotency_key": "original-review-key"
  }
}
POST/api/v1/admin/passes/{passId}/shares/{shareId}/usage-review/closeBearer token

Close an original shared-usage review

Requires passes.manage AND bookings.manage and canonical SQL booking location authority. Idempotency-Key (trimmed, control-free, 1–200 characters) and exact original version 1 body are required. One key cannot change actor, organization, pass/share or body. Allocations contain 1–200 unique booking UUIDs, exact reviewed timestamps/pass IDs and contribution 0 or 1; reason is trimmed, 1–1000 characters. Original replay precedes mutable review facts. Applied allocates retained remainder without increasing the aggregate; closed seals that original request without allocating. Both terminal receipts echo the original key/request and immutable pass/share. A null read only means no receipt was found, never permission to discard an uncertain request. 409 conflict, 403 refusal and 503 busy/stale/unknown do not authorize replacement; retain original body/key and read or explicitly close it. Nothing is sent.

Parameters, scopes and examples

Required scopes

passes.managebookings.manage

Path parameters

passIdstring · required
Original pass UUID
shareIdstring · required
Original share UUID belonging to this pass

Exact reviewed version 1 request, retained before the first submission

Request body

{
  "version": 1,
  "expected_generation_id": "00000000-0000-4000-8000-000000000005",
  "expected_usage_revision": 2,
  "expected_counter": 3,
  "allocations": [
    {
      "booking_id": "00000000-0000-4000-8000-000000000006",
      "expected_updated_at": "2026-09-26T10:00:00Z",
      "expected_pass_id": "00000000-0000-4000-8000-000000000003",
      "contribution": 1
    }
  ],
  "reason": "Original attendance records reviewed"
}

Response example

{
  "data": {
    "kind": "closed",
    "operation_id": "00000000-0000-4000-8000-000000000009",
    "pass_id": "00000000-0000-4000-8000-000000000003",
    "share_id": "00000000-0000-4000-8000-000000000004",
    "request": {
      "version": 1,
      "expected_generation_id": "00000000-0000-4000-8000-000000000005",
      "expected_usage_revision": 2,
      "expected_counter": 3,
      "allocations": [
        {
          "booking_id": "00000000-0000-4000-8000-000000000006",
          "expected_updated_at": "2026-09-26T10:00:00Z",
          "expected_pass_id": "00000000-0000-4000-8000-000000000003",
          "contribution": 1
        }
      ],
      "reason": "Original attendance records reviewed"
    },
    "generation_id": "00000000-0000-4000-8000-000000000005",
    "usage_revision": 3,
    "counter": 3,
    "attributed": 1,
    "remainder": 2,
    "replayed": false,
    "idempotency_key": "original-review-key"
  }
}
POST/api/v1/admin/passes/{passId}/shares/operations/readBearer token

Read an original sharing operation

Requires passes.manage. Retain actor, owning organization, pass UUID, original key and exact versioned request before submitting. The submitted actor is an assertion checked against authenticated authority. The request authority must be staff. Grant uses shareId:null; revoke retains the reviewed share UUID and recipient UUID. Idempotency-Key must equal operationKey. SQL enforces tenant, location, rights and usage atomically. Read returns immutable proof or null; null and errors do not authorize a replacement request. Close seals an unapplied request and returns kind:closed; an existing committed receipt wins. No notifications are sent. HTTP403 is denied scope,409 is conflicting original input,503 is unresolved. Preserve the original request for read/retry/close.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Original owning pass UUID

Exact retained operation envelope; Idempotency-Key echoes operationKey

Request body

{
  "actor": {
    "userId": "00000000-0000-4000-8000-000000000001",
    "orgId": "00000000-0000-4000-8000-000000000002"
  },
  "passId": "00000000-0000-4000-8000-000000000003",
  "shareId": null,
  "operationKey": "original-share-key",
  "request": {
    "version": 1,
    "authority": "staff",
    "action": "grant",
    "recipient_user_id": "00000000-0000-4000-8000-000000000004",
    "expected_usage_revision": null,
    "max_classes_per_month": 3,
    "invite_token": null
  }
}

Response example

{
  "data": {
    "actor": {
      "userId": "00000000-0000-4000-8000-000000000001",
      "orgId": "00000000-0000-4000-8000-000000000002"
    },
    "operationKey": "original-share-key",
    "shareId": null,
    "result": null
  }
}
POST/api/v1/admin/passes/{passId}/shares/operations/applyBearer token

Apply an original sharing operation

Requires passes.manage. Retain actor, owning organization, pass UUID, original key and exact versioned request before submitting. The submitted actor is an assertion checked against authenticated authority. The request authority must be staff. Grant uses shareId:null; revoke retains the reviewed share UUID and recipient UUID. Idempotency-Key must equal operationKey. SQL enforces tenant, location, rights and usage atomically. Read returns immutable proof or null; null and errors do not authorize a replacement request. Close seals an unapplied request and returns kind:closed; an existing committed receipt wins. No notifications are sent. HTTP403 is denied scope,409 is conflicting original input,503 is unresolved. Preserve the original request for read/retry/close.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Original owning pass UUID

Exact retained operation envelope; Idempotency-Key echoes operationKey

Request body

{
  "actor": {
    "userId": "00000000-0000-4000-8000-000000000001",
    "orgId": "00000000-0000-4000-8000-000000000002"
  },
  "passId": "00000000-0000-4000-8000-000000000003",
  "shareId": null,
  "operationKey": "original-share-key",
  "request": {
    "version": 1,
    "authority": "staff",
    "action": "grant",
    "recipient_user_id": "00000000-0000-4000-8000-000000000004",
    "expected_usage_revision": null,
    "max_classes_per_month": 3,
    "invite_token": null
  }
}

Response example

{
  "data": {
    "actor": {
      "userId": "00000000-0000-4000-8000-000000000001",
      "orgId": "00000000-0000-4000-8000-000000000002"
    },
    "operationKey": "original-share-key",
    "shareId": null,
    "result": null
  }
}
POST/api/v1/admin/passes/{passId}/shares/operations/closeBearer token

Close an original sharing operation

Requires passes.manage. Retain actor, owning organization, pass UUID, original key and exact versioned request before submitting. The submitted actor is an assertion checked against authenticated authority. The request authority must be staff. Grant uses shareId:null; revoke retains the reviewed share UUID and recipient UUID. Idempotency-Key must equal operationKey. SQL enforces tenant, location, rights and usage atomically. Read returns immutable proof or null; null and errors do not authorize a replacement request. Close seals an unapplied request and returns kind:closed; an existing committed receipt wins. No notifications are sent. HTTP403 is denied scope,409 is conflicting original input,503 is unresolved. Preserve the original request for read/retry/close.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Original owning pass UUID

Exact retained operation envelope; Idempotency-Key echoes operationKey

Request body

{
  "actor": {
    "userId": "00000000-0000-4000-8000-000000000001",
    "orgId": "00000000-0000-4000-8000-000000000002"
  },
  "passId": "00000000-0000-4000-8000-000000000003",
  "shareId": null,
  "operationKey": "original-share-key",
  "request": {
    "version": 1,
    "authority": "staff",
    "action": "grant",
    "recipient_user_id": "00000000-0000-4000-8000-000000000004",
    "expected_usage_revision": null,
    "max_classes_per_month": 3,
    "invite_token": null
  }
}

Response example

{
  "data": {
    "actor": {
      "userId": "00000000-0000-4000-8000-000000000001",
      "orgId": "00000000-0000-4000-8000-000000000002"
    },
    "operationKey": "original-share-key",
    "shareId": null,
    "result": null
  }
}
GET/api/v1/admin/passes/{passId}/loyaltyBearer token

Get a pass loyalty-price context

Accepts a Bearer JWT with `loyalty_price.grant` OR `passes.manage`. Unlike every other capability-gated pass read, a pass whose type offers no loyalty price and whose client holds no by-hand grant answers `{ context: null }` — never a 422.

Parameters, scopes and examples

Required scopes

loyalty_price.grantpasses.manage

Path parameters

passIdstring · required
Pass id

Response example

{
  "data": {
    "context": {
      "user_id": "uuid",
      "pass_type_name": "10 Classes",
      "currency": "DKK",
      "enabled": true,
      "charges_live": true,
      "current_price": 249,
      "catalog_price": 299,
      "current_override_reason": "loyalty_price: agreed at the desk",
      "status": {
        "status": "active",
        "status_source": "manual_grant",
        "loyal_since": "2026-08-18",
        "grace_expires_on": null,
        "window_closes_on": null
      },
      "config": null,
      "venue_label": "Loyalty Price",
      "history": []
    }
  },
  "error": null
}
POST/api/v1/admin/members/{id}/membership/save-offerBearer token

Issue a save offer at membership termination

Accepts a Bearer JWT with `loyalty_price.grant` (no API-key credential — this is a desk/termination-flow action). Requires an Idempotency-Key (≤128 chars). Issues the comeback offer with `origin: save_accepted` AND applies it immediately (`accepted_via: staff_save`), mirroring the web `acceptSaveOfferAtTermination`; notify is fixed silent. The app then continues its own termination flow.

Parameters, scopes and examples

Required scopes

loyalty_price.grant

Path parameters

idstring · required
Client id

The pass being saved and the win-back template to apply.

Request body

{
  "pass_id": "uuid",
  "template_id": "offer-1"
}

Response example

{
  "data": {
    "offer_id": "uuid",
    "applied": true
  },
  "error": null
}
POST/api/v1/admin/loyalty-price/revokeBearer or API key

Take away a client’s by-hand loyalty price

Accepts exactly one credential: Bearer JWT with `loyalty_price.grant`, or an API key bound to the venue with `write:passes`. Requires a UUID `Idempotency-Key`, mirroring `loyalty-price/grant`. Writes through the context-free `revokeLoyaltyPriceCore` (the ONE writer).

Parameters, scopes and examples

Required scopes

write:passes

user_id + a short reason. Optional pass_id also clears a loyalty override on that pass.

Request body

{
  "user_id": "uuid",
  "reason": "Client asked to go back to the standard rate"
}

Response example

{
  "data": {
    "status": {
      "status": "none",
      "status_source": null,
      "loyal_since": null,
      "grace_until": null,
      "window_closes_on": null
    }
  },
  "error": null
}
POST/api/v1/admin/rates/lockBearer or API key

Lock a pass rate until a future date

Accepts exactly one credential: Bearer JWT with `passes.manage`, or an API key bound to the venue with `write:passes`. Requires an Idempotency-Key (≤128 chars). Default-silent client notification — a non-empty channel selection requires `notifications.send` re-checked on the same request (JWT only; an API-key caller can never notify) plus a per-channel preflight before the mutation — refused as 422 `PASS_NOTIFICATION_CHANNEL_UNAVAILABLE` / 502 `PASS_NOTIFICATION_PREFLIGHT_FAILED`, the same codes every P1 pass route answers.

Parameters, scopes and examples

Required scopes

write:passes

pass_id + the agreed amount + a locked_until date (venue-local, must be after today) + a reason.

Request body

{
  "pass_id": "uuid",
  "locked_rate_amount": 450,
  "locked_until": "2027-01-31",
  "reason": "Agreed one-year price hold"
}

Response example

{
  "data": {
    "ok": true,
    "notification_summary": {
      "requested_channels": [],
      "sent": false
    }
  },
  "error": null
}
DELETE/api/v1/admin/rates/lock/{passId}Bearer or API key

Remove a pass rate lock

Accepts exactly one credential: Bearer JWT with `passes.manage`, or an API key bound to the venue with `write:passes`. Requires an Idempotency-Key (≤128 chars). Default-silent client notification — a non-empty channel selection requires `notifications.send` re-checked on the same request (JWT only; an API-key caller can never notify) plus a per-channel preflight before the mutation — refused as 422 `PASS_NOTIFICATION_CHANNEL_UNAVAILABLE` / 502 `PASS_NOTIFICATION_PREFLIGHT_FAILED`, the same codes every P1 pass route answers.

Parameters, scopes and examples

Required scopes

write:passes

Path parameters

passIdstring · required
Pass id

Response example

{
  "data": {
    "ok": true,
    "notification_summary": {
      "requested_channels": [],
      "sent": false
    }
  },
  "error": null
}
GET/api/v1/admin/rates/context/{passId}Bearer or API key

Get a pass’s full rate context

Accepts a Bearer JWT with `rates.view`, falling back to `passes.manage` on a 403 (mirrors the web `getPassRateContext`), or an API key bound to the venue with `read:passes`.

Parameters, scopes and examples

Required scopes

read:passes

Path parameters

passIdstring · required
Pass id

Response example

{
  "data": {
    "context": {
      "pass_type_name": "10 Classes",
      "currency": "DKK",
      "current_rate": 450,
      "base_rate": 500,
      "active_override": null,
      "active_lock": {
        "amount": 450,
        "until": "2027-01-31",
        "reason": "Agreed one-year price hold"
      },
      "history": [],
      "floating_rates_enabled": true
    }
  },
  "error": null
}
GET/api/v1/admin/passes/{passId}/subscription/timelineBearer token

Read a membership’s billing timeline

Client-account parity P4 (A27): the membership’s real billing history from the processor — the current cycle, whether it is set to cancel, the last twelve invoices with their paid/failed state and hosted links, the next payment, and the card on file. Requires passes.manage (not billing.manage — this mirrors the web editor’s own gate). Capability gate: subscription_timeline, so a non-recurring pass, an unfinished setup and a pass with no live subscription all answer 422 PASS_ACTION_NOT_ELIGIBLE. Every processor call is scoped to the pass’s own connected account. 502 STRIPE_ERROR when the processor is unreachable or unconfigured; 502 INCOMPLETE_PERIOD when it returns a billing period the app cannot render — retry. Nothing is written and the client is never contacted.

Parameters, scopes and examples

Required scopes

passes.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "timeline": {
    "subscription_id": "sub_example",
    "status": "active",
    "current_period_start": "2026-07-01T00:00:00.000Z",
    "current_period_end": "2026-08-01T00:00:00.000Z",
    "cancel_at_period_end": false,
    "cancel_at": null,
    "latest_invoices": [
      {
        "id": "in_example",
        "number": "2026-0042",
        "date": "2026-07-01T00:00:00.000Z",
        "amount_minor": 49900,
        "currency": "EUR",
        "status": "paid",
        "paid": true,
        "hosted_invoice_url": "https://invoice.example/in_example",
        "failure_reason": null
      }
    ],
    "upcoming": {
      "amount_minor": 49900,
      "currency": "EUR",
      "next_payment_attempt": "2026-08-01T00:00:00.000Z"
    },
    "default_payment_method": {
      "last4": "4242",
      "brand": "visa",
      "exp_month": 12,
      "exp_year": 2029
    }
  }
}
POST/api/v1/admin/passes/{passId}/subscription/billing-dateBearer token

Move a membership’s next payment date

Client-account parity P4 (A29): shifts the membership’s next charge to a chosen date, with no proration — the canonical processor mechanism, scoped to the pass’s own connected account. Requires billing.manage, an Idempotency-Key and capability subscription_timeline. new_date is YYYY-MM-DD and must be in the future and within one year; anything else answers 422 INVALID_BILLING_DATE with the exact reason. 422 NO_STRIPE_SUBSCRIPTION when the membership has no recurring billing to shift. 502 STRIPE_ERROR frees the key for a corrected retry — the processor refused before anything moved. 500 DB_ERROR KEEPS the key, because the anchor already moved and a retry must not shift it twice.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

The new date, plus an optional explicit client-notify selection

Request body

{
  "new_date": "2026-08-15",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "next_billing_date": "2026-08-15",
  "previous_next_billing_date": "2026-08-01",
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
POST/api/v1/admin/passes/{passId}/subscription/priceBearer token

Change a membership’s price permanently

Client-account parity P4 (A30): changes what the membership costs from its next payment onward. A new processor price is created on the SAME product and interval and swapped onto the subscription item with no proration; per-period overrides still win for the periods they cover. Requires billing.manage, an Idempotency-Key and capability subscription_timeline. amount_minor is an integer in the venue’s minor units and must be greater than zero. 422 NO_BILLABLE_ITEM when the subscription has no item to reprice. A 502 STRIPE_ERROR frees the key when the new price was never created, and KEEPS it when the price exists but the item swap failed — a retry must not create a second price. 500 DB_ERROR keeps the key (the processor already changed).

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

The new recurring amount in minor units, plus an optional notify selection

Request body

{
  "amount_minor": 59900,
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "base_amount_minor": 59900,
  "previous_base_amount_minor": 49900,
  "currency": "EUR",
  "stripe_price_id": "price_example",
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
GET/api/v1/admin/passes/{passId}/subscription/overridesBearer token

Read a membership’s payment overrides and upcoming periods

Client-account parity P4 (A28): the SERVER-computed preview the web override panel renders, so the app never re-derives billing math. `preview` is the next `periods` billing windows (default 6, 1…36, anything else is 400 VALIDATION_ERROR) with each window marked as the base price or as a covering override, matched exactly as the invoice interceptor matches them. `overrides` is every saved row, ascending by start date; rows whose id appears in no preview period are the panel’s "Other overrides". A row with applied_payment_id set has already charged a real invoice and is locked (no edit, no delete). billing_active is false for a parked or migrated membership with no live subscription — overrides still save, they just stay staged until billing resumes, and billing_inactive_reason is the exact banner text. Requires billing.manage and capability subscription_overrides.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

Query parameters

periodsinteger
How many upcoming billing periods to preview (1…36)Default: 6

Response example

{
  "context": {
    "pass_type_name": "Unlimited",
    "status": "active",
    "next_billing_date": "2026-08-01",
    "base_amount_minor": 49900,
    "currency": "EUR",
    "billing_interval": "month",
    "billing_interval_count": 1,
    "has_stripe_subscription": true,
    "import_resume_state": null,
    "billing_active": true,
    "billing_inactive_reason": null
  },
  "overrides": [
    {
      "id": "00000000-0000-4000-8000-0000000000e1",
      "period_number": 1,
      "period_start_date": "2026-08-01",
      "period_end_date": "2026-08-31",
      "amount_minor": 0,
      "reason": "Prepaid via previous system",
      "applied_at": null,
      "applied_payment_id": null,
      "locked": false
    }
  ],
  "preview": [
    {
      "period_number": 1,
      "start_date": "2026-08-01",
      "end_date": "2026-08-31",
      "amount_minor": 0,
      "is_override": true,
      "override_id": "00000000-0000-4000-8000-0000000000e1",
      "override_reason": "Prepaid via previous system",
      "applied": false
    }
  ]
}
POST/api/v1/admin/passes/{passId}/subscription/overridesBearer token

Apply a payment override to upcoming periods

Client-account parity P4 (A28): sets an agreed price for upcoming membership payments. mode "periods" expands the membership’s real billing interval forward from its next payment date and writes one row per period; mode "range" writes one row covering an explicit window. amount_minor 0 is a comped period — nothing is charged. Requires billing.manage, an Idempotency-Key and capability subscription_overrides. 422 NO_UPCOMING_BILLING when "periods" is used on a membership with no next payment date; 400 INVALID_RANGE when the range ends before it starts; 500 DB_ERROR frees the key because nothing was written. `created` is how many rows were saved.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

Either the next N periods or one explicit window, plus an optional notify selection

Request body

{
  "mode": "periods",
  "periods": 3,
  "amount_minor": 0,
  "reason": "Prepaid via previous system",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "created": 3,
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
PATCH/api/v1/admin/passes/{passId}/subscription/overrides/{overrideId}Bearer token

Amend one scheduled payment override

Client-account parity P4 (A28): changes one not-yet-charged override’s amount and window in place. Requires billing.manage, an Idempotency-Key (bound to this pass AND this override, so a key cannot be replayed against another row) and capability subscription_overrides. The override is resolved only with both the venue and THIS pass, so an unknown id, another venue’s row and another pass’s row all answer the same 404 NOT_FOUND. 422 OVERRIDE_APPLIED when the row already charged a payment — it can no longer be edited. 400 INVALID_RANGE when the window ends before it starts. The answer is the row as stored, not the request echoed back.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID
overrideIdstring · required
Payment override UUID

The corrected amount and window, plus an optional notify selection

Request body

{
  "amount_minor": 25000,
  "start_date": "2026-08-01",
  "end_date": "2026-08-31",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "override": {
    "id": "00000000-0000-4000-8000-0000000000e1",
    "period_number": 1,
    "period_start_date": "2026-08-01",
    "period_end_date": "2026-08-31",
    "amount_minor": 25000,
    "reason": "Prepaid via previous system",
    "applied_at": null,
    "applied_payment_id": null,
    "locked": false
  },
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
DELETE/api/v1/admin/passes/{passId}/subscription/overrides/{overrideId}Bearer token

Remove one scheduled payment override

Client-account parity P4 (A28): removes a not-yet-charged override so that period returns to the normal price. Requires billing.manage, an Idempotency-Key (bound to this pass and this override) and capability subscription_overrides. Same 404 NOT_FOUND rule as the PATCH. 422 OVERRIDE_APPLIED when the row already charged a payment — it cannot be removed retroactively. The body may be empty; send one only to choose notification channels.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID
overrideIdstring · required
Payment override UUID

No fields required; send a notify selection only if the client should hear

Request body

{
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "removed": true,
  "override_id": "00000000-0000-4000-8000-0000000000e1",
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
POST/api/v1/admin/passes/{passId}/subscription/overrides/bulkBearer token

Set or restore several payment periods at once

Client-account parity P4 (A28): the panel’s multi-select writes. action "set" gives every selected period the same agreed amount — periods that already have an override are updated, base periods get a new row, and if the update fails the rows just inserted are removed again so the batch leaves nothing behind (which is why 500 DB_ERROR frees the key). action "restore" deletes the selected overrides so those periods return to the normal price, and is offered only when every selected period IS an override. Requires billing.manage, an Idempotency-Key and capability subscription_overrides. At most six periods or ids per call. 400 INVALID_RANGE for duplicate or inverted periods; 409 OVERRIDE_CONFLICT when a selected row no longer exists or a new period already has one — refresh and try again; 422 OVERRIDE_APPLIED when a selected row already charged a payment.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

Either the selected periods and one amount, or the override ids to restore

Request body

{
  "action": "set",
  "periods": [
    {
      "override_id": "00000000-0000-4000-8000-0000000000e1",
      "start_date": "2026-08-01",
      "end_date": "2026-08-31"
    },
    {
      "override_id": null,
      "start_date": "2026-09-01",
      "end_date": "2026-09-30"
    }
  ],
  "amount_minor": 25000,
  "reason": "Agreed rate",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "updated": 2,
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
GET/api/v1/admin/passes/{passId}/subscription/convertBearer token

List the membership types this membership can convert to

Client-account parity P4 (A31): the venue’s other live recurring membership types, name-ordered, with each one’s price in the venue’s minor units and its billing cadence. The membership’s own type is excluded, and so are archived types — the change engine refuses those anyway, so offering one would be a dead end. Requires billing.manage and capability change_plan (a non-recurring, terminal or unfinished membership answers 422 PASS_ACTION_NOT_ELIGIBLE).

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

Response example

{
  "current_pass_type_id": "00000000-0000-4000-8000-0000000000t1",
  "targets": [
    {
      "id": "00000000-0000-4000-8000-0000000000t2",
      "name": "Unlimited annual",
      "price_minor": 499000,
      "currency": "EUR",
      "billing_interval": "year",
      "billing_interval_count": 1
    }
  ]
}
POST/api/v1/admin/passes/{passId}/subscription/convert/previewBearer token

Quote a membership type change

Client-account parity P4 (A31): quotes the change through the canonical membership-change engine and returns its object verbatim — the same shape GET /api/v1/admin/memberships/change already serves, including the signed, short-lived `quote` the confirm call must hand back. READ-ONLY: it changes nothing, needs NO Idempotency-Key, and runs on the read rate limit. Requires billing.manage and capability change_plan. This editor never offers a custom price, so the quote’s override_amount_minor is always null. Engine refusals map exactly as the memberships/change route maps them (404 / 403 / 409 / 422 / 402 / 503).

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

The target membership type

Request body

{
  "pass_type_id": "00000000-0000-4000-8000-0000000000t2"
}

Response example

{
  "preview": {
    "ok": true,
    "currency": "EUR",
    "direction": "upgrade",
    "applyMode": "immediate",
    "target": {
      "id": "00000000-0000-4000-8000-0000000000t2",
      "name": "Unlimited annual",
      "price_major": 4990,
      "billing_interval": "year",
      "billing_interval_count": 1
    },
    "current": {
      "id": "00000000-0000-4000-8000-0000000000t1",
      "name": "Unlimited",
      "price_major": 499
    },
    "charge_now_major": 4491,
    "credit_now_major": 0,
    "proration_adjustment_major": 4491,
    "settlement": "charge_now",
    "effective_date": "2026-08-01",
    "next_renewal_date": "2027-08-01",
    "clip_change": null,
    "blocked": null,
    "quote": {
      "fingerprint": "0000000000000000000000000000000000000000000000000000000000000000",
      "issued_at": "2026-08-01T09:00:00.000Z",
      "expires_at": "2026-08-01T09:10:00.000Z",
      "override_amount_minor": null
    }
  }
}
POST/api/v1/admin/passes/{passId}/subscription/convertBearer token

Convert a membership to another type

Client-account parity P4 (A31): applies the change the preview quoted. Requires billing.manage, an Idempotency-Key and capability change_plan, and the exact signed quote from the preview — its override_amount_minor MUST be null. Both this route and POST /api/v1/admin/memberships/change converge on the same durable quote fingerprint, so a change started on one cannot double-apply on the other. `replayed` is true when a repeated confirmation converged on an already-applied change; the client is not told twice. 409 QUOTE_STALE and the 422s free the key so the app can re-preview; 402 CHARGE_FAILED, 503 PARTIAL_APPLY, 503 CHANGE_IN_PROGRESS and 503 DATABASE_ERROR KEEP it, because the engine may already have moved money or claimed the quote.

Parameters, scopes and examples

Required scopes

billing.manage

Path parameters

passIdstring · required
Pass UUID

The target type, the signed quote from the preview, and an optional notify selection

Request body

{
  "pass_type_id": "00000000-0000-4000-8000-0000000000t2",
  "quote": {
    "fingerprint": "0000000000000000000000000000000000000000000000000000000000000000",
    "issued_at": "2026-08-01T09:00:00.000Z",
    "expires_at": "2026-08-01T09:10:00.000Z",
    "override_amount_minor": null
  },
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}

Response example

{
  "direction": "upgrade",
  "effective_date": "2026-08-01",
  "charged_minor": 449100,
  "replayed": false,
  "notification_summary": {
    "requested_channels": [
      "email"
    ],
    "sent": true
  }
}
GET/api/v1/admin/members/{id}/payment-methodsBearer token

Selected-client saved-card availability

Business POS read for an active venue client. Returns sanitized card references only (brand, last4, expiry, default, expired, chargeable); imported display-only cards are explicitly non-chargeable. Requires pos.access (register) OR members.contact (client record, the web admin gate). Never returns customer IDs, processor metadata, full card data, or client secrets.

Parameters, scopes and examples

Required scopes

pos.accessmembers.contact

Path parameters

idstring · required
Active member user ID

Response example

{
  "payment_methods": [
    {
      "id": "pm_example",
      "brand": "visa",
      "last4": "4242",
      "exp_month": 12,
      "exp_year": 2030,
      "is_default": true,
      "chargeable": true,
      "expired": false
    }
  ],
  "has_chargeable_card": true
}
POST/api/v1/admin/members/{id}/payment-methods/setup-intentBearer token

Collect a client card in person

Business POS card-setup operation for an active venue client. Requires an in-person consent attestation and Idempotency-Key. Returns the SetupIntent client secret, exact Stripe account namespace, legal merchant country, and frozen regional revision for native Payment Sheet.

Parameters, scopes and examples

Required scopes

members.contact

Path parameters

idstring · required
Active member user ID

Client-present consent attestation

Request body

{
  "consent_channel": "in_person"
}

Response example

{
  "data": {
    "client_secret": "seti_xxx_secret_xxx",
    "setup_intent_id": "seti_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  }
}
PATCH/api/v1/admin/members/{id}Bearer or API key

Update member contact details

Update the member phone number after an explicit staff confirmation. Requires members.edit and writes an audit record.

Parameters, scopes and examples

Required scopes

write:members

Path parameters

idstring · required
Member user ID

Supported member profile fields

Request body

{
  "phone": "+4512345678"
}
POST/api/v1/admin/members/{id}/creditsBearer or API key

Issue account credit

Grant account credit to an active member (positive manual adjustment) on the same atomic, organization-scoped ledger path as the web action. The response balance is the canonical venue-available balance; profiles.credit_balance is maintained only as an account-wide compatibility cache. Currency must equal the venue currency (422 CURRENCY_MISMATCH). Client delivery is silent by default and requires an explicit canonical Email/SMS/Push selection plus notifications.send; membership and unavailable selected channels are rejected before the balance changes, and legacy booleans remain silent. Idempotency-Key is required (1–255 characters): an identical retry returns the original transaction and balance, while reuse for a different semantic request returns 409 IDEMPOTENCY_KEY_REUSE_MISMATCH.

Parameters, scopes and examples

Required scopes

write:members

Path parameters

idstring · required
Member user ID

Credit adjustment

Request body

{
  "amount": 50,
  "currency": "DKK",
  "reason": "Goodwill",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}
GET/api/v1/admin/members/duplicatesBearer token

Scan the venue for duplicate client groups

Venue-wide duplicate suggestions for the clients-list merge wizard. Matches are never merged automatically. Requires members.merge plus members.contact and the acting membership’s full client-contact visibility.

Parameters, scopes and examples

Required scopes

members.mergemembers.contact
GET/api/v1/admin/members/{id}/duplicatesBearer token

Possible duplicates for one client

Ranked duplicate candidates for the current client in this venue, excluding dismissed pairs. Requires members.merge plus members.contact and the acting membership’s full client-contact visibility.

Parameters, scopes and examples

Required scopes

members.mergemembers.contact

Path parameters

idstring · required
Active member ID
POST/api/v1/admin/members/{id}/duplicatesBearer token

Dismiss a duplicate candidate

Dismiss one candidate pair for this venue. Body: { candidate_user_id }. Idempotency-Key required; concurrent reuse is serialized before mutation. Requires members.merge plus members.contact and the acting membership’s full client-contact visibility, matching candidate review.

Parameters, scopes and examples

Required scopes

members.mergemembers.contact

Path parameters

idstring · required
Active member ID
POST/api/v1/admin/members/{id}/merge/previewBearer token

Preview a client profile merge

Fail-closed, venue-scoped merge preview with explicit transfer, retained-identity, indirect invoice descendant, and unsupported-data blocker counts. Immutable brand payment history is counted through a service-only scoped RPC and blocks merging the source profile when populated. Unknown or unreadable ownership is never reported as zero. Body: { primary_user_id, secondary_user_id }. The path id must be one of those two. Requires members.merge plus members.contact and full membership contact visibility. Merge is notification-silent.

Parameters, scopes and examples

Required scopes

members.mergemembers.contact

Path parameters

idstring · required
Primary or secondary member ID
POST/api/v1/admin/members/{id}/mergeBearer token

Merge duplicate client profiles

Merges the secondary profile into the primary survivor using the transactional admin_merge_venue_profiles guard. A populated original-subject brand payment archive blocks the merge, including when the preview is stale. Unsupported data, concurrent setup operations, provider-wallet/subscription ownership changes, and material row conflicts roll back without deactivating the source; retained identity/audit data remains explicit. confirm_token must be the literal MERGE. Notification-silent. Idempotency-Key is bound to venue, path member, and canonical body, with an atomic pre-mutation claim that serializes concurrent reuse. Requires members.merge plus members.contact and full membership contact visibility.

Parameters, scopes and examples

Required scopes

members.mergemembers.contact

Path parameters

idstring · required
Primary or secondary member ID

Survivor, merged-away profile, field choices, and MERGE confirmation

Request body

{
  "primary_user_id": "uuid",
  "secondary_user_id": "uuid",
  "field_choices": {
    "first_name": "primary",
    "phone": "secondary"
  },
  "confirm_token": "MERGE"
}
GET/api/v1/admin/members/{id}/win-backBearer token

Win-back context and offers for a client

Staff win-back templates and this client’s issued offers. Permission: loyalty_price.grant. A venue with loyalty pricing off returns enabled:false.

Parameters, scopes and examples

Required scopes

loyalty_price.grant

Path parameters

idstring · required
Active member ID
POST/api/v1/admin/members/{id}/win-backBearer token

Send a win-back offer

Issues one frozen-term comeback promise per client/template/settlement day, including after that promise closes. Delivery is silent by default; an explicit notify audience and channel set plus notifications.send is required to contact the client. Every selected channel is preflighted before the offer is created and the exact selection only narrows delivery—an SMS-only request can never fall back to email. The response reports actual delivery, and crash-safe deterministic provider/channel dedup permits a silent existing offer to be notified without double-send. Idempotency-Key is bound to venue, path member, and canonical body and atomically claimed before mutation. Permission: loyalty_price.grant.

Parameters, scopes and examples

Required scopes

loyalty_price.grant

Path parameters

idstring · required
Active member ID

Template and optional silent-or-explicit notify

Request body

{
  "template_id": "comeback",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}
POST/api/v1/admin/members/{id}/win-back/{offerId}Bearer token

Close a win-back offer

Mark an open offer as declined (they said no) or revoked (withdrawn). Idempotency-Key required and atomically claimed before mutation. Permission: loyalty_price.grant.

Parameters, scopes and examples

Required scopes

loyalty_price.grant

Path parameters

idstring · required
Active member ID
offerIdstring · required
Offer ID
GET/api/v1/admin/members/{id}/invoicesBearer token

List a client’s formal invoices

Client-record invoices for this member. Drafts are excluded. Each row includes status, totals, and a share_url for the public invoice page. Permission: invoices.view. Org-wide invoice management remains /admin/invoices.

Parameters, scopes and examples

Required scopes

invoices.view

Path parameters

idstring · required
Active member ID
GET/api/v1/admin/members/{id}/invoices/{invoiceId}Bearer token

One client invoice

Detail for one formal client invoice owned by this member in this venue, including share_url. Permission: invoices.view.

Parameters, scopes and examples

Required scopes

invoices.view

Path parameters

idstring · required
Active member ID
invoiceIdstring · required
Invoice ID
GET/api/v1/admin/members/{id}/relationshipsBearer token

List a client’s relationships

Venue-scoped family/partner/guest relationships for this client. Permission: members.view_insights. Related email fields are returned only when the caller also holds members.contact and full membership contact visibility; otherwise they are null.

Parameters, scopes and examples

Required scopes

members.view_insights

Path parameters

idstring · required
Active member ID
POST/api/v1/admin/members/{id}/tagsBearer token

Add a client tag

Add a manual member tag. Idempotency-Key required and atomically claimed before mutation. Permission: members.edit.

Parameters, scopes and examples

Required scopes

members.edit

Path parameters

idstring · required
Active member ID
DELETE/api/v1/admin/members/{id}/tagsBearer token

Remove a client tag

Remove a member tag. The tag travels in the query string (?tag=). Permission: members.edit.

Parameters, scopes and examples

Required scopes

members.edit

Path parameters

idstring · required
Active member ID

Query parameters

tagstring · required
Tag to remove
GET/api/v1/admin/bookingsBearer or API key

All bookings

Venue-wide booking list with filtering by date, status, class, member, and location.

Parameters, scopes and examples

Required scopes

read:bookings

Query parameters

fromstring
Start date (YYYY-MM-DD)
tostring
End date (YYYY-MM-DD)
statusstring
Filter by booking status
class_instance_idstring
Filter by class instance
user_idstring
Filter by member
location_idstring
Filter by location
POST/api/v1/admin/bookingsBearer or API key

Book for member

Create a confirmed or waitlisted booking through canonical pass, course, network and allowance eligibility. Requires a caller-stable Idempotency-Key bound to the full request, including notes and notification choices. A 503 BOOKING_COMMIT_UNCONFIRMED retains the same key and exact body; a recovered committed receipt never repeats notification dispatch or notes writes. Narrow staff timing/capacity exceptions do not waive funding. An authorized after-cutoff attempt returns 422 BOOKING_AFTER_CUTOFF_CONFIRMATION_REQUIRED before mutation; confirm with after_cutoff_confirmed and a new intent key. Same-day ended classes also require ended_class_confirmed. Client delivery is silent by default and requires an explicit Email/SMS/Push selection plus notifications.send; unavailable selected channels are rejected before booking/pass/count effects.

Parameters, scopes and examples

Required scopes

write:bookings

Admin booking

Request body

{
  "user_id": "uuid",
  "class_instance_id": "uuid",
  "pass_id": "uuid",
  "override_capacity": false,
  "after_cutoff_confirmed": false,
  "ended_class_confirmed": false,
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "push": true
    }
  }
}
POST/api/v1/admin/bookings/waitlistBearer or API key

Add member to waitlist

Manually place a member on a class's waitlist at the queue tail. Always creates a waitlisted booking (never auto-confirms). Client delivery is default-silent and requires both an explicit client audience/channel selection and notifications.send; unavailable selected channels are rejected before queue effects. Current actor/location/body authority precedes org-and-intent-bound replay; ADD requires active venue membership. Atomic queue changes do not consume seats or credits. Responses include operation_id, replayed and effects_status (completed, pending or held); a committed change remains successful if follow-up work fails.

Parameters, scopes and examples

Required scopes

write:bookings

Manual waitlist add

Request body

{
  "user_id": "uuid",
  "class_instance_id": "uuid",
  "attendance_type": "physical",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": false
    }
  }
}
DELETE/api/v1/admin/bookings/waitlist/{bookingId}Bearer or API key

Remove member from waitlist

Remove a waitlisted booking. Waitlisted rows only (409 on a confirmed booking); never triggers auto-promotion. Idempotent. Client delivery is default-silent and requires both an explicit client audience/channel selection and notifications.send; unavailable selected channels are rejected before queue effects. Current actor/location/body authority precedes org-and-intent-bound replay; ADD requires active venue membership. Atomic queue changes do not consume seats or credits. Responses include operation_id, replayed and effects_status (completed, pending or held); a committed change remains successful if follow-up work fails.

Parameters, scopes and examples

Required scopes

write:bookings

Path parameters

bookingIdstring · required
Waitlisted booking ID

Optional reason and explicit client notification channels. Omit notify to stay silent.

Request body

{
  "reason": "Client requested removal",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": true
    }
  }
}
POST/api/v1/admin/bookings/{bookingId}/cancelBearer or API key

Cancel a confirmed booking

Cancel a member's confirmed booking on their behalf (admin cancel semantics — may charge late fees + restore clips per policy; NOT the fee-free lapsed-booking path). Decrements booked_count, writes audit + booking.cancelled webhook, and issues a 30s undo ticket. Client delivery is silent by default: notify.clients.channels (or the older notify.{audience,channels}) requires notifications.send and sends the booking_cancelled_client notice with real Email, SMS and Push content on exactly the chosen channels; an unavailable chosen channel returns 422 BOOKING_CANCELLATION_NOTIFICATION_CHANNEL_UNAVAILABLE {audience, channel, reason_code, unavailable_reason} before any mutation; legacy notify_client remains silent. `notified` is true only when a chosen channel was sent or queued; `notify_outcome` reports per chosen channel {sent, skipped, failed, pending, reasons} (null when nothing was chosen). Idempotent via Idempotency-Key. Returns 409 ALREADY_CANCELLED on a cancelled booking, 409 BOOKING_CHANGED when the booking changed during the cancel (retry after refresh), and 409 ON_WAITLIST for a waitlisted row (use the waitlist remove endpoint). The success body carries `warning` (string or null): set when the booking was removed but its clip could not be returned to the pass.

Parameters, scopes and examples

Required scopes

write:bookings

Path parameters

bookingIdstring · required
Confirmed booking ID

Cancel options

Request body

{
  "reason": "Client requested",
  "notify": {
    "clients": {
      "channels": [
        "email",
        "sms"
      ]
    }
  },
  "waive_fee": false
}
GET/api/v1/admin/bookings/{bookingId}/historical-correctionsBearer token

Review a past attendance correction

Human staff JWT plus history and ordinary operation permissions. Returns data.preview with current booking/class CAS, authoritative fee/payment/refund ledger availability, consumed credit, per-audience notification availability and a ten-minute signed reviewToken bound to actor, venue, booking and target. Query operation and status (omit status for invalidate). Does not correct attendance or move money.

Parameters, scopes and examples

Required scopes

scheduling.manage_history

Path parameters

bookingIdstring · required
Existing class booking UUID
POST/api/v1/admin/bookings/historical-correction-notificationsBearer token

Finalize a bulk attendance notification summary

Requires human JWT, scheduling.manage_history and notifications.send. Finalizes the actor-owned notification_batch_id after individual corrections settle. One durable summary per instructor and selected channel includes only committed records. Repeated finalization cannot resend a claimed delivery; finalized batches reject new records. Returns effects, recordCount and recipientCount.

Parameters, scopes and examples

Required scopes

scheduling.manage_historynotifications.send

Stable UUID shared by reviewed corrections in this bulk operation

Request body

{
  "notification_batch_id": "uuid"
}
POST/api/v1/admin/bookings/{bookingId}/historical-correctionsBearer token

Correct a past class roster record

Human staff JWT and additive history and ordinary operation permissions. Idempotency-Key must be a UUID. expected_class_updated_at is a compare-and-set token; expected_updated_at is a compare-and-set token for existing bookings. Existing-booking reasons are optional and the explicit confirmation button suffices; legacy REWRITE remains accepted. GET review_token binds authoritative fee/refund and credit facts before any selected financial or notification action. financial.fee keep/refund/waive and financial.clip keep/return default to keep; explicit refund/waive requires billing.refunds.full, clip return passes.manage, and notifications require notifications.send. Charged fee badges never override succeeded refund ledger evidence. Money stays on the canonical reviewed refund engine; atomic clip/waive changes retain historical and financial audit. Email, SMS, and Push are available only through explicit notify.audience clients/instructors and channel selections; the default is silent. notification_batch_id consolidates bulk instructor notices on the batch-finalization endpoint. Returns attendance result plus per-effect held/pending/completed outcomes; no claim of full financial/delivery success from attendance alone. If the reviewed database executor is unavailable, the route fails closed with HISTORICAL_EXECUTOR_UNAVAILABLE. Retrocreate retains mandatory reason/REWRITE and has no financial or notification effects.

Parameters, scopes and examples

Required scopes

scheduling.manage_history

Path parameters

bookingIdstring · required
Class booking ID

One closed class-booking correction. Every operation requires expected_class_updated_at; existing-booking operations also require expected_updated_at. Existing-booking history_reason and legacy history_confirmation_token are optional; reviewed effects require review_token. Retrocreate still requires REWRITE.

Request body

{
  "operation": "class_booking.correct_attendance_state",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_class_updated_at": "2026-08-20T09:00:00.000Z",
  "history_reason": "Signed paper roster confirms this correction",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-20T10:00:00.000Z",
  "intent": {
    "status": "checked_in"
  }
}
POST/api/v1/admin/memberships/previewBearer token

Preview a staff-created recurring membership

Bearer-JWT, passes.manage-scoped server-authoritative preview for a recurring membership. Resolves the selected client and saved card in the active venue, validates the venue-local start-date policy, and returns canonical buyer-specific gross pricing, registration fee/waiver, due-today amount, access date, first charge, next renewal, card label, contract/terms summary, and — when an operator discount schedule is requested — the resolved discount_schedule block with the agreed amount, the number of discounted periods and the first full-price charge date. A Flexible recurring membership additionally requires exactly one selection (selection_kind=quantity plus quantity, selection_kind=unlimited, or selection_kind=option plus option_id) and returns flexible_selection plus a short-lived flexible_quote bound to that selection, member, venue, active immutable pricing version, regional tax facts, and full minor-unit breakdown. Create must echo that flexible_quote. Sale/preview Flexible option identity on POST /admin/pos/sale uses camelCase optionId; membership uses top-level option_id. Optional additive flash_sale_id maps to the existing core flashSaleId and is exclusive of promo_code. Optional additive promo_window_exception { kind: flash_sale_after_end | late_code_redemption, reason } prices an ended flash_sale_id (requires marketing.flash_sales) or an expired promo_code (requires marketing.campaigns) exactly as in-window; preview records nothing. Manual registration-fee concessions, free periods, and discount schedules are unavailable for Flexible memberships; only the shared recurring Flexible Flash Sale promo is accepted. Returns review {version:1,fingerprint}; create must echo it as expected_review_version and expected_review_fingerprint. saved_payment_method is null for external tender; contract_and_terms.delivery_availability is optional. Contract delivery defaults to false. This endpoint never mutates or charges.

Parameters, scopes and examples

Required scopes

passes.manage

Recurring membership options with exactly one of saved_payment_method_id (card-collected) or external_tender_method (venue-collected renewals, billing_mode=external). Optional registration_fee_discount applies a per-sale percentage discount to the one-time registration fee; legacy waive_registration_fee remains accepted. Optional discount_schedule sets an operator-agreed price for the first period, a fixed number of periods, or for as long as the membership runs; discount_reason stores the staff rationale with review, Stripe metadata, audit, and rate records.

Request body

{
  "member_id": "uuid",
  "pass_type_id": "uuid",
  "start_date": "2026-08-20",
  "payment_timing": "on_start",
  "first_period_free": false,
  "waive_registration_fee": false,
  "registration_fee_discount": {
    "kind": "percent_off",
    "percent": 50
  },
  "saved_payment_method_id": "pm_…",
  "discount_schedule": {
    "duration_kind": "first_n_periods",
    "value_kind": "fixed_price",
    "value": 399,
    "periods": 3
  },
  "discount_reason": "Retention offer approved by studio manager",
  "send_contract_and_terms": false
}
POST/api/v1/admin/membershipsBearer token

Create a recurring membership for a client

Bearer-JWT, passes.manage-scoped recurring membership creation through the canonical subscription checkout core. Contract delivery is default-silent; explicit send_contract_and_terms=true additionally requires notifications.send before mutation. Echo expected_review_version and expected_review_fingerprint from preview.review; a mismatch returns STALE_REVIEW (409), requiring a fresh review. For an ambiguous transport or in-progress retry, retain the exact request body and Idempotency-Key. Idempotency-Key is required. Re-resolves pricing, dates, saved-card ownership, Stripe locality, VAT/age band, concessions, and legal delivery before mutation. Optional additive flash_sale_id maps to the existing core flashSaleId and is exclusive of promo_code. Optional additive promo_window_exception { kind, reason } (same permissions as preview) records an audited 30-minute database exception that waives only the sale end / code expiry; caps, per-client limits, eligibility and price are unchanged, refusals are 422, and it is part of the idempotency fingerprint. Flexible memberships require the exact unexpired flexible_quote returned by preview together with the same selection; the server rebuilds and byte-compares its versioned material and never accepts a client-authored amount. A stale, expired, or mismatched quote is refused before checkout. Returns the exact preview plus pass/subscription ids, payment status, and contract-delivery result. Off-session declines remain 402. First-invoice SCA returns 202 with client_secret/payment_intent_id for in-register confirmation, then POST /admin/memberships/finalize. Succeeded first charges also write a pos_transactions receipt row (transaction_id/payment_id). CARD_DECLINED_SETUP_REMOVED (402) proves a linked declined setup passed processor cleanup and atomic removal; a client may release only its first confirmed refusal. CARD_DECLINED, SCA_REQUIRED, unknown errors, later refusals after response loss and finalize failures do not grant fresh-operation authority.

Parameters, scopes and examples

Required scopes

passes.manage

The same options accepted by preview, plus the exact review version/fingerprint and Flexible quote returned by that review

Request body

{
  "member_id": "uuid",
  "pass_type_id": "uuid",
  "payment_timing": "now",
  "first_period_free": true,
  "waive_registration_fee": true,
  "registration_fee_discount": {
    "kind": "percent_off",
    "percent": 100
  },
  "saved_payment_method_id": "pm_…",
  "send_contract_and_terms": false,
  "expected_review_version": 1,
  "expected_review_fingerprint": "<fingerprint from preview.review>"
}
POST/api/v1/admin/memberships/finalizeBearer token

Finalize an in-register membership card confirmation

Bearer-JWT, passes.manage-scoped completion after a 202 requires_action membership create. Verifies the PaymentIntent succeeded, completes mint/activation through the canonical invoice-paid path, and writes the pos_transactions receipt row only for that PaymentIntent’s succeeded payments row in this venue/member with a correlated membership pass_id. Missing pass_id/pass projection or invoice-handler failure is 409 PAYMENT_PROCESSING and does not cache a created membership. Duplicate finalize replays only a locally correlated payment/pass/receipt. Idempotency-Key required. Body: { payment_intent_id }. Does not bounce staff to member checkout.

Parameters, scopes and examples

Required scopes

passes.manage

Succeeded PaymentIntent from in-register SCA

Request body

{
  "payment_intent_id": "pi_…"
}
GET/api/v1/admin/memberships/change-optionsBearer token

List membership change options for a client pass

Bearer-JWT, passes.manage-scoped option list for an organization-owned pass. Each option is buyer-priced by the canonical membership-change quote engine; a failed target is reported separately and cannot hide valid sibling options. Query: pass_id.

Parameters, scopes and examples

Required scopes

passes.manage
GET/api/v1/admin/memberships/changeBearer token

Preview a client membership change

Bearer-JWT, passes.manage-scoped server quote. Query: pass_id, target_pass_type_id and optional override_price_major. Returns exact charge, credit, effective date, next renewal and a short-lived signed quote binding; it never mutates or charges.

Parameters, scopes and examples

Required scopes

passes.manage
POST/api/v1/admin/memberships/changeBearer token

Apply a client membership change

Bearer-JWT, passes.manage-scoped confirmation through the canonical membership-change core. Requires Idempotency-Key and the exact signed quote returned by preview; foreign-venue passes resolve as not found and client-supplied prices are not accepted.

Parameters, scopes and examples

Required scopes

passes.manage

Accepted server quote binding

Request body

{
  "pass_id": "uuid",
  "target_pass_type_id": "uuid",
  "quote": {
    "fingerprint": "64-character SHA-256 hex",
    "issued_at": "2026-08-05T20:00:00.000Z",
    "expires_at": "2026-08-05T20:05:00.000Z",
    "override_amount_minor": null
  }
}
POST/api/v1/admin/pos/pass-mobilepay/prepareBearer or API key

Prepare original MobilePay pass sale

Requires pos.access OR pos.sell and venue-wide location access. Send the exact original POS sale body with its original Idempotency-Key. One payable pass only; configured built-in MobilePay collection rules remain authoritative. Zero-total sales use the ordinary sale endpoint. Freezes the complete sale before provider preparation; no receipt/link is sent. Retain the encrypted original body/key before calling. Response supplies the canonical transformed-request fingerprint; never hash the snake-case body for recovery. Readiness remains disabled until release acceptance.

Parameters, scopes and examples

Existing POS sale body; original Idempotency-Key header required

Request body

{
  "member_id": "uuid",
  "items": [
    {
      "type": "pass",
      "pass_type_id": "uuid"
    }
  ],
  "payment_method": "mobilepay",
  "expected_total": 100
}
POST/api/v1/admin/pos/pass-mobilepay/recoverBearer or API key

Resolve original MobilePay pass sale

Requires original authorized actor/venue and current POS/location access. Reads only the exact original body fingerprint/key and optional bound PI; never reconstructs a sale or allocates a replacement source. Returns original approval or verified original completion, including after a subsequent refund; payment_status is the actual current payment state, never an invented succeeded fallback. Missing/unknown sources stay resolving. Keep approval URLs only in memory; closing the panel never cancels. Poll sequentially every three seconds for at most four minutes, then offer explicit original-status checks. No automatic SMS.

Parameters, scopes and examples

Original identity; operation_key is the original sale key, not a new request key

Request body

{
  "member_id": "uuid",
  "pass_type_id": "uuid",
  "operation_key": "original-sale-key",
  "request_fingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "payment_intent_id": "pi_original"
}
POST/api/v1/admin/pos/pass-mobilepay/linkBearer or API key

Explicitly send original MobilePay pass link

Requires notifications.send independently of POS transaction and venue-wide location access. Send only after an explicit staff choice. Reads the original bound source/account/PI and obtains its approval URL from the provider; never accepts a caller URL or creates/confirms a payment. Canonical recipient normalization, consent, preferences, suppression, provisioning and source-bound outbox deduplication apply. Accepted means provider acceptance, not delivery. Accepted without notification_id and unknown outcomes must lock repeat send; no automatic replacement or retry.

Parameters, scopes and examples

Required scopes

notifications.send

Original identity plus explicitly chosen phone

Request body

{
  "member_id": "uuid",
  "pass_type_id": "uuid",
  "operation_key": "original-sale-key",
  "request_fingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "payment_intent_id": "pi_original",
  "phone": "+4512345678"
}
POST/api/v1/admin/pos/pass-mobilepay/link-statusBearer or API key

Read original pass link delivery evidence

Requires original authorized actor/venue and current POS/location access. Reads a notification scoped to this exact sale, member, organization and template. Pending/unknown is not acceptance; failed is not permission to resend. This endpoint never sends. Poll sequentially with a fixed four-minute deadline.

Parameters, scopes and examples

Original identity plus original notification ID

Request body

{
  "member_id": "uuid",
  "pass_type_id": "uuid",
  "operation_key": "original-sale-key",
  "request_fingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "payment_intent_id": "pi_original",
  "notification_id": "uuid"
}
POST/api/v1/admin/pos/saleBearer or API key

POS sale

Process an idempotent, default-silent point-of-sale transaction with pos.access OR pos.sell and venue-wide staff location access. No receipt is queued by this endpoint; an explicit post-sale receipt choice uses the separate receipt endpoint. Supports cash, venue credit, and a server-validated saved card. Pass carts may send split_payments (max 8), Flexible/rich pass fields, and optional additive flash_sale_id (exclusive of promo_code; resolved by the existing sale core). Optional additive promo_window_exception { kind, reason } (pass sales for a client only; reason 5-500 characters): flash_sale_after_end sells an ended flash_sale_id and requires marketing.flash_sales; late_code_redemption uses an expired promo_code and requires marketing.campaigns. Only the time window is waived (published sale that ended by time / code expired in the last 90 days); caps, per-client limits, eligibility, scope and price are unchanged. It is part of the idempotency fingerprint, records an audited 30-minute database exception, and refusals are 422. One sale may carry many lines with quantities (max 50 lines, 100 units per line): products with bundles, or services with products; a pass is always its own sale (422 MIXED_CART_NOT_SUPPORTED) and bundles never combine with services. products.max_quantity_per_order applies to every cart shape, summed across lines, after replay lookup and before collection (422 PRODUCT_QUANTITY_LIMIT with details.product_id/max_quantity/requested_quantity); a retried or 3DS-completed sale keeps its frozen cart. expected_total from POST /admin/pos/catalog-preview guards product, bundle and service carts (400 PRICE_CHANGED before collection). Ordinary product card sales may select product_currency with product_price_book_versions (product UUID to accepted version), the exact signed product_quote, and required expected_total from catalog-preview. New operations verify complete residence, item and included-tax authority before collection; existing operation recovery precedes fresh quote expiry or changed-evidence checks. The member must have current home-country billing evidence; every line needs an enabled catalog-tax book. Commissions, loyalty/course/staff benefits, recurring products, product variants, bundles, stored value and other collection methods are refused with PRODUCT_PRICE_BOOK_UNAVAILABLE (422). The exact selection remains part of the retry/finalize body; replay uses frozen money even after settings change. This additive path requires its selected-product SQL authority before activation. Saved-card SCA returns a 202 challenge response and is completed with a separate idempotent finalize request. Recurring memberships stay on POST /admin/memberships. Fresh cards, MobilePay and Stripe Terminal use their dedicated flows.

Parameters, scopes and examples

Sale

Request body

{
  "member_id": "uuid",
  "items": [
    {
      "type": "product",
      "product_id": "uuid",
      "quantity": 1
    },
    {
      "type": "bundle",
      "bundle_id": "uuid",
      "quantity": 1
    }
  ],
  "payment_method": "card",
  "saved_card_id": "pm_…",
  "split_payments": [
    {
      "method": "cash",
      "amount": 100
    }
  ]
}

Response example

{
  "data": {
    "payment_status": "requires_action",
    "client_secret": "pi_xxx_secret_xxx",
    "payment_intent_id": "pi_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  }
}
GET/api/v1/admin/pos/recentBearer or API key

Recent POS transactions

Recent POS transactions filtered by location and date. Requires pos.access and venue-wide staff location access; a location filter never grants access to restricted staff. Refund headroom subtracts both succeeded and in-flight operation claims; refunded_amount reports succeeded claims and pending_refund_amount reports the reserved in-flight amount.

Parameters, scopes and examples

Required scopes

pos.access
POST/api/v1/admin/pos/transactions/{id}/receiptBearer or API key

Resend POS receipt

Send a tenant-scoped POS transaction receipt by an explicit email, SMS or push choice. Requires pos.access, notifications.send and venue-wide staff location access before delivery. Uses the client's stored contact unless an explicit recipient is supplied for email/SMS. Idempotency-Key is required and retries must keep its exact body. Atomic caller/venue/payload claims prevent concurrent re-sends during the replay window. RECEIPT_CHANNEL_UNAVAILABLE means no provider send was attempted; RECEIPT_DELIVERY_UNCONFIRMED retains the original operation for reconciliation. A sent result is provider acceptance, not device delivery.

Parameters, scopes and examples

Required scopes

pos.accessnotifications.send

Path parameters

idstring · required
POS transaction ID

Receipt delivery channel and optional recipient override

Request body

{
  "method": "email"
}
GET/api/v1/admin/pos/summaryBearer or API key

Daily POS sales summary

Requires pos.access and venue-wide staff location access. Daily sales breakdown for the given date (default today): totals (gross/discounts/VAT/credits/net) plus per-payment-method and per-transaction-type buckets. Completed transactions only; same date-window semantics as /admin/pos/recent.

Parameters, scopes and examples

Required scopes

pos.access

Query parameters

datestring
YYYY-MM-DD (default today)
location_idstring
Filter by location
GET/api/v1/admin/pos/payment-methodsBearer or API key

Read configured POS tenders

Active configured tenders are available with settings.business OR venue-wide POS transaction access (pos.access OR pos.sell). include_archived=true requires settings.business. Creation, edits and archiving remain settings.business-only. Collector availability remains enforced by the canonical POS engine.

Parameters, scopes and examples

Query parameters

include_archivedboolean
Include archived tenders; requires settings.businessDefault: false
POST/api/v1/admin/checkin/{classInstanceId}/bulkBearer token

Bulk check-in, no-show or undo

Requires booking.checkin; no-show additionally requires bookings.mark_no_show. Only canonical notify audience/client channel choices enable delivery and require notifications.send before mutation. Omitted/empty channels and legacy booleans remain silent. Each recipient is preflighted; a failed check-in never dispatches. Successful explicit check-in delivery uses attendance_corrected with the booking-owning venue. Existing check-in cutoff, history, tenant and idempotency rules remain enforced. Check-in skips streaming (online) bookings, whose attendance is recorded on stream playback, and lists them in an additive skipped array ({ booking_id, code: ONLINE_ATTENDANCE_AUTO, reason }) instead of failing them.

Parameters, scopes and examples

Required scopes

booking.checkin

Path parameters

classInstanceIdstring · required
Class instance UUID

Selected action, booking IDs and explicit client channels

Request body

{
  "action": "check_in",
  "booking_ids": [
    "uuid"
  ],
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {}
  }
}
GET/api/v1/admin/pos/transactions/{id}/receiptBearer or API key

Read receipt channel availability

Read canonical email/SMS/push contact, preference and venue capability reasons for a tenant-owned sale. Requires pos.access and venue-wide location access. Opening the sheet never sends a receipt; POST separately requires notifications.send.

Parameters, scopes and examples

Required scopes

pos.access

Path parameters

idstring · required
POS transaction UUID
GET/api/v1/admin/terminal/receiptBearer or API key

Read captured-sale receipt availability

Resolves the stored payment and POS transaction inside the authenticated venue, then returns the same canonical receipt channel availability. Requires pos.access and venue-wide location access. This read does not enable hardware collection or send a receipt.

Parameters, scopes and examples

Required scopes

pos.access

Query parameters

payment_intent_idstring · required
Recorded payment intent ID
GET/api/v1/admin/dashboard/revenue-seriesBearer or API key

Daily revenue series (sparkline)

Zero-filled daily revenue series ending today — succeeded payments bucketed by UTC day, matching the dashboard revenue_today semantics. days clamps to 1–90 (mobile uses 7 and 30).

Parameters, scopes and examples

Required scopes

read:reports

Query parameters

daysinteger
Window length in days (1–90)Default: 7
POST/api/v1/admin/members/{id}/membership/pauseBearer or API key

Pause membership

Pause (freeze) a member’s pass for a date window. Validated against the pass type’s pause policy; recurring memberships receive exact per-cycle billing credits on their own Stripe account; audit_log pass_paused. Client delivery is silent by default and requires notify.audience.clients=true plus explicit Email/SMS/Push channels and notifications.send. Legacy notification booleans remain silent. Selected-channel availability is rechecked before mutation; exact unavailable/preflight-failed responses leave the membership unchanged. Idempotency-Key honored. {id} accepts UUID or display ID (e.g. HYC-0042).

Parameters, scopes and examples

Required scopes

write:members

Path parameters

idstring · required
Member user ID or display ID

Pause window

Request body

{
  "pass_id": "uuid",
  "pause_start": "2026-08-01",
  "pause_end": "2026-08-21",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": false,
      "push": true
    }
  }
}
POST/api/v1/admin/members/{id}/membership/resumeBearer or API key

Resume membership

Resume a paused pass (Stripe-first ordering with compensating re-pause). Audit_log pass_resumed. Client delivery is silent by default and uses only an explicit canonical Email/SMS/Push selection. Selected-channel availability is rechecked before mutation; exact unavailable/preflight-failed responses leave the membership unchanged. Idempotency-Key honored.

Parameters, scopes and examples

Required scopes

write:members

Path parameters

idstring · required
Member user ID or display ID

Resume immediately or from a venue-local date. An unproven Stripe pause stays blocked unless acknowledge_unproven_pause types CLEAR PAUSE plus a reason. Optional explicit client delivery.

Request body

{
  "pass_id": "uuid",
  "resume_from": "2026-09-15",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}
POST/api/v1/admin/members/{id}/membership/terminateBearer or API key

Cancel / terminate membership

Cancel or terminate a recurring membership with explicit effective dates: mode period_end (cancel at current cycle end), chosen_cycle (kth upcoming cycle, cycle required), or immediate. Runs the kill-switch-gated termination engine (fail-closed Stripe). Response carries the engine-confirmed effective_at. Client delivery is silent by default and requires an explicit canonical channel choice plus notifications.send; legacy booleans remain silent. Selected-channel availability is rechecked before mutation; exact unavailable/preflight-failed responses leave the membership unchanged. Idempotency-Key honored.

Parameters, scopes and examples

Required scopes

write:members

Path parameters

idstring · required
Member user ID or display ID

Termination request

Request body

{
  "pass_id": "uuid",
  "mode": "period_end",
  "reason": "Member request",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true,
      "sms": true
    }
  }
}
GET/api/v1/admin/members/{id}/membership/termination-previewBearer or API key

Termination preview (cycle picker)

Next 6 cycle boundaries (effective_at, venue-local last usable day, precedes-binding flag), venue policy defaults, and billing horizon for the terminate endpoint’s cycle picker.

Parameters, scopes and examples

Required scopes

read:members

Path parameters

idstring · required
Member user ID or display ID

Query parameters

pass_idstring
Pass ID (uuid)
POST/api/v1/admin/terminal/connection-tokenBearer or API key

Stripe Terminal connection token

Mint a Stripe Terminal connection token plus the venue Terminal location id (`{secret, location_id}`) for card-present readers and Tap-to-Pay. Ephemeral-token fetch — no Idempotency-Key (the Terminal SDK always needs a fresh token).

Parameters, scopes and examples

Required scopes

write:pos
POST/api/v1/admin/terminal/payment-intentBearer or API key

Create Terminal payment intent

Create a card-present PaymentIntent (manual capture) on the venue connected account. Returns `{client_secret, payment_intent_id}`. Money mutation — send an Idempotency-Key; replays return the cached response and the key is forwarded to Stripe.

Parameters, scopes and examples

Required scopes

write:pos

Payment intent

Request body

{
  "amount": 12000,
  "currency": "DKK"
}
POST/api/v1/admin/terminal/captureBearer or API key

Capture Terminal payment

Capture a confirmed card-present PaymentIntent. Returns `{captured: true, payment_intent_id}`. Money mutation — send an Idempotency-Key; an already-captured intent returns success.

Parameters, scopes and examples

Required scopes

write:pos

Capture

Request body

{
  "payment_intent_id": "pi_xxx"
}
POST/api/v1/admin/terminal/receiptBearer token

Send Terminal receipt

Email or SMS a receipt for a captured Tap-to-Pay sale, resolved from the Stripe payment intent id. Requires pos.access, notifications.send and venue-wide location access before delivery. TERM-IDEMP-01: requires a caller-scoped Idempotency-Key; a replayed key returns the cached terminal response instead of re-sending. The atomic claim is bound to caller, venue and exact body. RECEIPT_CHANNEL_UNAVAILABLE is a confirmed pre-send refusal; RECEIPT_DELIVERY_UNCONFIRMED must retain the original operation for reconciliation.

Parameters, scopes and examples

Required scopes

pos.accessnotifications.send

Receipt

Request body

{
  "payment_intent_id": "pi_xxx",
  "method": "email",
  "recipient": "client@example.com"
}
POST/api/v1/admin/notifications/broadcastBearer or API key

Send broadcast

Send one or more push, email, and SMS channels to all members, selected member ids, or a server-resolved tag/pass/class audience. Idempotency-Key is required. Each channel derives a stable per-recipient delivery reference; a partial retry skips terminal successes/suppressions and resumes failed legs. Returns sent/skipped/failed counts per channel.

Parameters, scopes and examples

Required scopes

write:notifications

Broadcast

Request body

{
  "channels": [
    "email",
    "sms"
  ],
  "title": "New class added",
  "target": {
    "type": "tag",
    "tag": "vip"
  },
  "subject": "New class added",
  "body": "Check out our new Hot Power class on Saturday!"
}
GET/api/v1/admin/notifications/recentBearer or API key

Recent notifications

Recent email, SMS, and push notifications sent by the venue.

Parameters, scopes and examples

Required scopes

read:notifications
POST/api/v1/admin/staff/inviteBearer token

Invite a staff member

PROMPT_02 (S1-03) — provisions the auth user + profile + membership (status=invited), mints a staff_invitations claim token, and emails the venue-branded /auth/claim-invite link. Permission: staff.manage. Membership role may be admin, manager, reception, instructor, staff, or service_provider; profiles.role stays on the legacy CHECK (staff/provider seats persist as member there). The token is consumed by the WEB claim page (set password → membership flips invited→active); there is no separate accept API endpoint because the claim sets a password. 409 EMAIL_EXISTS when a Booking Bible account already exists for the email (adding an existing user as staff is a role change — use the admin UI). location_ids is stored on the invitation for record-keeping; location assignment remains a post-onboarding admin action. Emits staff.invited. Idempotency-Key supported.

Parameters, scopes and examples

Invitation fields

Request body

{
  "email": "teacher@example.com",
  "role": "instructor",
  "first_name": "Anna",
  "last_name": "Jensen",
  "location_ids": []
}
GET/api/v1/admin/staff-scheduleBearer token

List all staff shifts

Every staff shift in the venue for a date range. Permission: staff_scheduling.view. Joins staff profile name. Optional ?status= filter.

Parameters, scopes and examples

Query parameters

fromstring
Start ISO datetimeDefault: -7 days
tostring
End ISO datetimeDefault: +14 days
statusstring
Filter by ShiftStatus
POST/api/v1/admin/staff-scheduleBearer token

Create a staff shift

Create a new shift. Permission: staff_scheduling.manage. Note: API path skips the engine compliance pre-checks; for full compliance use the admin panel or the createShift server action.

Parameters, scopes and examples

Shift fields

Request body

{
  "staff_id": "uuid",
  "location_id": "uuid",
  "shift_type": "regular",
  "start_time": "2026-05-01T09:00:00Z",
  "end_time": "2026-05-01T17:00:00Z",
  "break_minutes": 30,
  "role_required": "reception",
  "hourly_rate": 200
}
POST/api/v1/admin/staff-schedule/{id}/assignBearer token

Assign a staff member to a shift

Permission: staff_scheduling.manage. Emits shift.assigned.

Parameters, scopes and examples

Path parameters

idstring · required
Shift ID

Staff to assign

Request body

{
  "staff_id": "uuid"
}
GET/api/v1/admin/walk-in-queueBearer or API key

Walk-in queue

Current walk-in queue (waiting + notified) for the authenticated org, ordered by position.

Parameters, scopes and examples

Required scopes

read:bookings
POST/api/v1/admin/walk-in-queueBearer or API key

Add walk-in

Add a walk-in to the queue. Allocates an atomic position via allocate_queue_position(). Idempotency-Key required. Add is silent; notification choice belongs to a later explicit Call. Optional service_id, client_id, preferred_provider_id and location_id must belong to the authenticated organization.

Parameters, scopes and examples

Required scopes

write:bookings

Walk-in entry

Request body

{
  "customer_name": "Jane Doe",
  "customer_phone": "+4512345678",
  "notify_sms": false
}
POST/api/v1/admin/walk-in-queue/{id}/callBearer or API key

Call queue entry

Atomically claim a waiting queue entry as called. Default-silent; explicit notify_sms=true requires notifications.send and SMS availability before mutation. Saved Add-time preferences never authorize delivery. A concurrent or already-called entry returns 409 INVALID_STATE. Returns sent_sms and optional notification_failure separately from committed call success. Permission: bookings.manage.

Parameters, scopes and examples

Required scopes

write:bookings

Path parameters

idstring · required
Queue entry ID

Explicit notification choice for this call only

Request body

{
  "notify_sms": false
}
DELETE/api/v1/admin/walk-in-queue/{id}Bearer or API key

Remove walk-in

Cancel/remove a walk-in queue entry. Permission: bookings.manage.

Parameters, scopes and examples

Required scopes

write:bookings

Path parameters

idstring · required
Queue entry ID
GET/api/v1/admin/checkin/{classInstanceId}/qr-tokenBearer or API key

Get check-in QR token

Returns the current rotating QR token for a class instance. A new token is generated if none exists or the existing one is expired. Force rotation with ?refresh=true. Token TTL: 5 minutes. Permission: booking.checkin.

Parameters, scopes and examples

Required scopes

write:checkins

Path parameters

classInstanceIdstring · required
Class instance UUID

Query parameters

refreshboolean
Force generate a new token (rotate)Default: false

Response example

{
  "data": {
    "token": "a1b2c3d4e5f6...64hex chars",
    "expires_at": "2026-06-01T10:05:00Z",
    "class_instance_id": "uuid"
  }
}
POST/api/v1/admin/schedule/series/previewBearer token

Preview a new class series

Read-only venue-local projection, reference validation, conflicts and daylight-saving time choices. No classes are created. The same body is accepted by the create endpoint.

Parameters, scopes and examples

Required scopes

scheduling.manage

Series template with venue-local wall times and recurrence.

Request body

{
  "location_id": "00000000-0000-4000-8000-000000000001",
  "room_id": "00000000-0000-4000-8000-000000000002",
  "class_type_id": "00000000-0000-4000-8000-000000000003",
  "day_of_week": 2,
  "start_time": "17:00",
  "end_time": "18:00",
  "effective_from": "2026-09-22",
  "recurrence_type": "weekly"
}
POST/api/v1/admin/schedule/seriesBearer token

Create a class series

Creates the template and projected occurrences atomically. Idempotency-Key is required; the durable operation is scoped to actor, venue and endpoint. Reusing an operation with a different body returns a conflict. Times resolve in the venue timezone; nonexistent wall times are refused and repeated wall times require an explicit choice.

Parameters, scopes and examples

Required scopes

scheduling.manage

Reviewed series template; confirm_conflicts explicitly accepts permitted conflicts.

Request body

{
  "location_id": "00000000-0000-4000-8000-000000000001",
  "room_id": "00000000-0000-4000-8000-000000000002",
  "class_type_id": "00000000-0000-4000-8000-000000000003",
  "day_of_week": 2,
  "start_time": "17:00",
  "end_time": "18:00",
  "effective_from": "2026-09-22",
  "recurrence_type": "weekly"
}
GET/api/v1/admin/schedule/series/{id}Bearer token

Read a class series

Returns the authoritative venue-scoped template for editing. Existing occurrence times can differ from the template; clients must display and choose the intended source explicitly.

Parameters, scopes and examples

Required scopes

scheduling.manage

Path parameters

idstring · required
Series UUID in the authenticated venue
POST/api/v1/admin/schedule/series/{id}/previewBearer token

Preview a class series edit

Read-only preview for this_only, all_future, from_date or all_including_past. Reports conflicts, affected bookings, capacity floors, notification availability and venue-local time ambiguity. A preview never authorizes client delivery.

Parameters, scopes and examples

Required scopes

scheduling.manage

Path parameters

idstring · required
Series UUID in the authenticated venue

Patch and edit scope; from_date is required for that scope.

Request body

{
  "patch": {
    "start_time": "17:30",
    "end_time": "18:30"
  },
  "scope": "all_future"
}
PATCH/api/v1/admin/schedule/series/{id}Bearer token

Apply a reviewed class series edit

Idempotency-Key is required. Start and end are independent fields. The canonical transaction validates the resulting series and current physical/online booking counts. Existing booked/past confirmation requirements remain. Staff delivery defaults silent; only explicit participant or instructor channels request delivery, with notifications.send checked before mutation. Cancelled occurrences require a separate reviewed reactivation instead of silently recreating bookings.

Parameters, scopes and examples

Required scopes

scheduling.manage

Path parameters

idstring · required
Series UUID in the authenticated venue

Patch, scope, applicable confirmations, and optional explicit notify channels.

Request body

{
  "patch": {
    "start_time": "17:30",
    "end_time": "18:30"
  },
  "scope": "all_future"
}
GET/api/v1/admin/schedule/series/{id}/reactivationBearer token

Review cancelled series occurrences

Read-only review of eligible future cancelled occurrences and previous cancelled booking candidates. Returns opaque schedule/occurrence versions and venue-local projected times. Candidate inclusion is not booking eligibility approval; canonical booking rules are checked when each selected client is restored.

Parameters, scopes and examples

Required scopes

scheduling.manage

Path parameters

idstring · required
Series UUID in the authenticated venue
POST/api/v1/admin/schedule/series/{id}/reactivationBearer token

Reactivate reviewed series occurrences

Requires Idempotency-Key and the exact reviewed schedule and occurrence versions. Selected client restoration additionally requires booking authority. Previous cancellation history remains immutable; selected clients receive new canonical bookings or individual ineligible/pending outcomes and current waitlist placement. No client notification or payment is requested by this endpoint. Operational projections retain their own recorded effect state. Stale reviews and changed retry intent return 409.

Parameters, scopes and examples

Required scopes

scheduling.manage

Path parameters

idstring · required
Series UUID in the authenticated venue

Explicit occurrence and client selection. Empty occurrences reactivates only the template.

Request body

{
  "expected_schedule_version": "0123456789abcdef0123456789abcdef",
  "occurrences": [
    {
      "instance_id": "00000000-0000-4000-8000-000000000004",
      "expected_version": "0123456789abcdef0123456789abcdef",
      "restore_booking_ids": []
    }
  ],
  "confirm_conflicts": false
}
GET/api/v1/admin/staff/{staffId}Bearer token

Read a venue staff profile

Requires staff.view and an operational or service-delivering membership in the selected venue. Returns contract_version: 1, basic identity, membership role/status/capabilities plus additive show_on_teacher_page and selectable_for_named_booking, and profile_owned_by_venue plus editable.identity/photo. Those two booleans are not writable on this identity PATCH; use GET|PATCH /admin/staff/{staffId}/public-booking-settings. Foreign-home collaborator email, phone, bio and specialties are omitted as null. No compensation, payroll, customer treatment records or new role grants. User JWT only; API keys and mixed credentials are rejected.

Parameters, scopes and examples

Required scopes

staff.view

Path parameters

staffIdstring · required
Staff profile UUID within the selected venue membership
PATCH/api/v1/admin/staff/{staffId}Bearer token

Update a venue staff profile

User JWT only; mixed credentials and API keys are rejected. Idempotency-Key is required and bound to the organization, actor, target and validated body. Concurrent/replayed requests do not repeat the mutation. No client notification is sent. Mutation errors add details.write_state: not_written, partial, saved or unknown. Only proven not_written releases the reservation; partial/saved/unknown retain it. Missing or malformed metadata on older responses is unknown. PROFILE_PARTIAL alone does not prove a save. Keep the same body/key for the same uncertain operation; explicitly reload and review authoritative state before choosing a new intent. A failed acknowledgement never replaces the known HTTP response. Requires staff.edit; protected capability changes additionally enforce settings.permissions, self/hierarchy and last-admin rules. Identity belongs to the home venue; collaborator role changes do not permit editing another venue’s personal profile. Returns the refreshed staff detail DTO. Role/capabilities use membership CAS, not new profiles.role values. Last-admin occupancy plus row CAS is not an organization-wide concurrency lock.

Parameters, scopes and examples

Required scopes

staff.edit

Path parameters

staffIdstring · required
Staff profile UUID within the selected venue membership

At least one of first_name, last_name, phone, nickname, bio, specialties and role is required. Empty updates are rejected before claiming the key. No organization, compensation, photo URL, show_on_teacher_page or selectable_for_named_booking fields.

Request body

{
  "first_name": "Alex",
  "specialties": [
    "Nail care"
  ]
}
POST/api/v1/admin/staff/{staffId}/photo/upload-urlBearer token

Prepare an owned staff photo upload

User JWT only; mixed credentials and API keys are rejected. Idempotency-Key is required and bound to the organization, actor, target and validated body. Concurrent/replayed requests do not repeat the mutation. No client notification is sent. Requires staff.edit and home-venue profile ownership. Returns upload_url, immutable path, content_type and max_bytes (5000000) for instructor-photos storage. Upload the original to the returned signed URL, then call finalize with the same path. This step does not change the profile photo.

Parameters, scopes and examples

Required scopes

staff.edit

Path parameters

staffIdstring · required
Staff profile UUID within the selected venue membership

Supported content_type: image/jpeg, image/png or image/webp.

Request body

{
  "content_type": "image/png"
}
POST/api/v1/admin/staff/{staffId}/photo/finalizeBearer token

Verify and save an owned staff photo

User JWT only; mixed credentials and API keys are rejected. Idempotency-Key is required and bound to the organization, actor, target and validated body. Concurrent/replayed requests do not repeat the mutation. No client notification is sent. Mutation errors add details.write_state: not_written, partial, saved or unknown. Only proven not_written releases the reservation; partial/saved/unknown retain it. Missing or malformed metadata on older responses is unknown. PROFILE_PARTIAL alone does not prove a save. Keep the same body/key for the same uncertain operation; explicitly reload and review authoritative state before choosing a new intent. A failed acknowledgement never replaces the known HTTP response. Requires staff.edit and home-venue profile ownership. Accepts the owned immutable upload path or explicit null to remove the photo, never an arbitrary URL. For uploads, the server checks object existence, size and JPEG/PNG/WebP magic bytes before an organization-scoped profile update. Returns photo_url, including null after removal; a missing, oversized or invalid original is rejected without a profile write. Web and JWT share persistence and cache/path invalidation.

Parameters, scopes and examples

Required scopes

staff.edit

Path parameters

staffIdstring · required
Staff profile UUID within the selected venue membership

Required path: the exact path returned by upload-url, or null for explicit removal. Omitted, empty and unknown fields are rejected.

Request body

{
  "path": "user/00000000-0000-4000-8000-000000000001/original-00000000-0000-4000-8000-000000000002.png"
}
GET/api/v1/admin/members/{id}/treatmentsBearer token

List a client’s treatment records

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Returns contract_version 1, scope ("own" | "all"), can_manage and records[] with appointment, service name, provider name, note, normalized formula, products_used, photo URLs, privacy/pin flags and timestamps. Optional limit (1–200, default 50).

Parameters, scopes and examples

Required scopes

treatments.view_own

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
POST/api/v1/admin/members/{id}/treatmentsBearer token

Record a treatment

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Send a UUID Idempotency-Key; a replay returns the stored response. Body: optional appointment_id (must belong to this client at this venue; an own-scope caller must have performed it, else 403 NOT_OWN_APPOINTMENT), note, optional formula (color formula schema), products_used[{name, quantity?, unit?, sku?}], is_private, is_pinned. Returns 201 with the staff record.

Parameters, scopes and examples

Required scopes

treatments.record_own

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
GET/api/v1/admin/members/{id}/treatments/{recordId}Bearer token

Read a treatment record

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Returns the staff record or 404 outside the caller’s scope.

Parameters, scopes and examples

Required scopes

treatments.view_own

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
recordIdstring · required
Treatment record UUID
PATCH/api/v1/admin/members/{id}/treatments/{recordId}Bearer token

Update a treatment record

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Send a UUID Idempotency-Key; a replay returns the stored response. Own-scope callers may change only records they authored (403 NOT_RECORD_AUTHOR); managers may change any. Same body fields as create, all optional.

Parameters, scopes and examples

Required scopes

treatments.record_own

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
recordIdstring · required
Treatment record UUID
DELETE/api/v1/admin/members/{id}/treatments/{recordId}Bearer token

Remove a treatment record

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Send a UUID Idempotency-Key; a replay returns the stored response. Requires treatments.manage; every other scope receives 403 FORBIDDEN. Audit-logged.

Parameters, scopes and examples

Required scopes

treatments.manage

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
recordIdstring · required
Treatment record UUID
GET/api/v1/admin/members/{id}/patch-testsBearer token

List a client’s patch tests

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Own scope returns tests the caller performed. Returns result, product, performed_at, expires_at and validity_hours.

Parameters, scopes and examples

Required scopes

treatments.view_own

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
POST/api/v1/admin/members/{id}/patch-testsBearer token

Record a patch test

Requires the member JWT and X-Organization-ID. treatments.view_own / treatments.record_own are the entry gates; the server then resolves the caller’s real scope: treatments.view_all or treatments.manage see every record, otherwise only records the caller authored or whose linked appointment the caller performed are returned or writable (others answer 404). Venue-wide reads are audit-logged. Formula data requires active color_formula_v1 consent for the client at this venue (409 CONSENT_REQUIRED / CONSENT_WITHDRAWN / CONSENT_RE_PROMPT_REQUIRED, 503 CONSENT_UNAVAILABLE when the check cannot be read). No payroll, compensation or other clients’ data is ever included. Send a UUID Idempotency-Key; a replay returns the stored response. Body: result (pass|fail|pending), product, notes, validity_hours (1–168, default 48), performed_at. Requires color_formula_v1 consent. Returns 201.

Parameters, scopes and examples

Required scopes

treatments.record_own

Path parameters

idstring · required
Client user UUID (active member of the selected venue)
GET/api/v1/admin/staff/{staffId}/public-booking-settingsBearer token

Read public listing and named-booking flags

User JWT and staff.edit required; API keys and mixed credentials are rejected. Returns contract_version:1, organization_id, staff_id, show_on_teacher_page and selectable_for_named_booking for the selected venue membership. The two booleans are independent. Missing/null named-booking coalesces to true; a present non-boolean fails closed. Not appointment-policies and not a raw membership dump. Public booking enforcement remains a required Stage B companion; this read does not change availability or checkout. This dedicated companion is explicitly silent: it never sends Email, SMS or Push and does not accept notification fields. Full Role & access saves keep their explicit channel picker and notifications.send-before-write rule.

Parameters, scopes and examples

Required scopes

staff.edit

Path parameters

staffIdstring · required
Staff profile UUID within the selected venue membership
PATCH/api/v1/admin/staff/{staffId}/public-booking-settingsBearer token

Update public listing and named-booking flags

User JWT and staff.edit required; API keys and mixed credentials are rejected. Strict body: at least one of show_on_teacher_page and selectable_for_named_booking as booleans. Idempotency-Key binds selected organization, actor, operation staff.membership_public_booking.patch, staff_id and the validated body. Known refusals add write_state:not_written and release the claim. Unconfirmed writes return HTTP 409 SAVE_OUTCOME_UNKNOWN with write_state:unknown and keep the claim. A known save may include refresh_failed:true. Keep the same key on unconfirmed outcomes and reload before a new intent. Does not write identity, role, compensation or appointment policies. Native Business authoring remains required before full parity. This dedicated companion is explicitly silent: it never sends Email, SMS or Push and does not accept notification fields. Full Role & access saves keep their explicit channel picker and notifications.send-before-write rule.

Parameters, scopes and examples

Required scopes

staff.edit

Path parameters

staffIdstring · required
Staff profile UUID within the selected venue membership

At least one boolean. No organization, actor, notification, identity or appointment-policy fields.

Request body

{
  "show_on_teacher_page": true,
  "selectable_for_named_booking": false
}
GET/api/v1/admin/ratesBearer or API key

Get floating-rate rules

Accepts exactly one credential: Bearer JWT with `rates.view`, or an API key bound to the venue with `read:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

read:passes

Query parameters

pass_type_idstring · required
Pass type id

Response example

{
  "data": {
    "pass_type_id": "uuid",
    "graduation_tiers": [],
    "seasonal_rates": []
  },
  "error": null
}
GET/api/v1/admin/rates/impactBearer or API key

Preview a floating-rate rule impact

Accepts exactly one credential: Bearer JWT with `rates.view`, or an API key bound to the venue with `read:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

read:passes

Query parameters

pass_type_idstring · required
Pass type id
rulestring · required
graduation or seasonal

Response example

{
  "data": {
    "affectedMembers": 4,
    "currentRevenue": 2000,
    "projectedRevenue": 2200
  },
  "error": null
}
GET/api/v1/admin/rates/distributionBearer or API key

Get revenue distribution by rate

Accepts exactly one credential: Bearer JWT with `rates.view`, or an API key bound to the venue with `read:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

read:passes

Query parameters

pass_type_idstring · required
Pass type id

Response example

{
  "data": {
    "totalMRR": 0,
    "byBucket": []
  },
  "error": null
}
GET/api/v1/admin/rates/upcomingBearer or API key

List upcoming rate changes

Accepts exactly one credential: Bearer JWT with `rates.view`, or an API key bound to the venue with `read:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

read:passes

Query parameters

days_aheadnumber
Window from 1 to 365 daysDefault: 30

Response example

{
  "data": {
    "days_ahead": 30,
    "changes": []
  },
  "error": null
}
GET/api/v1/admin/rates/previewBearer or API key

Preview a client rate

Accepts exactly one credential: Bearer JWT with `rates.view`, or an API key bound to the venue with `read:passes`. Dual credentials are rejected before authentication. The client and pass type must both belong to that venue.

Parameters, scopes and examples

Required scopes

read:passes

Query parameters

user_idstring · required
Client user id
pass_type_idstring · required
Pass type id
effective_datestring
Optional ISO timestamp

Response example

{
  "data": {
    "baseRate": 500,
    "resolvedRate": 450
  },
  "error": null
}
POST/api/v1/admin/rates/overrideBearer or API key

Set a pass rate override

Accepts exactly one credential: Bearer JWT with `rates.override`, or an API key bound to the venue with `write:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

write:passes

Rate override

Request body

{
  "pass_id": "uuid",
  "rate": 450,
  "reason": "Retention offer"
}

Response example

{
  "data": {
    "ok": true
  },
  "error": null
}
DELETE/api/v1/admin/rates/override/{passId}Bearer or API key

Clear a pass rate override

Accepts exactly one credential: Bearer JWT with `rates.override`, or an API key bound to the venue with `write:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

write:passes

Path parameters

passIdstring · required
Pass id

Response example

{
  "data": {
    "ok": true
  },
  "error": null
}
GET/api/v1/admin/private-eventsBearer or API key

List private-event bookings

Accepts exactly one credential: Bearer JWT with `private_events.view`, or an API key bound to the venue with `read:private_events`. Targets are non-enumerating and venue-scoped.

Parameters, scopes and examples

Required scopes

read:private_events

Query parameters

statusstring
Optional booking status

Response example

{
  "data": [
    {
      "id": "uuid",
      "status": "inquiry"
    }
  ],
  "error": null
}
POST/api/v1/admin/private-eventsBearer or API key

Create a private-event booking

Accepts exactly one credential: Bearer JWT with `private_events.manage`, or an API key bound to the venue with `write:private_events`. The canonical venue-scoped mutation workflow is used.

Parameters, scopes and examples

Required scopes

write:private_events

Private-event booking. Every PS-B2 field below is OPTIONAL and additive: client_id / save_as_client (link or create the client the session is for), partner_id + billing_target/billing_address/billing_vat_number/po_number/department/cost_center (bill a company — the billing block prefills from the partner record), brand_id, location_id, staff_note (a message the client sees), pricing_override ({mode: per_person|total, amount} — total is VAT-inclusive), and start_mode (confirmed | inquiry | confirm_on_payment) with payment_due_at. The legacy `status` field keeps working.

Request body

{
  "event_type_id": "uuid",
  "start_time": "2026-07-20T16:00:00.000Z",
  "participant_count": 12,
  "contact_name": "Client",
  "contact_email": "client@example.test",
  "start_mode": "confirm_on_payment",
  "billing_target": "partner",
  "partner_id": "uuid",
  "pricing_override": {
    "mode": "per_person",
    "amount": 250
  }
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "quoted",
    "client_id": null,
    "client_created": false,
    "payment_due_at": "2026-07-19T16:00:00.000Z"
  },
  "error": null
}
GET/api/v1/admin/private-events/{id}Bearer or API key

Get a private-event booking

Accepts exactly one credential: Bearer JWT with `private_events.view`, or an API key bound to the venue with `read:private_events`. Targets are non-enumerating and venue-scoped.

Parameters, scopes and examples

Required scopes

read:private_events

Path parameters

idstring · required
Booking id

Response example

{
  "data": {
    "id": "uuid",
    "status": "inquiry"
  },
  "error": null
}
PATCH/api/v1/admin/private-events/{id}Bearer or API key

Update a private-event booking

Accepts exactly one credential: Bearer JWT with `private_events.manage`, or an API key bound to the venue with `write:private_events`. The canonical venue-scoped mutation workflow is used.

Parameters, scopes and examples

Required scopes

write:private_events

Path parameters

idstring · required
Booking id

Fields to update. PS-B2 adds the same optional fields the create route takes (client_id, partner_id + billing block, brand_id, location_id, staff_note, pricing_override, payment_due_at) plus `reprice` (recompute the frozen subtotal/VAT/total/deposit) and `notify_client` ({enabled, channels}). The response carries the client-visible change summary.

Request body

{
  "participant_count": 14,
  "reprice": true
}

Response example

{
  "data": {
    "id": "uuid",
    "updated": [
      "participant_count",
      "total_amount"
    ],
    "changeSummary": {
      "changes": [
        {
          "field": "participant_count",
          "label": "Participants",
          "from": "12",
          "to": "14"
        }
      ],
      "clientVisible": true
    },
    "notifyIntent": {
      "requested": false,
      "channels": [
        "email"
      ],
      "willSend": false,
      "reason": "staff_opted_out"
    }
  },
  "error": null
}
POST/api/v1/admin/private-events/{id}/approveBearer or API key

Approve a private-event booking

Accepts exactly one credential: Bearer JWT with `private_events.manage`, or an API key bound to the venue with `write:private_events`. The canonical venue-scoped mutation workflow is used.

Parameters, scopes and examples

Required scopes

write:private_events

Path parameters

idstring · required
Booking id

Response example

{
  "data": {
    "id": "uuid",
    "status": "confirmed",
    "invoice_id": null
  },
  "error": null
}
POST/api/v1/admin/private-events/{id}/cancelBearer or API key

Cancel a private-event booking

Accepts exactly one credential: Bearer JWT with `private_events.manage`, or an API key bound to the venue with `write:private_events`. The canonical venue-scoped mutation workflow is used.

Parameters, scopes and examples

Required scopes

write:private_events

Path parameters

idstring · required
Booking id

Cancellation reason

Request body

{
  "reason": "Client request"
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "cancelled",
    "fee": 250,
    "paidToDate": 500,
    "suggestedRefund": 250
  },
  "error": null
}
POST/api/v1/admin/private-events/{id}/quoteBearer or API key

Send a private-event quote

Accepts exactly one credential: Bearer JWT with `private_events.manage`, or an API key bound to the venue with `write:private_events`. The canonical venue-scoped mutation workflow is used.

Parameters, scopes and examples

Required scopes

write:private_events

Path parameters

idstring · required
Booking id

Quote values

Request body

{
  "total_amount": 2500,
  "valid_days": 7
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "quoted"
  },
  "error": null
}
GET/api/v1/admin/loyalty-priceBearer or API key

Get the venue loyalty programme snapshot

Accepts exactly one credential: Bearer JWT with `loyalty_price.manage`, or an API key bound to the venue with `read:passes`. Dual credentials are rejected before authentication.

Parameters, scopes and examples

Required scopes

read:passes

Response example

{
  "data": {
    "enabled": true,
    "display_name": "Loyalty Price",
    "settings": {
      "program": {},
      "visibility": {},
      "recurring": {},
      "packs": {}
    },
    "status_counts": {
      "none": 0,
      "qualifying": 12,
      "active": 48,
      "grace": 3,
      "lapsed": 7
    },
    "loyal_clients": 51
  },
  "error": null
}
GET/api/v1/admin/loyalty-price/members/{memberId}Bearer or API key

Get a client loyalty-price context for a grant-capable operator

Accepts exactly one credential: Bearer JWT with `loyalty_price.grant`, or an API key bound to the venue with `write:passes`. Returns only the named active member’s programme label and standing; venue-wide configuration and aggregate counts remain manage-only.

Parameters, scopes and examples

Required scopes

write:passes

Response example

{
  "data": {
    "enabled": true,
    "display_name": "Loyalty Price",
    "status": "active",
    "status_source": "manual_grant",
    "loyal_since": "2026-08-18",
    "grace_until": null,
    "window_closes_on": null,
    "manual_grant_active": true
  },
  "error": null
}
POST/api/v1/admin/loyalty-price/grantBearer or API key

Give a client the loyalty price

Accepts exactly one credential: Bearer JWT with `loyalty_price.grant`, or an API key bound to the venue with `write:passes`. Requires a UUID `Idempotency-Key`; a replay is bound to the same venue, caller and grant payload. Writes through the context-free grant core (never a cookie action), so the audit trail and status recompute are identical to the admin web surface.

Parameters, scopes and examples

Required scopes

write:passes

user_id + a short reason. Optional venue-local expiry, and an optional pass_id with an agreed price on the catalog (MAJOR) scale — pass_price_override requires pass_id.

Request body

{
  "user_id": "uuid",
  "reason": "Agreed with the owner at the desk",
  "expires_on": "2027-01-31",
  "pass_id": "uuid",
  "pass_price_override": 249
}

Response example

{
  "data": {
    "granted": true,
    "source_id": "uuid",
    "status": "active",
    "status_source": "manual_grant",
    "loyal_since": "2026-08-18",
    "grace_until": null,
    "window_closes_on": null
  },
  "error": null
}
GET/api/v1/admin/members/{id}/default-passBearer token

A client's default pass

Staff with members.view_insights (the same read permission as the client's pass history; changing it needs passes.manage). The client's default pass at the caller's venue and every pass that could be chosen (active, past_due, pending_activation, paused) with is_default, usable and unusable_reason (clips_exhausted | expired | paused | past_due | not_eligible | not_found) for the venue-local today; a renewing membership whose current cycle is spent stays usable (it refills). 404 NOT_FOUND when the client is not an active member of the caller's venue.

Parameters, scopes and examples

Path parameters

idstring · required
Client id (UUID or public client id)

Response example

{
  "data": {
    "member_id": "uuid",
    "default_pass_id": "uuid",
    "passes": [
      {
        "id": "uuid",
        "name": "Ten Class Card",
        "status": "active",
        "clips_remaining": 4,
        "end_date": "2026-12-31",
        "is_default": true,
        "usable": true,
        "unusable_reason": null
      }
    ]
  }
}
PUT/api/v1/admin/members/{id}/default-passBearer token

Set a client's default pass

Staff with passes.manage. Sets or clears (pass_id null) which pass the client's bookings at the caller's venue use first. The client must be an active member of the caller's venue (404 NOT_FOUND) and the pass must be that client's pass at the same venue (404 PASS_NOT_FOUND, including another venue's pass); non-selectable passes are 422 PASS_NOT_SELECTABLE. Audited as member.default_pass_set with the staff actor. The client is not notified.

Parameters, scopes and examples

Path parameters

idstring · required
Client id (UUID or public client id)

Default pass

Request body

{
  "pass_id": "uuid"
}

Response example

{
  "data": {
    "organization_id": "uuid",
    "member_id": "uuid",
    "default_pass_id": "uuid",
    "previous_default_pass_id": null
  }
}
Auth2 documented operations
POST/api/v1/auth/staff-report-exchangePublic

Exchange a bound staff reporting handoff

Server-to-server exchange of a single-use short-lived handoff bound to browser nonce, state, destination, brand, venue and approved origin. Returns a read-only staff report token for httpOnly cookie storage. Existing BookingBible business login authorizes the handoff; no new password or email allowlist.

Parameters, scopes and examples

Exact handoff fields from the authorized business redirect; nonce remains in the site httpOnly cookie.

Request body

{
  "code": "<single-use-code>",
  "nonce": "<browser-nonce>",
  "state": "<state>",
  "organizationId": "<venue-uuid>",
  "brandId": "<brand-uuid>",
  "origin": "https://namasteonline.com",
  "destination": "/admin"
}
POST/api/v1/auth/staff-report-logoutBearer token

Revoke delegated reporting session

Consumes the hashed current reporting token; does not sign out the member or other business sessions. Token has no non-report authority.

Profile82 documented operations
GET/api/v1/me/live-audio-preferencesBearer token

Get brand live audio preferences

Confirmed pilot member only; account-synced teacher/music volume and optional music diagnostics consent.

Parameters, scopes and examples

Query parameters

brand_idstring · required
Brand UUID
PATCH/api/v1/me/live-audio-preferencesBearer token

Save brand live audio preferences

Confirmed pilot member only. Consent withdrawal deletes their music events for this brand.

Parameters, scopes and examples

Brand UUID, two 0–1 volumes and diagnostics consent.

Request body

{
  "brand_id": "00000000-0000-4000-8000-000000000001",
  "teacher_volume": 1,
  "music_volume": 0.3,
  "share_music_diagnostics": false
}
GET/api/v1/me/referralsBearer token

My referral status

The caller's referral status for their active org: code, referred-friend count, conversions, rewards earned, and an anonymized (first-name + last-initial) per-referral list. Returns an empty summary when the `referrals` module is disabled.

Parameters, scopes and examples

Response example

{
  "data": {
    "referral_code": "JANE-4821",
    "total": 3,
    "completed": 1,
    "pending": 2,
    "expired": 0,
    "rewards_earned": 100,
    "ongoing_discount_active": true,
    "reward": {
      "enabled": true,
      "reward_type": "points",
      "reward_value": 50,
      "referred_ongoing_discount_percent": 20
    },
    "referred": [
      {
        "name": "Sam P.",
        "status": "completed",
        "created_at": "2026-05-01T00:00:00Z",
        "completed_at": "2026-05-08T00:00:00Z"
      }
    ]
  }
}
GET/api/v1/me/outstanding-paymentsBearer token

List my outstanding payments

Everything the caller currently owes in one read: renewing memberships in past_due/suspended with a failed renewal charge, overdue client invoices with a balance at venues that take card payments online (including the remaining balance of a partially paid invoice past its due date), and declined one-off charges. Each item carries its frozen ISO currency, amount (major) and amount_minor, a plain-language failure_message, a state_key for "dismiss until it changes", and the action to take (pay_renewal, retry_invoice or update_payment_method). A membership_renewal item also carries cards_on_file: the member's saved cards on the Stripe account the membership bills through (the only cards pay_renewal can charge), with is_subscription_card marking the card the membership charges today and is_default the customer's default; omitted when the cards could not be read. Totals are per currency. Read-only; never charges. Optional X-Organization-Slug scopes the call to one venue (branded apps, brand sites); an unknown slug returns nothing and never another venue’s rows.

Parameters, scopes and examples

Response example

{
  "data": {
    "count": 1,
    "totals": [
      {
        "currency": "EUR",
        "amount": 59,
        "amount_minor": 5900,
        "count": 1
      }
    ],
    "items": [
      {
        "kind": "membership_renewal",
        "id": "uuid",
        "payment_id": "11111111-2222-4333-8444-555555555555",
        "state_key": "membership_renewal:uuid:suspended:uuid:5900",
        "organization_id": "uuid",
        "title": "Monthly Unlimited",
        "status": "suspended",
        "amount": 59,
        "amount_minor": 5900,
        "currency": "EUR",
        "failed_at": "2026-09-10T08:00:00Z",
        "due_date": "2026-09-10",
        "days_overdue": 13,
        "failure_code": "card_declined",
        "failure_message": "The card was declined by the bank.",
        "decline_code": "card_declined",
        "next_step": "update_card",
        "invoice_number": null,
        "late_fee_amount": null,
        "next_billing_date": "2026-10-10",
        "action": {
          "type": "pay_renewal",
          "method": "POST",
          "path": "/api/v1/me/passes/uuid/pay-renewal",
          "confirm_path": "/api/v1/me/passes/uuid/pay-renewal/confirm",
          "accepts_payment_method_id": true,
          "payment_methods_path": "/api/v1/me/payment-methods"
        },
        "cards_on_file": [
          {
            "id": "pm_1AbCdEfGhIjKlMnOpQrStUv",
            "brand": "visa",
            "last4": "6073",
            "exp_month": 3,
            "exp_year": 2027,
            "is_default": false,
            "is_subscription_card": true
          },
          {
            "id": "pm_1XyZaBcDeFgHiJkLmNoPqRs",
            "brand": "visa",
            "last4": "6787",
            "exp_month": 11,
            "exp_year": 2029,
            "is_default": true,
            "is_subscription_card": false
          }
        ]
      }
    ]
  }
}
GET/api/v1/me/failed-paymentsBearer token

List my failed payments

The caller’s ten most recent declined charges (failure_code present), newest first, with description for banner copy. Prefer GET /api/v1/me/outstanding-payments, which de-duplicates renewals and names the pay action. Optional X-Organization-Slug scopes the call to one venue (branded apps, brand sites); an unknown slug returns nothing and never another venue’s rows.

POST/api/v1/me/invoices/{id}/retryBearer token

Pay my invoice on my saved card

Charges the saved default card for the balance still owed (total − amount_paid) on the caller’s sent, viewed, overdue or partially paid invoice (one charge per invoice at a time); a partially paid invoice is settled by paying the remaining balance and becomes paid. Optional body { expected_amount_minor }: the balance the client showed, in the smallest currency unit (use outstanding_balance_minor from GET /api/v1/me/invoices). The invoice is read again right before charging; if the balance no longer equals that quote the call returns 409 BALANCE_CHANGED and charges nothing — refetch and show the new amount. Returns status succeeded; processing (bank still settling — do not pay again); requires_action with client_secret and stripe_account for Stripe 3-D Secure (then call …/retry/confirm); failed with failure_code/decline_code; or paid with already_settled when an earlier payment already covered it. Settlement records the invoice payment like a Payment Link payment and deactivates the invoice's Payment Link. An open 3-D Secure attempt never outlives a balance change: any payment write on the invoice cancels open retry PaymentIntents (unless still for exactly the balance owed), and a challenge whose balance changed while it was being created is cancelled instead of returned (409 BALANCE_CHANGED). Staff payment writes share the per-invoice lock, so a charge started during one answers 409 PAYMENT_PENDING. A charge received but not yet applied returns status processing with applied: false (never paid; do not pay again), plus review_required: true when the venue must apply or refund it. Every refusal carries error.details.next_step (update_card | refresh | contact_venue | null) and a failed charge carries data.next_step — show only that action: update_card only for a real card decline, null for a processor hiccup (processing_error, try_again_later) or a failure without a reason. 422 ALREADY_PAID, NOT_RETRYABLE or NO_PAYMENT_METHOD (no usable saved card); 422 CARD_PAYMENTS_UNAVAILABLE when the invoice's venue cannot take card payments online (no charge is attempted; can_pay_now is false for such invoices); 409 PAYMENT_PENDING (an earlier payment is processing or another is in progress), BALANCE_CHANGED or RECONCILIATION_REQUIRED (an earlier payment no longer matches the balance; the venue is alerted); 400 VALIDATION_ERROR for a malformed body; 404 for an unknown, foreign or malformed id. Owner scoped, 5/min, audited. Optional X-Organization-Slug scopes the call to one venue (branded apps, brand sites); an unknown slug returns nothing and never another venue’s rows.

Parameters, scopes and examples

Path parameters

idstring · required
Invoice id

Optional. expected_amount_minor = the balance the client showed (outstanding_balance_minor); omit the body to charge the balance read when the request starts.

Request body

{
  "expected_amount_minor": 70000
}
POST/api/v1/me/invoices/{id}/retry/confirmBearer token

Confirm my invoice payment after 3-D Secure

Body { payment_intent_id }. Verifies first that the PaymentIntent belongs to this invoice, venue and member (retrieved on the invoice venue's own accounts), then reports: paid (applied like a Payment Link payment), pending (only while Stripe is processing it; do not pay again) or failed — nothing was charged — with reason not_started (the bank check never ran, e.g. Stripe.js did not load; next_step null, the client may pay again), not_completed (the check ran and failed; failure_code is the processor's reason and next_step is update_card only for a real card decline) or cancelled (the platform stopped it because the amount owed changed; next_step refresh). Call it after the 3-D Secure step whatever the SDK returned. 422 PAYMENT_MISMATCH (not this invoice or not equal to the balance) or NOT_RETRYABLE; 409 RECONCILIATION_REQUIRED; 400 VALIDATION_ERROR; 502 RETRY_FAILED. Owner scoped, 5/min. Optional X-Organization-Slug scopes the call to one venue (branded apps, brand sites); an unknown slug returns nothing and never another venue’s rows.

Parameters, scopes and examples

Path parameters

idstring · required
Invoice id

The PaymentIntent id returned by POST /api/v1/me/invoices/{id}/retry.

Request body

{
  "payment_intent_id": "pi_123"
}
GET/api/v1/me/venue-affinitiesBearer token

My venue affinities

Active public venues ranked by explicit favourite, then canonical pass, class-booking, and appointment signals. Returns counts and last activity; caller identity is server-bound.

POST/api/v1/me/passes/{id}/resume-checkoutBearer token

Resume my existing unpaid membership checkout

Member-JWT, tenant-scoped same-attempt recovery for one already-created recurring membership. Requires the owned pending pass, its immutable checkout operation and disclosure, an exact matching incomplete Stripe subscription, an open unpaid first invoice, and a still-resumable existing PaymentIntent. The route returns the existing client_secret only; it never accepts price, selection, promotion, quote, or checkout attempt input, and never creates, cancels, reprices, or re-evaluates a sale. Paid, terminal, ambiguous, mismatched, or concurrently changed operations fail closed with 409. Confirm the returned existing PaymentIntent through POST /me/checkout/confirm, which answers a lapsed reservation with 409 OFFER_CONTACT_VENUE at the settle step (never OFFER_HOLD_EXPIRED) whenever fulfilment was refused, including while its refund is still pending or needs a manual touch — a lapsed reservation is terminal there regardless of where the refund itself has gotten to. This endpoint RENEWS the reservation on every call (rate-limited to 15/user) — it is not a passive re-read or a safe poll. Call it only when the buyer explicitly resumes or restarts a checkout, never on a timer to "keep the countdown fresh": each call extends the hold by another 3 minutes, so polling it would hold a place indefinitely. The response carries hold_expires_at (ISO 8601 UTC instant the renewed reservation lapses, or null when no offer hold applies) and server_time (ISO 8601 UTC, the server clock at response time), always together, alongside the still-valid client_secret. Compute the countdown once as hold_expires_at minus server_time and run it locally, never against the device clock. A renewal that fails because the reservation already lapsed and a restart could still succeed fails closed with 409 OFFER_HOLD_EXPIRED; one where restarting cannot work (capacity gone, the per-client limit reached, or a late charge already refunded) fails closed with 409 OFFER_CONTACT_VENUE, which never offers a restart and instead points the buyer to the venue desk. OFFER_SOLD_OUT does not apply here: this endpoint only ever renews a reservation that already existed, never opens a first one.

Parameters, scopes and examples

Path parameters

idstring · required
Owned pending pass UUID

Response example

{
  "data": {
    "pass_id": "uuid",
    "payment_intent_id": "pi_existing",
    "client_secret": "pi_existing_secret_x",
    "stripe_account_id": null,
    "payment_status": "requires_payment_method",
    "hold_expires_at": "2026-09-15T09:15:34.000Z",
    "server_time": "2026-09-15T09:12:34.000Z",
    "frozen_checkout": {
      "charged_today_minor": 9900,
      "currency": "DKK",
      "flexible_selection": {
        "kind": "quantity",
        "quantity": 5,
        "class_allowance": 5,
        "allowance_label": "5 classes / month"
      },
      "purchase_obligation": {
        "status": "available",
        "intro_through_date": "YYYY-MM-DD",
        "minimum_total_payable": 67400,
        "cancellation": {
          "kind": "conditional_contractual_minimum",
          "condition": "timely_valid_notice",
          "request_method": "contact_studio",
          "earliest_possible_end_on": "YYYY-MM-DD"
        },
        "commitment": {
          "currency": "DKK",
          "first_full_price_date": "YYYY-MM-DD",
          "full_price_amount_minor": 57500,
          "minimum_full_price_cycles": 1,
          "commitment_ends_on": "YYYY-MM-DD",
          "intro_through_date": "YYYY-MM-DD"
        }
      }
    }
  }
}
POST/api/v1/me/favorite-venues/{organizationId}Bearer token

Favourite a venue

Idempotently stars one active public venue for the authenticated member.

Parameters, scopes and examples

Path parameters

organizationIdstring · required
Venue organization UUID
DELETE/api/v1/me/favorite-venues/{organizationId}Bearer token

Unfavourite a venue

Idempotently removes only the authenticated member and requested venue pair.

Parameters, scopes and examples

Path parameters

organizationIdstring · required
Venue organization UUID
GET/api/v1/me/credits/balancesBearer token

My venue credit balances

Complete ledger-derived balances grouped by venue and currency. Each row carries `balance` (the venue ledger total), `available` (that total minus credit held by open checkout reservations, which is what checkout will actually spend) and `reserved`. Amounts are in major units. Consumer is account-wide; branded requests are fail-closed to x-organization-slug.

POST/api/v1/me/avatar/upload-urlBearer token

Create avatar upload ticket

Returns a caller-owned, MIME-bound storage path and two-hour signed upload URL for PNG, JPEG, or WebP up to 5 MB.

PATCH/api/v1/me/avatarBearer token

Finalize my avatar

Validates caller path ownership, metadata, size, and image magic bytes before deriving and saving the public URL.

DELETE/api/v1/me/avatarBearer token

Remove my avatar

Idempotently clears the profile reference and removes only the caller-owned canonical avatar object.

GET/api/v1/me/workspace-profileBearer token

My active venue operating profile

Server-authoritative Business-app profile for the active venue selected by X-Organization-ID. Returns booking_mode (classes, appointments, or both), business_type, resolved class/appointment operation gates, venue surface applicability, appointment access/counts, active_modules, and additive operations.appointments.creation_enabled (verified active basic/full staff creation) and historical_creation_enabled (also history permission plus the protected retrocreate executor). Missing creation flags are false; history_enabled preserves existing-row reads after downgrade/archive, separately from management_enabled and new creation. Intersect history with bookings.manage. reschedule_enabled is independently true only for verified active/wind-down fulfilment, including hidden verticals; missing is false. the resolved vertical_modules visibility map. business_type is informational and never used to infer booking_mode. Surface values are venue-level applicability; clients must still intersect them with the caller's effective permissions from GET /api/v1/me.

Parameters, scopes and examples

Response example

{
  "data": {
    "contract_version": 1,
    "organization_id": "00000000-0000-0000-0000-000000000aaa",
    "booking_mode": "appointments",
    "business_type": "nail_salon",
    "operations": {
      "classes": {
        "enabled": false,
        "visibility": "hide"
      },
      "appointments": {
        "enabled": true,
        "management_enabled": true,
        "history_enabled": true,
        "reschedule_enabled": true,
        "creation_enabled": true,
        "historical_creation_enabled": false,
        "visibility": "default_on",
        "access_level": "full",
        "service_count": 8,
        "provider_count": 4,
        "plan_full_access": true
      }
    },
    "venue_surfaces": {
      "dashboard": true,
      "schedule": false,
      "check_in": false,
      "appointments": true,
      "clients": true,
      "point_of_sale": true
    },
    "active_modules": [
      "products"
    ],
    "vertical_modules": {
      "classes": "hide",
      "appointments": "default_on",
      "pos": "default_on"
    }
  }
}
GET/api/v1/meBearer token

My profile

Current user profile with all active venue memberships and roles. Each membership carries `permissions: string[]` (the caller's OWN effective permission keys for that org — per-user overrides applied over role/capability defaults, resolved identically to requireApiPermissionWithDefaults) and `capabilities: string[]` (the membership capability set, surfaced for every membership). To bound per-request cost in this multi-tenant app, `permissions` is resolved for the ACTIVE org only (top-level `permissions_scope: "active_org"`; non-active memberships carry `[]`) — mobile refetches /me on org switch. Workspace ownership is server-projected as `is_individual`, `is_owned`, `is_workplace`, `is_relationship`, `is_selectable`, and an explicit `workspace_group` (`owned`, `works_at`, `member_venues`, or `relationships`). Accepted role-bearing employer memberships remain selectable in Business under “Works at”; member-only Network relationships do not. Business clients must only put selectable rows in their workspace picker. Gates UI on these instead of discovering denials via 403s. A PATCH /admin/permissions/user/{userId} is reflected within ≤60s (permission-cache TTL). Caller's own permissions only. See docs/api/ME_PERMISSIONS_CONTRACT.md.

POST/api/v1/me/active-organizationBearer token

Switch my active workspace

Authoritatively switches the caller to an active, selectable workspace. When the caller owns an individual professional venue, accepted role-bearing employer memberships remain selectable; only non-operational/member-only relationships return WORKSPACE_NOT_SELECTABLE. The response includes effective permissions for the selected workspace so native role gating is safe immediately.

Parameters, scopes and examples

Workspace to make active

Request body

{
  "organization_id": "00000000-0000-0000-0000-000000000aaa"
}

Response example

{
  "data": {
    "organization_id": "00000000-0000-0000-0000-000000000aaa",
    "role": "admin",
    "permissions": [
      "network.view",
      "network.manage"
    ]
  }
}
GET/api/v1/me/featuresBearer token

My feature toggles

Resolved feature-module map for the caller's active org (C07): `{ <module_key>: { enabled, source, tier?, settings? } }` — the same four-tier resolution (plan → group → venue → tenant) the admin sees at /admin/features. Also includes `professional_collaborations`, which reflects the platform-wide teacher-settlements rollout independently of the venue-to-venue `network` plan gate. Drives every <FeatureGate> in the branded mobile app. Multi-membership callers must send X-Organization-ID; without it the map resolves empty (all off).

GET/api/v1/me/minimalPublic

Minimal auth check

Cross-origin auth check for venue marketing sites. Returns { logged_in, first_name, venue_id, preferred_brand_id } — or logged_in=false when no session. CORS is gated by the venue/brand embed_allowed_origins allowlist; unknown origins get no CORS headers (treated as "not logged in" by the caller).

PATCH/api/v1/meBearer token

Update profile

Update profile fields. Cannot modify role, balance, or org membership.

Parameters, scopes and examples

Profile fields

Request body

{
  "first_name": "Jane",
  "phone": "+4512345678",
  "marketing_consent": true
}
GET/api/v1/me/payment-methodsBearer token

List payment methods

Saved cards and payment methods from Stripe.

POST/api/v1/me/payment-methodsBearer token

Add payment method

Create an account-local Stripe SetupIntent plus matching Customer/ephemeral-key credentials. Requires an Idempotency-Key header. Optional expected_renewals:[{pass_id,payment_id}] freezes the complete original renewal set after member, venue, payment and billing-account validation; omitted retains legacy behavior, [] pins only. The normalized target list is bound to the key and durable SetupIntent metadata, so a later debt cannot be substituted. An unknown outcome requires checking the same operation. The response freezes the server-owned venue country and exact Connect account for native Payment Sheet initialization.

Parameters, scopes and examples

Response example

{
  "data": {
    "client_secret": "seti_xxx_secret_xxx",
    "setup_intent_id": "seti_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  }
}
DELETE/api/v1/me/payment-methods/{id}Bearer token

Payment method removal is prohibited

Member self-service cannot detach saved cards. This endpoint returns PAYMENT_METHOD_REMOVAL_NOT_ALLOWED; add a replacement card or contact venue staff instead.

Parameters, scopes and examples

Path parameters

idstring · required
Stripe payment method ID
PATCH/api/v1/me/payment-methods/{id}/defaultBearer token

Set default payment method

Promote a saved card to the Stripe customer default (invoice_settings.default_payment_method; default_source for a legacy card_ source). Optional expected_renewals:[{pass_id,payment_id}] is the complete frozen debt target set; omitted retains legacy fanout, [] pins without collection. An explicit list requires Idempotency-Key. Targets must belong to the actor, venue, pass and original billing account before a provider update. The key binds the normalized list as well as caller, venue and card; a changed list returns 409 IDEMPOTENCY_KEY_REUSE_MISMATCH. An explicit unknown/foreign venue slug or stale active-venue pointer is 404 ORG_SCOPE_INVALID; a scope read failure is 503 ORG_SCOPE_UNAVAILABLE. A profile row with an explicit null active-venue pointer permits a platform-wallet default with no automatic membership collection; a missing or malformed profile is unavailable. A provider-ambiguous or post-provider failure is 503 DEFAULT_CARD_OUTCOME_UNKNOWN: read the current default and outstanding payments; the same key stays pending while its outcome is unknown. A proven no-effect failure releases the key for retry. GET /me/payment-methods then returns is_default:true on the matching row (PAY-P3.1). The response lists memberships[] with pin and original renewal outcomes, including payment_id when collected. A requires_action renewal can include client_secret and stripe_account for the existing attempt; complete that challenge then confirm settlement without another collection request. Contract: docs/api/MEMBER_SELF_PAY.md §2d.

Parameters, scopes and examples

Path parameters

idstring · required
Stripe payment method ID

Response example

{
  "data": {
    "memberships": [
      {
        "pass_id": "uuid",
        "pinned": true,
        "renewal": {
          "outcome": "paid"
        }
      },
      {
        "pass_id": "uuid",
        "pinned": true,
        "renewal": null
      }
    ]
  }
}
POST/api/v1/me/payment-sheet-initBearer token

Initialise Payment Sheet

Setup-only flow for Stripe Payment Sheet (PAY-P1.1). Requires an Idempotency-Key header. Returns customer_id, ephemeral_key, setup_intent_client_secret, and apple_merchant_id in the exact SetupIntent home account: connected only in direct mode, otherwise platform. Use when collecting a saved card before any purchase.

Parameters, scopes and examples

Venue context

Request body

{
  "organization_id": "uuid"
}

Response example

{
  "data": {
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx",
    "setup_intent_client_secret": "seti_xxx_secret_xxx",
    "apple_merchant_id": "merchant.com.bookingbible",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  }
}
POST/api/v1/me/push-tokenBearer token

Register push token

Register an Expo push notification token for iOS/Android/web. app_variant is required so member, branded-venue, and staff deliveries cannot cross application boundaries. Branded tokens require an explicit venue the member belongs to (X-Organization-Slug, or a member-validated X-Organization-ID; a conflicting pair is refused) and never use the profile's active venue, otherwise 400 ORG_REQUIRED. Business tokens require a validated organization context. A re-registered branded or business token is moved to a different venue only when that venue is named explicitly.

Parameters, scopes and examples

Push token

Request body

{
  "token": "ExponentPushToken[xxx]",
  "platform": "ios",
  "device_id": "installation-uuid",
  "app_variant": "branded"
}
DELETE/api/v1/me/push-tokenBearer token

Deregister push token

Deactivate the authenticated user's token or device before logout. The token/device selector is sent in the JSON body.

Parameters, scopes and examples

At least one token or device_id is required

Request body

{
  "device_id": "installation-uuid"
}
GET/api/v1/me/notificationsBearer token

Notification history

Cursor/page-paginated email, SMS, push, and in-app history. Rows include source-aware `data`, `read_at`, and `app_variant`; X-App-Variant filters app-specific inbox events, while X-Organization-Slug narrows branded clients to their venue.

POST/api/v1/me/notifications/{id}/readBearer token

Mark one notification read

Self-scoped read marker. Idempotency-Key is required; another user’s row returns 404.

Parameters, scopes and examples

Path parameters

idstring · required
Notification id
POST/api/v1/me/notifications/read-allBearer token

Mark notifications read

Marks all of the caller’s unread rows read. X-Organization-Slug narrows a branded client to its exact venue; otherwise Consumer marks its cross-venue inbox. Idempotency-Key is required.

GET/api/v1/me/notification-preferencesBearer token

Notification preferences

Returns the canonical ten-category catalog with effective email/SMS/push defaults, frequency caps, and per-member quiet hours for the active/requested organization.

PATCH/api/v1/me/notification-preferencesBearer token

Update notification preferences

Upserts canonical category toggles/frequency caps and quiet hours. Unknown categories are rejected and every database failure is returned; Idempotency-Key is required.

GET/api/v1/me/paymentsBearer token

List my payments

Cursor-paginated receipt-bearing payment ledger for the caller. Pending and failed attempts are excluded; successful, refunded, partially-refunded and disputed originals remain available with their payment receipt. An App Store purchase is shown at the price the member paid Apple, with `receipt_source: "apple"` (Apple issues that receipt).

Parameters, scopes and examples

Query parameters

limitnumber
Page size (default 20, max 100)
afterstring
Opaque cursor from a previous page
GET/api/v1/me/receipts/{paymentId}/pdfBearer token

Receipt PDF

Streams the receipt PDF (application/pdf) for one of the caller's payments — branded merchant header, line items, VAT breakdown, totals. Cached in storage after first render. App Store purchases return 409 APP_STORE_RECEIPT: Apple issues their receipt.

Parameters, scopes and examples

Path parameters

paymentIdstring · required
Payment id
POST/api/v1/me/receipts/{paymentId}/resendBearer token

Email me this receipt

Emails the venue-branded receipt PDF for one of the caller's own payments to the address already on file for their account — the same document served by the PDF download. No recipient field exists; any caller-supplied recipient is ignored. Idempotency-Key is required; a retried key replays the cached result instead of re-sending. App Store purchases return 409 APP_STORE_RECEIPT.

Parameters, scopes and examples

Path parameters

paymentIdstring · required
Payment id

Empty body — the request is never read.

Request body

{}
GET/api/v1/me/refundsBearer token

List member refund receipts

Owner-scoped successful refund operations, cursor-paginated and optionally restricted by the branded organization slug. Split-tender operations are returned once with a signed negative amount. Apple’s refunds of App Store purchases show the member’s own price, `receipt_source: "apple"` and no `receiptPath`.

Parameters, scopes and examples

Query parameters

limitnumber
Page size (default 20, max 100)
afterstring
Opaque cursor from a previous page
GET/api/v1/me/refunds/{refundId}/pdfBearer token

Refund receipt PDF

Owner- and venue-scoped canonical refund receipt PDF. Non-final, sibling, cross-member and cross-venue refund ids return a uniform not-found response. Apple’s refunds of App Store purchases return 409 APP_STORE_RECEIPT.

Parameters, scopes and examples

Path parameters

refundIdstring · required
Refund id
GET/api/v1/me/loyaltyBearer token

Loyalty balance + history

The caller's org-scoped loyalty point balance plus a recent per-event history slice. Full paginated history is on /api/v1/me/loyalty/points.

GET/api/v1/me/loyalty/pointsBearer token

Loyalty points history

Cursor-paginated per-event loyalty point ledger for the caller.

GET/api/v1/me/streakBearer token

Attendance streak

Current + longest attendance streak, freezes remaining, and at-risk flag.

GET/api/v1/me/rewardsBearer token

Redeemable rewards catalog

Active loyalty rewards for the caller's org with affordability (is_locked) computed against the caller's balance.

POST/api/v1/me/rewards/redeemBearer token

Redeem a reward

Redeem a loyalty reward. Idempotency-Key supported; audited.

Parameters, scopes and examples

Redemption

Request body

{
  "reward_id": "uuid"
}
POST/api/v1/feedbackBearer token

Submit feedback & tip

Rate a class (1-5 stars), leave a comment (optionally `anonymous`), and optionally tip the instructor via Stripe. The tip carries its own `anonymous` flag. The tip block of the response returns `client_secret`, `customer_id`, `ephemeral_key`, and `stripe_account_id` (non-null only in DIRECT charge mode).

Parameters, scopes and examples

Feedback

Request body

{
  "class_instance_id": "uuid",
  "booking_id": "uuid",
  "rating": 5,
  "comment": "Amazing class!",
  "anonymous": false,
  "tip": {
    "amount": 29,
    "currency": "DKK",
    "anonymous": false
  }
}
GET/api/v1/post-attendance/eligibilityBearer token

Post-attendance actions

Self-scoped class/appointment review and tip eligibility. Organization, target, settings, MobilePay capability, and prompt decision are server-derived from the owned source. Reads are side-effect-free unless `claim_prompt=true` is explicitly supplied by a prompt-mode entry check.

Parameters, scopes and examples

Query parameters

source_typestring · required
class or appointment
source_idstring · required
Owned booking id (class) or appointment id
claim_promptboolean
Reserve an in-app prompt only when true
POST/api/v1/post-attendance/reviewsBearer token

Submit class or appointment review

Creates one source-aware review after server-authoritative attendance/settings checks. Idempotency-Key required. `professional_rating`, tags, recommendation, anonymity, moderation, recipient notification, analytics, and webhooks are venue-controlled.

POST/api/v1/tipsBearer token

Tip a professional (no review)

Create a class or appointment tip in major currency units (`amount: 20` means DKK 20). Organization, professional, currency, Stripe account, and available methods are server-derived. Customer + ephemeral key are optional: customerless PaymentSheet still supports adding a card. MobilePay is returned only for verified Danish/DKK/venue-capable configurations. Idempotency-Key required.

Parameters, scopes and examples

Tip

Request body

{
  "source_type": "class",
  "source_id": "booking-uuid",
  "amount": 29,
  "currency": "DKK",
  "message": "Thank you!",
  "anonymous": false
}

Response example

{
  "data": {
    "tip_id": "uuid",
    "client_secret": "pi_xxx_secret_xxx",
    "payment_intent_id": "pi_xxx",
    "amount": 29,
    "amount_major": 29,
    "amount_minor": 2900,
    "amount_unit": "major",
    "currency": "DKK",
    "merchant_country_code": "DK",
    "source_type": "class",
    "source_id": "booking-uuid",
    "available_payment_methods": [
      "card",
      "mobilepay"
    ],
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_xxx",
    "stripe_account_id": null,
    "is_anonymous": false,
    "instructor_id": "uuid"
  },
  "error": null
}
POST/api/v1/tips/{id}/confirmBearer token

Confirm tip payment

Authenticated tipper-only reconciliation after PaymentSheet/MobilePay/3DS returns. Retrieves the server-owned PaymentIntent in its frozen Stripe account namespace, validates amount/currency/metadata, and emits receipts only after Stripe reports succeeded. Idempotency-Key required.

Parameters, scopes and examples

Path parameters

idstring · required
Tip id
GET/api/v1/tips/{id}Bearer or API key

Tip status

Poll a tip's status after confirming its PaymentIntent (incl. MobilePay / 3DS redirect returns). Access: the tipper (JWT), an org admin/manager (JWT), or an org-scoped API key. Cross-user / cross-tenant reads return 404.

Parameters, scopes and examples

Path parameters

idstring · required
Tip id

Response example

{
  "data": {
    "id": "uuid",
    "status": "succeeded",
    "amount": 29,
    "currency": "DKK",
    "is_anonymous": false,
    "instructor_id": "uuid",
    "created_at": "2026-07-06T12:00:00.000Z"
  },
  "error": null
}
GET/api/v1/me/suppression-statusBearer token

Email/SMS suppression status

Self-service check of the user's email/SMS suppressions. COMMS-1 — LOCKED shape: `data.suppressions[]`, each `{ category: 'marketing'|'transactional'|'system', channel: 'email'|'sms', reason, suppressed_at }`. See docs/api/suppression-status.md.

Parameters, scopes and examples

Response example

{
  "data": {
    "suppressions": [
      {
        "category": "marketing",
        "channel": "email",
        "reason": "user_unsubscribed",
        "suppressed_at": "2026-06-09T12:00:00.000Z"
      }
    ]
  },
  "error": null
}
POST/api/v1/me/dsrBearer token

Submit GDPR data subject request

Submit an Art. 15/16/17/20/21/22 request (access, erasure, portability, rectification, objection, art22 review). 30-day SLA. For erasure, account access is disabled immediately and the response reports erasure_status=pending_fulfillment; a super-admin performs the guarded erasure cascade within the SLA, while the SLA cron only alerts. Statutory records may be anonymised and retained for their legal period. Idempotency-Key required.

Parameters, scopes and examples

DSR request

Request body

{
  "kind": "access",
  "details": "Please send all data you have on me."
}
POST/api/v1/me/parental-consent/resendBearer token

Re-send guardian consent email

Re-trigger the guardian verification email for the caller's outstanding parental-consent request (C06). Matched by the authenticated email — no enumeration. Rotates the token and refreshes the 7-day expiry on the existing pending row (never a duplicate request). Empty body; Idempotency-Key supported; throttled 3/min per IP + 5/hr per user.

POST/api/v1/me/consentBearer token

Capture native consent

Grant or withdraw one supported legal, marketing, analytics, photo/community or health-questionnaire consent for the caller. Health consent requires an explicit membership-verified organization_id, records the current server policy text with checkbox evidence, and a rejection closes prior health grants for that venue. Marketing decisions also require an exact venue. Other venue IDs require active membership. Grant versions are server-canonical; an unavailable policy returns 503 without writing.

Parameters, scopes and examples

Consent capture

Request body

{
  "consent_type": "photo_use",
  "granted": true,
  "organization_id": "uuid"
}
GET/api/v1/me/health-questionnaireBearer token

Health questionnaire completion status

The caller's health-questionnaire completion timestamp (completed_at, null when never submitted). Pre-check for the mobile hot-yoga booking gate.

POST/api/v1/me/health-questionnaireBearer token

Submit health questionnaire (Art. 9)

Submit the spa/hot-yoga health questionnaire for the caller's active org. Runs the Art. 9 contraindication consent gate, inserts a health_questionnaires row (plaintext responses; encrypted at rest by cron), stamps profiles.health_questionnaire_completed_at so the booking gate clears, and writes audit_log/user_events. Requires an Idempotency-Key (a double submit replays). Org resolved via X-Organization-ID / active membership.

Parameters, scopes and examples

Questionnaire responses (7 keys)

Request body

{
  "responses": {
    "heart_condition": false,
    "pregnant": false,
    "blood_pressure": false,
    "medications": "",
    "injuries": "",
    "first_time_hot_yoga": true,
    "acknowledged_risks": true
  }
}
GET/api/v1/me/calendar-feedBearer token

Calendar feed state

Read the caller's external calendar-feed state: { token, enabled, generatedAt }. token is the opaque secret embedded in the public .ics feed URL (null when no feed is provisioned).

Parameters, scopes and examples

Response example

{
  "data": {
    "token": "r4nd0m_base64url_token",
    "enabled": true,
    "generatedAt": "2026-06-26T10:00:00.000Z"
  },
  "error": null
}
POST/api/v1/me/calendar-feedBearer token

Enable calendar feed

Enable the caller's external calendar feed and return the token. Idempotent — an existing token is returned unchanged (never rotated); a new one is minted (256-bit, base64url) only when absent. Empty body. Audited (calendar_feed_token_generated).

Parameters, scopes and examples

Response example

{
  "data": {
    "token": "r4nd0m_base64url_token",
    "feedUrl": null,
    "enabled": true,
    "generatedAt": "2026-06-26T10:00:00.000Z"
  },
  "error": null
}
DELETE/api/v1/me/calendar-feedBearer token

Revoke calendar feed

Revoke the caller's calendar feed: clears the token and disables the feed (the public feed then 404s). Empty body. Audited (calendar_feed_token_revoked).

Parameters, scopes and examples

Response example

{
  "data": {
    "enabled": false
  },
  "error": null
}
GET/api/public/calendar-feed/{token}Public

Public calendar feed

UNAUTHENTICATED — the opaque token in the path IS the credential. Returns one user's bookings as JSON for an external calendar subscription: { bookings, cancellations, userId, generatedAt }. bookings are upcoming events for the next 90 days; cancellations are bookings cancelled in the last 7 days (so calendar apps emit STATUS:CANCELLED). 404s on an unknown or disabled token (indistinguishable). Scoped strictly to the token's single user — no other user's data. 60 req/min per token.

Parameters, scopes and examples

Path parameters

tokenstring · required
Opaque per-user feed token

Response example

{
  "data": {
    "bookings": [
      {
        "id": "uuid",
        "classInstanceId": "uuid",
        "title": "Vinyasa Flow",
        "description": "Instructor: Jane Doe",
        "location": {
          "name": "Studio 1",
          "address": "Studio Lane 24"
        },
        "startAt": "2026-06-27T09:00:00.000Z",
        "endAt": "2026-06-27T10:00:00.000Z",
        "status": "confirmed",
        "instructor": "Jane Doe",
        "room": "Studio 1",
        "organizationName": "Harbor Movement",
        "cancelUrl": "https://harbor-movement.example/bookings/uuid"
      }
    ],
    "cancellations": [],
    "userId": "uuid",
    "generatedAt": "2026-06-26T10:00:00.000Z"
  },
  "error": null
}
POST/api/v1/me/checkout/payment-intentBearer token

Embedded member pass checkout

Venue-scoped browser checkout adapter over the canonical pass purchase engine. Requires member JWT, x-organization-slug and Idempotency-Key. Accepts pass_type_slug, optional expected_pass_type_id from the original authenticated preview, start_date, selection_kind/quantity/option_id, addons, credit_amount and the complete flexible_quote for Flexible passes. A mismatched expected_pass_type_id returns 409 CHECKOUT_PRODUCT_CHANGED before pricing, promotion or purchase effects; omitted identity keeps legacy behavior. Optional confirm_saved_card:true is reserved for the buyer's explicit Pay action; omitted/false prepares an intent without off-session saved-card confirmation. Prospective checkout_operation_version:1 requires expected_pass_type_id and a fresh checkout:v1:<UUIDv4> key; never upgrade an old attempt. An existing versioned attempt returns CHECKOUT_OPERATION_RESOLVING and must use read-only original-attempt confirmation, never mint replay. The initial bounded free/account-credit fixed-pass cohort retains durable completion; unsupported initial configurations keep existing behavior. Exact retries keep the original body and key. Optional flash_sale_id selects a direct offer and is mutually exclusive with promo_code; fixed direct offers require the complete preview direct_purchase_quote. Sale identity, eligibility, lifecycle, promotion, caps, product and signed reviewed pricing are revalidated server-side before a new payment. Unavailable direct offers return FLASH_SALE_UNAVAILABLE; changed or expired proof returns QUOTE_* without falling back to normal price. Generic requests without flash_sale_id retain existing behavior. Returns the existing browser intent_type, client_secret, payment_intent_id, provider account/customer and canonical pricing envelope. Additive pass_type.id identifies the resolved product. purchase_confirmation is null unless canonical no-PI completion is proven; a proof contains confirmed:true, pass_id, pass_type_id and checkout_attempt. Payment status alone never proves fulfilment. Retain the original preview pass_type_id and attempt before dispatch. A recurring migrated legacy card cannot be prepared without an explicit Pay action: 409 SAVED_CARD_PAY_ACTION_REQUIRED occurs before subscription creation. When the checkout opens or re-opens an offer hold, the response also carries hold_expires_at (ISO 8601 UTC instant the reservation lapses, or null when no offer hold applies) and server_time (ISO 8601 UTC, the server clock at response time), always together, next to client_secret. Compute the countdown once as hold_expires_at minus server_time and run it locally, never against the device clock; at zero, disable the pay step and stop any confirm from starting. A lapsed hold that can still be retried returns 409 OFFER_HOLD_EXPIRED; a lapsed hold with no way to restart (capacity gone, the per-client limit reached, or a late charge already refunded) returns 409 OFFER_CONTACT_VENUE, which never offers a restart and instead points the buyer to the venue desk. A first attempt with no reservation ever held, against an offer that is already fully booked, returns 409 OFFER_SOLD_OUT with plain copy and no charge attempted. The response also carries an additive subscription_id (nullable), so a client replaying this same request body later can re-read the countdown on a recurring reservation without opening a second checkout. A recurring membership whose first period is free today (a 0 kr offer or partner code, a free trial, a first cycle fully discounted) and that has no card on file returns intent_type setup with a SetupIntent client_secret (or a payment client_secret when a registration fee is due): nothing is activated and no offer place or code is consumed until that card step succeeds. Confirm it, then call POST /me/checkout/confirm. Abandoning it leaves the code unused; the same member can start a new checkout with the same code. A declined saved-card first invoice returns 402 INSUFFICIENT_FUNDS when the original Stripe charge proves that issuer reason; otherwise it returns 402 PAYMENT_FAILED.

Parameters, scopes and examples

Optional flash_sale_id and the exact flexible_quote or fixed direct_purchase_quote are forwarded unchanged from preview; client amounts never authorize a charge. Exact retained HTTP receipts replay the original payment status after offer expiry, without new provider work. A missing or pruned receipt still requires current offer validation.

Request body

{
  "pass_type_slug": "monthly-membership",
  "expected_pass_type_id": "original-preview-product-uuid",
  "flash_sale_id": "uuid",
  "direct_purchase_quote": {
    "version": 1,
    "organization_id": "uuid",
    "user_id": "uuid",
    "pass_type_id": "uuid",
    "flash_sale_id": "uuid",
    "authority_fingerprint": "server-owned-sha256",
    "credit_applied_minor": 6000,
    "payable_minor": 3900,
    "issued_at": "ISO-8601",
    "expires_at": "ISO-8601",
    "fingerprint": "server-owned-hmac"
  }
}

Response example

{
  "data": {
    "client_secret": "pi_xxx_secret_xxx",
    "payment_intent_id": "pi_xxx",
    "intent_type": "payment",
    "amount": 3900,
    "currency": "DKK",
    "hold_expires_at": "2026-09-15T09:15:34.000Z",
    "server_time": "2026-09-15T09:12:34.000Z",
    "subscription_id": null
  }
}
GET/api/v1/me/checkout/previewBearer token

Checkout pricing preview

Buyer-facing canonical PricingBreakdown with additive pass_type_id to retain before purchase — net/VAT split, registration fee, total today + recurring, localized policy terms, the start-date window, the required legal artifacts (with already_signed), and the buyer's saved signatures. NO charge. Member-JWT + stable x-organization-id (preferred) or legacy x-organization-slug, both membership-scoped. Query: pass_type_slug (required), start_date, binding_months, locale (en|da). A selected binding tier is validated and priced server-side; unavailable tiers return 422. Optional flash_sale_id (UUID) selects a direct venue offer and is mutually exclusive with promo_code. The server resolves the linked promotion and checks venue, lifecycle, eligibility, caps and product compatibility; unavailable offers return 422 FLASH_SALE_UNAVAILABLE, never a normal-price fallback. Flexible direct-offer quotes add flash_sale_id, flash_sale_rule_fingerprint and purchase_obligation_fingerprint; pass the complete flexible_quote unchanged to purchase. One-time direct offers are fixed-price products only: the existing compatibility policy continues to reject Flexible class-pack and time-pass flash sales. Direct offers add purchase_obligation {status:available} for canonical one-time purchases and supported non-deferred introductory memberships. minimum_total_payable is the all-tender contractual minimum in minor units. Recurring disclosures include intro_through_date inside purchase_obligation. Immediately reachable self-service notice retains earliest_cancellation_effective_on. Staff-managed cancellation instead includes purchase_obligation.cancellation {kind:conditional_contractual_minimum, request_method:contact_studio, condition:timely_valid_notice, earliest_possible_end_on} and omits earliest_cancellation_effective_on. Clearly display that both the minimum and earliest possible end depend on timely valid notice; neither is confirmation of an actual cancellation, receipt time or staff response SLA. Aligned trial schedules and exact first/whole-cycle coupon schedules use canonical renewal and commitment rules. Deferred, inexact coupon cadence, immediate-cancellation or unreadable policy contexts return {status:unavailable, reason_code, message} without purported exact facts. One-time purchases omit cancellation facts. Both signed quote authorities bind all facts and policy/anchor inputs; changed authority is refused before new payment side effects. Existing accepted purchase recovery keeps its original snapshot. Fixed direct offers instead return direct_purchase_quote {version:1, organization_id, user_id, pass_type_id, flash_sale_id, authority_fingerprint, credit_applied_minor, payable_minor, issued_at, expires_at, fingerprint}. This five-minute signed proof binds the reviewed canonical price, commercial terms and exact account-credit/cash split. Both tender fields are nonnegative integer minor units; payable_minor equals charged_today. A changed credit balance or tender requires a new review (QUOTE_STALE), never a larger cash charge. Direct gift cards are not supported unless quoted; generic gift-card checkout is unchanged. Send the proof unchanged to purchase. Optional direct_offer {flash_sale_id, name, normal_amount, offer_amount} is display metadata for fixed one-time offers (amounts in minor units); charged_today is the payable authority. Deprecated for promo codes: a promo_code in the query string lands in logs and referrers; send it with POST /api/v1/me/checkout/preview instead.

Parameters, scopes and examples

Query parameters

pass_type_slugstring · required
Venue pass slug
flash_sale_idstring
Optional direct-offer UUID; mutually exclusive with promo_code
promo_codestring
Deprecated here (URLs land in logs): send it with POST instead
POST/api/v1/me/checkout/previewBearer token

Checkout pricing preview (parameters in the body)

Identical to GET /api/v1/me/checkout/preview — same auth, rate limit, validation and response — with the parameters as a flat JSON object instead of the query string, so a promo code never travels in a URL. Use this whenever promo_code is sent.

Parameters, scopes and examples

The GET query parameters as a flat JSON object

Request body

{
  "pass_type_slug": "monthly-membership",
  "promo_code": "ACME-7KQ2M9XW",
  "locale": "en"
}
POST/api/v1/me/checkout/confirmBearer token

Confirm a member pass checkout

Alternatively provide checkout_attempt and the original preview pass_type_id for DB-read-only completion reconciliation: confirmed:true includes purchase_confirmation {confirmed:true,pass_id,pass_type_id,checkout_attempt}; missing or expired proof returns confirmed:false,purchase_confirmation:null,payment_state:resolving. This branch performs no purchase replay or provider work and does not authorize a retry. Free-pass recovery currently requires a retained original response proof; durable raw-attempt mapping remains incomplete. Confirm the exact existing PaymentIntent or deferred SetupIntent for an authenticated venue member. A successful client-side payment is not proof of pass fulfilment. The canonical finalizer can return 409 CUSTOMER_ELIGIBILITY if admission is refused or unresolved after settlement. Preserve the original purchase operation and show a neutral resolving outcome; do not start a replacement payment. Other existing 409 outcomes remain distinct. For a free-start membership (setup_intent_id), this call activates the membership and redeems its offer, exactly once together with the setup_intent.succeeded webhook; a SetupIntent that has not succeeded yet (for example still in 3-D Secure) returns 409 PAYMENT_NOT_COMPLETED and activates nothing. If the offer place can no longer be granted it returns 409 OFFER_HOLD_EXPIRED (start again; nothing was charged and the code was not used) or 409 OFFER_CONTACT_VENUE, and an attempt that was already closed returns 409 MEMBERSHIP_SETUP_EXPIRED.

Parameters, scopes and examples

Exactly one payment_intent_id, setup_intent_id, or checkout_attempt paired with original pass_type_id

Request body

{
  "payment_intent_id": "pi_existing"
}

Response example

{
  "data": {
    "confirmed": true,
    "payment_intent_id": "pi_existing"
  },
  "error": null
}
POST/api/v1/me/checkout/sign-artifactBearer token

Sign a purchase-time legal artifact

Records a waiver / ToS / privacy / contract acceptance with IP + user-agent + version + signature. Idempotent on (user, document, version); a stale version → 409 force-refetch; a minor (DOB < 18) → 409 + parental consent. Supports saved-signature reuse (saved_signature_id) honouring signature_kind. Linked contracts require pass_type_slug and may include start_date; the endpoint idempotently creates/adopts the exact current-version pre-purchase contract before signing.

Parameters, scopes and examples

Artifact acceptance

Request body

{
  "artifact_kind": "contract",
  "document_id": "uuid",
  "version_id": "uuid:2",
  "signature_data": "data:image/png;base64,…",
  "pass_type_slug": "monthly-membership",
  "start_date": "2026-09-01"
}
GET/api/v1/me/payments/historyBearer token

Read my archived payment history

Read-only payment snapshots retained during a brand split or transfer. These records are separate from the live payment ledger and do not support refunds, receipt resends, invoice actions or revenue totals. Each row retains its original amount in major currency units, ISO currency, source status, occurrence time and snapshot time. Archive IDs are not payment IDs. Pagination sorts by original occurred_at and archive ID; reuse the exact opaque cursor with the same venue and member. Invalid pagination returns 400; unavailable or unverified history returns 503 rather than an empty history. Always scoped to the authenticated member. Optional X-Organization-Slug restricts the collection to one venue; an unknown slug returns no rows, never a global fallback. Without that header, the collection includes this member’s archived history across venues.

Parameters, scopes and examples

Query parameters

limitnumber
Page size: 1–100, default 25.
afterstring
Opaque next_cursor from the same archive collection and scope.
POST/api/v1/me/checkout/recoveryBearer token

Inspect an original product-package or paid service purchase

Read-only, non-minting recovery for an active authenticated venue member. Requires x-organization-slug and the exact original purchase key. kind product_package reads the canonical product-package operation; kind service reads the paid appointment operation. Returns frozen amount_minor/currency and durable/provider status, never client secrets. unknown/read errors are distinct from an authoritative not_found; unbound creation stays pending and provider success stays pending until durable fulfillment. Every outcome retains the original key; not_found is a point-in-time read and never authorizes a replacement financial operation. Passes, recurring products, bundles and staff POS are unsupported.

Parameters, scopes and examples

Original purchase identity

Request body

{
  "kind": "product_package",
  "item_id": "00000000-0000-4000-8000-000000000001",
  "original_idempotency_key": "original-purchase-key"
}

Response example

{
  "data": {
    "status": "pending",
    "retry_policy": "retain_original_key",
    "operation_id": "uuid",
    "amount_minor": 2199,
    "currency": "USD",
    "payment_status": "succeeded"
  },
  "error": null
}
GET/api/v1/me/apple-subscriptionBearer token

My Apple subscription

Returns the authenticated Namasté Online member’s Apple-billed pass status and exact expiration. A null result means this account has no linked Apple purchase.

POST/api/v1/me/apple-subscriptionBearer token

Link a verified Apple subscription

Verifies a StoreKit signed transaction against Apple’s certificate chain and binds its appAccountToken to the authenticated Namasté Online member. Replays update the same pass. A Production purchase is also recorded once as an App Store sale at estimated net proceeds; Sandbox purchases never are.

Parameters, scopes and examples

StoreKit 2 signed transaction JWS

Request body

{
  "signed_transaction": "<Apple signed transaction JWS>"
}
GET/api/v1/me/entitlementsBearer token

My entitlements

The caller's entitlement matrix for one venue: can_book_physical (any active pass with grants_in_person), can_watch_online (grants_online_class_access), online_only, bookable_class_type_ids ("all" when any usable pass is unrestricted), and an active_passes[] summary (slug, category, grants, validity, clips, allowed_brand_ids). Optional brand_id OR class_instance_id scopes the capability flags to passes valid for that brand / occurrence (effective brand ∪ class-type brand ∪ "Also show under…" brands) and is echoed as brand_scope. Errors: 400 INVALID_BRAND_SCOPE (both, or a non-UUID), 404 BRAND_NOT_FOUND / CLASS_NOT_FOUND, 500 BRAND_LOOKUP_FAILED (brand read failed), 503 PASS_RESTRICTION_UNVERIFIED (pass restrictions unreadable; retry). The booking engine enforces the same matrix; booking refusals include PASS_BRAND_RESTRICTED and CLASS_TYPE_RESTRICTED (422).

Parameters, scopes and examples

Query parameters

organization_idstring · required
Venue to resolve entitlements for
brand_idstring
Optional brand (UUID, this venue): flags for classes owned by that brand
class_instance_idstring
Optional class occurrence (UUID, this venue): flags under the booking engine brand scope

Response example

{
  "data": {
    "organization_id": "uuid",
    "as_of": "2026-07-11",
    "brand_scope": null,
    "can_book_physical": false,
    "can_watch_online": true,
    "online_only": true,
    "bookable_class_type_ids": [
      "uuid-a",
      "uuid-b"
    ],
    "active_passes": [
      {
        "id": "uuid",
        "pass_type_slug": "online-unlimited",
        "name": "Online Unlimited",
        "category": "membership",
        "grants": {
          "in_person": false,
          "online": true
        },
        "valid_from": "2026-07-01",
        "valid_to": null,
        "clips_remaining": null,
        "allowed_brand_ids": []
      }
    ]
  }
}
GET/api/v1/me/invoicesBearer token

List my invoices

Cursor-paginated list of the caller's member-visible client invoices. Drafts are excluded and every row includes an authenticated document_path for the print-ready HTML invoice. Every row also carries outstanding_balance (the balance still owed on an open invoice, 0 once paid or closed, major units of the invoice currency), outstanding_balance_minor (the same in the smallest unit) and can_pay_now (true when POST /api/v1/me/invoices/{id}/retry accepts it: sent, viewed, overdue or partially paid with a balance, at a venue that can take card payments online) and pay_now_unavailable_reason (card_payments_unavailable when an owed balance cannot be paid online — tell the client to contact the venue — else null). Show "Pay remaining balance" when amount_paid > 0 and pass outstanding_balance_minor as expected_amount_minor.

Parameters, scopes and examples

Query parameters

limitnumber
Page size (default 20, max 100)
afterstring
Opaque cursor from a previous page

Response example

{
  "data": [
    {
      "id": "uuid",
      "invoice_number": "INV-0042",
      "status": "partially_paid",
      "total": 1000,
      "amount_paid": 300,
      "currency": "EUR",
      "issued_at": "2026-09-01T09:00:00Z",
      "due_date": "2026-09-15",
      "paid_at": null,
      "pdf_url": null,
      "created_at": "2026-09-01T09:00:00Z",
      "outstanding_balance": 700,
      "outstanding_balance_minor": 70000,
      "can_pay_now": true,
      "pay_now_unavailable_reason": null,
      "document_path": "/me/invoices/uuid/document"
    }
  ]
}
GET/api/v1/me/invoices/{id}Bearer token

Invoice detail

Owner-scoped detail for one issued client invoice, including line items and the totals breakdown (subtotal, discount, VAT, total, amount_paid), plus outstanding_balance, outstanding_balance_minor, can_pay_now and pay_now_unavailable_reason (same meaning as on GET /api/v1/me/invoices).

Parameters, scopes and examples

Path parameters

idstring · required
Invoice id
GET/api/v1/me/invoices/{id}/documentBearer token

Invoice print document

Authenticated owner- and venue-scoped print-ready HTML for one member-visible invoice.

Parameters, scopes and examples

Path parameters

idstring · required
Invoice id
GET/api/v1/me/guest-invitesBearer token

Can I bring a guest to this class?

GUEST-INVITE-01 — whether the caller's passes qualify them to host a guest at this class, the venue guest price, the standard single-class price to strike through (compare_at_price, display only), and any invitations they already have open for it. `reason` is plain-language copy safe to render verbatim when `eligible` is false. For the two Vibro brands, a booked host also receives `age_band_prices` for a separate one-class guest sale.

Parameters, scopes and examples

Query parameters

class_instance_idstring · required
Class to check

Response example

{
  "data": {
    "eligible": true,
    "reason": null,
    "reason_code": null,
    "price": 149,
    "age_band_prices": null,
    "compare_at_price": 275,
    "savings_percent": 46,
    "currency": "DKK",
    "class": {
      "id": "uuid",
      "className": "Hot Yoga",
      "startTime": "2026-08-12T17:00:00Z"
    },
    "invites": []
  },
  "error": null
}
POST/api/v1/me/guest-invitesBearer token

Invite a guest to a class

Creates the invitation plus its pending guest seat (a GUEST-PAY-01 `pending_payment` booking that holds NO capacity until paid). `payer:'guest'` returns the link to share; `payer:'host'` additionally returns a Stripe Checkout URL (saved card, new card, or MobilePay). `return_base_url` must be an allowlisted host or it is ignored. Vibro Yoga and Vibration Shower require a host-owned class booking, `payer:host`, and an `age_band` of `u30` or `o30`; U30 also requires `guest_date_of_birth`, and Vibro Yoga requires `guest_address`.

Parameters, scopes and examples

Guest invitation

Request body

{
  "class_instance_id": "uuid",
  "guest_name": "Alex Friend",
  "guest_email": "alex@example.com",
  "payer": "guest",
  "return_base_url": "https://hotyogacph.dk"
}
POST/api/v1/me/guest-invites/{id}/checkoutBearer token

Resume the same host-paid guest sale

For a pending host-paid guest invitation, returns the existing or idempotently prepared PaymentIntent so a brand site can reconcile an uncertain MobilePay or card return. A paid invite returns status `paid`. The caller must be the original host in the same venue.

DELETE/api/v1/me/guest-invites/{id}Bearer token

Withdraw a guest invitation

Withdraws an UNPAID invitation and releases its pending seat. A paid guest spot is a real booking — cancel it through the normal booking cancellation path so the venue's refund and fee rules apply (409 `ALREADY_PAID`).

GET/api/v1/guest-invites/{token}Public

Resolve a guest invitation (public)

GUEST-INVITE-01 — the invitation landing page a friend opens. Anonymous-allowed by design (the token is the capability); returns who invited them, the class, the price and the struck-through standard price, and nothing else about the host's account. `state` is `needs_account` for a signed-out visitor, `payable` once signed in, plus `already_paid` / `cancelled` / `expired` / `class_started` / `class_full`.

POST/api/v1/guest-invites/{token}/checkoutPublic

Pay a guest invitation without an account

GUEST-INVITE-01 — the Guest Visitor branch. ANONYMOUS-ALLOWED (the token is the capability): the invited friend pays without creating an account and receives a Stripe Checkout URL. Deliberately does NOT claim the seat, so no profile is created and `bookings.user_id` stays the host. Confirmation is still the verified-payment webhook. Trade-off the calling site MUST surface: with no login, only the host or the venue can cancel it afterwards. Refuses with 409 `ALREADY_CLAIMED` once someone has linked the invitation to an account.

Parameters, scopes and examples

Checkout options

Request body

{
  "return_base_url": "https://hotyogacph.dk",
  "legal_acceptances": []
}
POST/api/v1/guest-invites/{token}/waitlistPublic

Join the waiting list for a full class

GUEST-INVITE-01 — the class filled up before the invited friend accepted. ANONYMOUS-ALLOWED. An unpaid invitation never held a seat, so this is a normal outcome, not an error: the friend joins the waiting list and is NOT charged. If a spot opens, `reinviteWaitlistedGuests` sends a fresh payment link. Returns `{ position, already_on_waitlist }`.

POST/api/v1/guest-invites/{token}/claimBearer token

Claim a guest invitation

The invited friend, now signed in, takes ownership of the guest seat and gets a Stripe Checkout URL. Claiming rebinds `bookings.user_id` to their profile (the host stays on `host_user_id`), which is what makes the spot appear in their own bookings and cancellable by them under the venue's ordinary cancellation rules. Capacity is still only taken by the verified-payment confirm RPC.

Parameters, scopes and examples

Claim options

Request body

{
  "return_base_url": "https://hotyogacph.dk",
  "legal_acceptances": []
}
Bookings12 documented operations
POST/api/v1/live-music/eventsBearer token

Record consented music player observation

Confirmed pilot member and active scoped viewer session required. Stable event IDs dedupe selection, attempt, player progress, stop and error observations. Actual output is not inferred.

Parameters, scopes and examples

Idempotent event UUID, viewer session, brand, mix, kind, levels, position, error code and device.

Request body

{
  "id": "00000000-0000-4000-8000-000000000003",
  "viewer_session_id": "00000000-0000-4000-8000-000000000004",
  "brand_id": "00000000-0000-4000-8000-000000000001",
  "mix_id": null,
  "kind": "attempt",
  "music_volume": 0.3,
  "teacher_volume": 1,
  "position_seconds": 30,
  "error_code": null,
  "device": "web"
}
GET/api/v1/bookingsBearer or API key

My bookings

List authenticated user's bookings across all venues, or only the venue named by X-Organization-Slug when that header is sent (an unknown slug, or an organization_id naming another venue, returns an empty list). Supports cursor and offset pagination. Active upcoming bookings carry `cancellation_terms` (CANCEL-PREVIEW-01 v1.1): that booking’s effective window (`source` course | location | brand | venue), its late and no-show fee and `pass_effect_if_late` for its pass, by the same rule as GET /api/v1/me/bookings/{id}/cancellation-preview; null for cancelled, past or imported rows and when it cannot be read.

Parameters, scopes and examples

Required scopes

read:bookings

Query parameters

statusstring
Filter: confirmed, waitlisted, cancelled, checked_in, no_show
upcomingboolean
Only future bookingsDefault: true
organization_idstring
Filter to one venue
pass_idstring
Only bookings funded by this pass
cycle_startstring
Venue-local YYYY-MM-DD start of a pass usage cycle (inclusive). Requires organization_id.
cycle_endstring
Venue-local YYYY-MM-DD end of a pass usage cycle (exclusive). Requires organization_id.
afterstring
Cursor for pagination
limitinteger
Items per pageDefault: 20
POST/api/v1/bookingsBearer or API key

Book a class

Create a booking. Validates pass eligibility, capacity, booking window, daily limits, and class restrictions. A venue daily-limit refusal is DAILY_LIMIT_REACHED with details { daily_limit, daily_used, daily_limit_scope, workshops_count, course_sessions_count } and a message naming what does not count; a pass per-day cap refusal is CLASS_LIMIT_EXCEEDED with details { pass_daily_limit, pass_daily_used, workshops_count, course_sessions_count }; if a pass per-day cap cannot be checked the booking is refused with 503 DAILY_LIMIT_UNVERIFIED (nothing booked; retry). Supports idempotency via Idempotency-Key header. Default pass: when the member (JWT) has chosen a default pass (PUT /api/v1/me/default-pass) that is used up or cannot be used for this class and another pass can, the response is 409 DEFAULT_PASS_UNUSABLE with details { default_pass: { id, name, reason: clips_exhausted|expired|paused|past_due|not_eligible|not_found }, suggested_pass: { id, name, remaining, end_date }, choices }. Retry with pass_id = suggested_pass.id plus switch_default: true (book and make it the default) or use_once: true (book, keep the default). switch_default and use_once are mutually exclusive and require pass_id; API-key bookings never receive this prompt. With no default the existing order applies silently; the booking response names the pass used (passes.pass_types.name). Waiver: when the class type requires one, an unsigned member gets 400 WAIVER_REQUIRED; if the signature check cannot run the booking is refused with 503 WAIVER_CHECK_UNAVAILABLE (nothing booked; retry). The response carries `cancellation_terms` (CANCEL-PREVIEW-01 v1.2): the new booking’s effective cancellation terms at booking time, from the same rule as GET /api/v1/bookings/{id}; null when none apply.

Parameters, scopes and examples

Required scopes

write:bookings

Booking

Request body

{
  "class_instance_id": "uuid",
  "attendance_type": "physical",
  "pass_id": "uuid",
  "switch_default": false,
  "use_once": false
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "confirmed",
    "class_instance": {
      "class_name": "Hot Yoga",
      "start_time": "2026-04-12T07:00:00Z"
    }
  }
}
GET/api/v1/bookings/{id}Bearer or API key

Get booking

Retrieve a single booking with class details, pass info, and check-in status. Optional X-Organization-Slug restricts access to that venue; unknown or mismatched venue and another member return 404 NOT_FOUND before effects. No header preserves account-wide member ownership. An active upcoming booking carries `cancellation_terms` (CANCEL-PREVIEW-01 v1.1) — its effective window, fees and `pass_effect_if_late`, by the preview’s rule; null otherwise.

Parameters, scopes and examples

Required scopes

read:bookings

Path parameters

idstring · required
Booking ID
POST/api/v1/bookings/buddyBearer token

Invite a client to the same class

Creates and emails a venue-branded invitation to an existing active client at the same venue. The inviter must already have a confirmed booking. The recipient books with their own pass or payment; no guest funding or inviter entitlement is used. Idempotency-Key supported.

Parameters, scopes and examples

Registered-client class invitation

Request body

{
  "class_instance_id": "uuid",
  "recipient_email": "friend@example.com",
  "message": "Want to join me?"
}

Response example

{
  "data": {
    "invite_id": "uuid",
    "invitation_url": "https://venue.example/buddy-invite/token",
    "expires_at": "2026-08-01T17:00:00Z",
    "recipient_name": "Alex",
    "delivery_status": "sent"
  }
}
POST/api/v1/bookings/buddy/acceptBearer token

Accept a client class invitation

Accepts a buddy invitation for the authenticated recipient and books the same class with that recipient’s own eligible pass or normal venue booking rules. The invited email/account and active venue membership must match. Idempotency-Key supported.

Parameters, scopes and examples

Invitation token

Request body

{
  "token": "48-character-hex-token"
}

Response example

{
  "data": {
    "invite_id": "uuid",
    "booking_id": "uuid",
    "class_instance_id": "uuid",
    "status": "confirmed",
    "already_booked": false
  }
}
DELETE/api/v1/bookings/{id}Bearer or API key

Cancel booking

Cancel a booking with the same rule as GET /api/v1/me/bookings/{id}/cancellation-preview. data.applied is that CancellationDecision for what the cancel actually did (fee, clip effect, copy); cancellation_fee (major units) is unchanged. Send accepted_outcome and accepted_fee_minor from the preview the member confirmed: when the fresh terms differ (the window closed meanwhile, the fee changed, or online cancellation is no longer allowed) nothing is cancelled and the response is 409 CANCELLATION_TERMS_CHANGED with error.details.decision holding the fresh terms. 503 CANCELLATION_TERMS_UNAVAILABLE means the venue rules could not be read and nothing was cancelled. Send accepted_outcome and accepted_fee_minor together or neither. A body that is not JSON, only one of the two, or a malformed reason / accepted_*, is 400 VALIDATION_ERROR and nothing is cancelled. Optional X-Organization-Slug restricts access to that venue; unknown or mismatched venue and another member return 404 NOT_FOUND before effects. No header preserves account-wide member ownership.

Parameters, scopes and examples

Required scopes

write:bookings

Path parameters

idstring · required
Booking ID

Optional reason plus the terms the member confirmed

Request body

{
  "reason": "Plans changed",
  "accepted_outcome": "late",
  "accepted_fee_minor": 5000
}
POST/api/v1/bookings/{id}/cancelBearer or API key

Cancel booking (POST)

Cookie-authenticated alternate cancellation endpoint with its legacy raw response envelope (the RPC result plus `applied`, the CancellationDecision for what the cancel did). Accepts the same accepted_outcome / accepted_fee_minor as DELETE /api/v1/bookings/{id} (409 cancellation_terms_changed with details.decision when they no longer hold; 400 validation_error for a body that is not JSON, only one of accepted_outcome / accepted_fee_minor, or a malformed reason / accepted_*; nothing cancelled). Honors X-Organization-Slug and member ownership before mutation; unknown or conflicting venue returns 404.

Parameters, scopes and examples

Required scopes

write:bookings

Path parameters

idstring · required
Booking ID
POST/api/v1/bookings/{id}/checkinBearer token

Self check-in

Member self check-in. Available within configured time window before class start. Optional X-Organization-Slug restricts access to that venue; unknown or mismatched venue and another member return 404 NOT_FOUND before effects. No header preserves account-wide member ownership.

Parameters, scopes and examples

Path parameters

idstring · required
Booking ID
POST/api/v1/class-instances/{id}/join-onlineBearer token

Join class online (live watch)

NAMASTE-GATES-01 / LIVE-PARTICIPATION-01 — entitlement-based live-watch path. An existing physical booking returns 409 PHYSICAL_BOOKING_SWITCH_REQUIRED with error.details {booking_id, class_instance_id} before reservation, signing or admission; a Join tap never converts attendance or assesses a fee. The explicit switch endpoints are not yet released. Requires an active pass whose type grants online class access and covers the class type; finds or creates the caller’s attendance_type=online booking (idempotent, respects online_capacity, consumes a clip only for clip-based passes) and returns a provider-signed playback URL plus viewer-session telemetry token. The additive playback_transport is hls or whep. A WHEP input requires X-Playback-Transports containing whep; legacy clients receive 426 PLAYBACK_TRANSPORT_UNSUPPORTED before booking or clip consumption. WHEP never falls back to HLS. First entry opens 10 minutes before start and closes exactly at start; admitted viewers and an entitled viewer with a server-confirmed pre-start waiting reservation may recover through end + 5 minutes unless they explicitly leave after start. A not-ready/paused response includes additive error.details {viewer_session_id, class: {name, instructor, start_time, end_time}} only when the reservation RPC supports durable waiting; legacy RPC deployments omit that promise. Waiting creates no booking or clip consumption and every retry rechecks current entitlement. Stream readiness is checked BEFORE any booking side effect: the class instance must be status=live AND carry a playback id, otherwise 409 STREAM_NOT_READY (retryable) — a prepared playback id alone is never readiness, because the phone publisher stamps one while the instance is still scheduled. The additive stream_paused flag is true while the operator has paused the stream; the URL stays valid, so render a paused notice instead of the player. The additive music_pilot_enabled field controls music UI; signed private music_url, stable mix IDs and brand choices are issued only to the confirmed pilot account. GET /api/v1/bookings/{id}/online-join (the booking-scoped twin for members who already hold an online booking) returns the same STREAM_NOT_READY code and the same stream_paused flag. A request with `X-App-Digital-Content: none` (store build without in-app digital content) is refused by both routes with 403 IN_APP_DIGITAL_CONTENT_DISABLED before any booking or signed playback URL.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID

Query parameters

music_brand_idstring
Optional brand UUID for the private music pilot; only the confirmed pilot account receives signed mix choices.
max_resolutionstring
Optional Mux playback cap: 720p or 1080p, embedded in the signed token. Omit for automatic quality. Invalid values return 400 INVALID_QUALITY; Cloudflare delivery ignores this cap.

Response example

{
  "data": {
    "playback_url": "https://stream.mux.com/PLAYBACK_ID.m3u8?token=…",
    "playback_transport": "hls",
    "playback_id": "PLAYBACK_ID",
    "expires_at": "2026-07-11T11:00:00Z",
    "booking_id": "uuid",
    "stream_paused": false,
    "class": {
      "name": "Namasté Flow",
      "instructor": "Jane Doe",
      "start_time": "2026-07-11T10:00:00Z",
      "end_time": "2026-07-11T11:00:00Z"
    }
  }
}
GET/api/v1/me/bookings/{id}/cancellation-previewBearer token

Preview cancelling my class booking

What cancelling one owned class booking NOW means, from the venue’s current rules and in the venue’s timezone. `data.decision` is the CancellationDecision every client renders: outcome free | late | not_cancellable, reason_code, the moment the free window closes (ISO with the venue offset), the exact fee (minor units, currency, VAT breakdown, formatted), the pass effect (clip_returned | clip_deducted | unlimited | course_booking_returned | course_booking_used | none), the policy line, the consequence message and the confirm-button label, each in English and Danish. The window resolves course-access snapshot → class location → venue → 3 h; the fee resolves from the venue’s cancellation matrix (pass type → category → default) and the location/venue late_cancel_fee_amount. The window is closed AT its closing instant. The member cancel endpoints decide with the same rule, so the preview is what the cancel does; send accepted_outcome / accepted_fee_minor on the cancel to have it refused (409 CANCELLATION_TERMS_CHANGED) when the terms changed meanwhile. Computed per request and served with Cache-Control: no-store — never cache it across the window boundary. The pre-v1 flat fields (can_cancel, blocked_reason, cancellation_window_hours, is_late, will_charge, fee_amount in major units, currency, will_forfeit_clip, course_access, will_forfeit_course_booking, message) remain, derived from the same decision; locale=da selects the language of the flat message. Honours the optional x-organization-slug tenant scope; another member’s or venue’s booking returns 404. A failed rules read returns 500 CANCELLATION_PREVIEW_QUERY_FAILED — clients must then refuse to cancel blindly and offer a retry.

Parameters, scopes and examples

Path parameters

idstring · required
Booking UUID

Query parameters

localestring
Language of the legacy flat `message` fieldDefault: en

Response example

{
  "data": {
    "booking_id": "uuid",
    "can_cancel": true,
    "blocked_reason": null,
    "cancellation_window_hours": 3,
    "is_late": true,
    "will_charge": true,
    "fee_amount": 50,
    "currency": "DKK",
    "will_forfeit_clip": false,
    "course_access": false,
    "will_forfeit_course_booking": false,
    "message": "Cancelling now counts as a late cancellation: the cancellation window closed at 06:30 today (venue time). You will be charged 50 kr.",
    "decision": {
      "version": 1,
      "booking_id": "uuid",
      "outcome": "late",
      "reason_code": "inside_window",
      "computed_at": "2026-09-24T05:05:00.000Z",
      "timezone": "Europe/Copenhagen",
      "class": {
        "name": "Hot Yoga 60",
        "starts_at": "2026-09-24T09:30:00+02:00"
      },
      "window_hours": 3,
      "window_closes_at": "2026-09-24T06:30:00+02:00",
      "hours_before_start": 2.42,
      "fee": {
        "amount_minor": 5000,
        "currency": "DKK",
        "vat_included": true,
        "vat_rate_percent": 0,
        "vat_amount_minor": 0,
        "formatted": "50 kr"
      },
      "pass_effect": "unlimited",
      "pass": {
        "id": "uuid",
        "name": "Unlimited Monthly"
      },
      "refund": null,
      "policy_summary": {
        "en": "Free cancellation until 3 hours before class. After that, a 50 kr late-cancellation fee applies.",
        "da": "Gratis afmelding indtil 3 timer før holdet. Derefter koster en sen afmelding 50 kr."
      },
      "message": {
        "en": "Cancelling now counts as a late cancellation: the cancellation window closed at 06:30 today (venue time). You will be charged 50 kr.",
        "da": "Afmelder du nu, er det en sen afmelding: afmeldingsfristen udløb kl. 06:30 i dag (lokal tid). Du bliver opkrævet 50 kr."
      },
      "confirm_label": {
        "en": "Cancel and pay 50 kr",
        "da": "Afmeld og betal 50 kr"
      }
    }
  }
}
POST/api/v1/qr/checkinBearer token

QR self-check-in

Client-facing: exchange a valid QR token for a check-in on the caller's booking. Token must be active and not expired. Anti-replay: a booking can only transition to checked_in once. Every scan writes an audit_log entry regardless of outcome.

Parameters, scopes and examples

QR token from scanned code

Request body

{
  "token": "a1b2c3d4e5f6...64hex chars"
}

Response example

{
  "data": {
    "booking_id": "uuid",
    "checked_in_at": "2026-06-01T10:02:13Z",
    "class_instance_id": "uuid"
  }
}
Benefits5 documented operations
GET/api/v1/benefitsBearer token

List client benefits

Returns offers and current server eligibility for the authenticated member. Optional venue filter narrows active memberships; branded organization slug scope is enforced. Paused, ended and unavailable offers may remain visible without valid proof. No cached result is proof.

Parameters, scopes and examples

Query parameters

organization_idstring
Venue UUID filter
POST/api/v1/benefits/proofBearer token

Create a live benefit card

Rechecks the vendor agreement, venue settings and exact qualifying pass or service settlement under the agreed trigger. Proof rotates every 20 seconds and expires after 60 seconds. verification_url contains an opaque fragment token; never log or persist this response.

Parameters, scopes and examples

Selected offer and owning venue

Request body

{
  "deal_id": "uuid",
  "organization_id": "uuid"
}
GET/api/v1/admin/benefitsBearer token

Read venue benefits configuration

Requires passes.manage. Returns approved deals, pass/service choices, saved assignments, rollout enabled state and optimistic revision for the authorized venue.

PUT/api/v1/admin/benefitsBearer token

Configure venue benefits

Requires passes.manage. Atomically replaces pass and service assignments within current vendor constraints. accepted_version must match each enabled agreement; expected_revision prevents lost edits. Does not notify clients.

Parameters, scopes and examples

Complete configuration; disabled assignments retain history of accepted terms

Request body

{
  "enabled": true,
  "expected_revision": 0,
  "assignments": [],
  "service_assignments": []
}
GET/api/v1/admin/benefits/previewBearer token

Preview saved member eligibility

Requires passes.manage and resolves the target member only within the authenticated venue. Evaluates saved configuration with the canonical eligibility service; never issues a proof for staff.

Parameters, scopes and examples

Query parameters

user_idstring · required
Member UUID
Discovery22 documented operations
GET/api/v1/discoveryPublic

Discover live venue inventory

Bounded, paginated Universe discovery for one canonical world and venue-local date. Returns capped live class or service/appointment summaries with explicit partial failures; member coordinates are not accepted.

Parameters, scopes and examples

Query parameters

worldstring · required
Required: classes, treatments, or salon
datestring · required
Required venue-local date (YYYY-MM-DD)
time_windowstring
any, morning, afternoon, or eveningDefault: any
searchstring
Venue, location, class, or service search
pageinteger
Page numberDefault: 1
limitinteger
Venue items per page (max 4)Default: 4
GET/api/v1/discovery/countsPublic

Legacy activity and published service venue counts

Lightweight Explore landing counts. Legacy classes, treatments and salon fields retain their same-day activity signals for older clients. Additive offering_treatments and offering_salon fields count eligible venues with at least one active published service. Offering counts do not promise a free slot today; use /discovery for actual availability.

Parameters, scopes and examples

Query parameters

datestring · required
Required venue-local date (YYYY-MM-DD)

Response example

{
  "data": {
    "date": "2026-08-10",
    "classes": 3,
    "treatments": 1,
    "salon": 0
  },
  "error": null
}
GET/api/v1/venuesPublic

List venues

Search and discover venues. Supports text search, geo-location search with Haversine distance, city filtering, and pagination.

Parameters, scopes and examples

Query parameters

searchstring
Search by venue name, slug, or venue ID
latnumber
Latitude for location-based search
lngnumber
Longitude for location-based search
radiusnumber
Search radius in kmDefault: 10
citystring
Filter by city name
pageinteger
Page numberDefault: 1
limitinteger
Items per page (max 100)Default: 20

Response example

{
  "data": [
    {
      "id": "uuid",
      "name": "Acme Studio",
      "slug": "acme-studio",
      "address": "123 Example Street, Copenhagen",
      "class_count": 12,
      "location_count": 2
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 45,
    "has_more": true
  }
}
GET/api/v1/venues/{slug}Public

Get venue details

Full venue profile including brands, locations with rooms, opening hours, amenities, photos, booking mode and branded app configuration. settings.booking.max_bookings_per_day is the daily booking limit (0 = no limit); settings.booking.daily_limit_counts_workshops and daily_limit_counts_course_sessions (default true) say whether workshops and course sessions count toward it and toward pass per-day caps. fresh=1 requests an uncached configuration read for active mobile venue refreshes. Failed configuration reads return 503 rather than a successful empty configuration.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug

Query parameters

freshstring
Set to 1 for a private no-store configuration read
GET/api/v1/venues/{slug}/joinBearer token

Check venue join eligibility

Bearer-authenticated, read-only membership check for Consumer apps. Resolves the target from its slug or organization UUID and reports member, can_join, or an unavailable reason without changing account state.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug or organization UUID
POST/api/v1/venues/{slug}/joinBearer token

Join a venue with explicit consent

Bearer-authenticated and Idempotency-Key protected. Requires {consent:true}; creates one active member relationship without changing an existing role, assigns the venue client ID, and emits the canonical audit, analytics, and member.created integration events.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug or organization UUID

Explicit user consent to add this venue to their account.

Request body

{
  "consent": true
}
GET/api/v1/venues/{slug}/schedulePublic

Get class schedule

Live class schedule with real-time availability. Filter by date range, location, brand, class type, instructor, or online-only. `class_type.image_url` is the nullable class hero image for native discovery and booking. Each row includes the additive general-policy `requires_workshop_entry` flag and a nullable public `workshop_entry_target`; `is_bookable` retains its capacity/status/time meaning. Each row carries `course_identifiers` (additive; `[]` when none): `{course_id, label, tone, course_name}` pills the venue set on the public course(s) a workshop date is linked to, `tone` one of default|success|warning|danger|info|purple|pink. Each row also carries `cancellation_terms` (CANCEL-PREVIEW-01 v1.1): the class’s effective cancellation window (`window_hours`, `window_closes_at` in the venue timezone, `source` location | brand | venue), the venue-default `late_fee` / `no_show_fee` (`varies_by_pass` when some passes have their own rule), `online_cancellation` and a `policy_summary`, from the same rule as the cancellation preview; null when it cannot be read. Static per class, so safe to cache; the preview stays authoritative at cancel time.

Parameters, scopes and examples

Required scopes

read:schedule

Path parameters

slugstring · required
Venue URL slug

Query parameters

fromstring
Start date (YYYY-MM-DD)Default: today
tostring
End date (YYYY-MM-DD)Default: +7 days
location_idstring
Filter by location
brand_idstring
Filter by brand
class_type_idstring
Filter by class type
instructor_idstring
Filter by instructor
online_onlyboolean
Only online/hybrid classes

Response example

{
  "data": [
    {
      "id": "uuid",
      "start_time": "2026-09-05T08:00:00Z",
      "end_time": "2026-09-05T09:30:00Z",
      "status": "scheduled",
      "class_type": {
        "id": "uuid",
        "name": "Vinyasa Flow",
        "image_url": "https://example.com/class.jpg"
      },
      "is_bookable": true,
      "requires_workshop_entry": true,
      "workshop_entry_target": {
        "kind": "workshop",
        "id": "uuid",
        "slug": "teacher-training-workshop"
      },
      "course_identifiers": [
        {
          "course_id": "uuid",
          "label": "8W",
          "tone": "warning",
          "course_name": "8-Week Program"
        }
      ]
    }
  ],
  "error": null,
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "has_more": false
  }
}
GET/api/v1/classes/{id}Bearer token

Get a member-visible class instance

Bearer-authenticated exact class detail for Consumer push deep links. Requires X-Organization-ID for an active member relationship, retains public/member-entitled completed or cancelled classes, and never exposes an unlisted class. `class_type.image_url` is the nullable class hero image. Includes the same additive general-policy `requires_workshop_entry` and nullable public `workshop_entry_target` fields as the venue schedule, plus the additive `course_identifiers` pills; `is_bookable` remains capacity/status/time-only. Also carries `cancellation_terms` (CANCEL-PREVIEW-01 v1.1), the class’s effective cancellation window and venue-default fees by the preview’s rule; null when unavailable.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance UUID

Response example

{
  "data": {
    "id": "uuid",
    "status": "scheduled",
    "is_bookable": true,
    "requires_workshop_entry": true,
    "workshop_entry_target": {
      "kind": "course",
      "id": "uuid",
      "slug": "teacher-training-course"
    }
  },
  "error": null
}
GET/api/v1/venues/{slug}/pricingPublic

Get pricing & passes

Active residence price books add residence_commerce with price_book_version, exact prices_minor and enabled countries mapping to currencies. No legal research or provider/reviewer identifiers are public. Invalid configured policy returns an empty unavailable book; an unactivated draft retains ordinary catalog behavior. Display discovery is not a signed purchase quote. All active pass types with pricing tiers, binding commitments, class restrictions, and location availability. Returns an `{ org, pass_types }` envelope (BB-R4): `org` carries `slug`, `name`, `currency`, `timezone`, and `vat_exempt_age_threshold` (for an under-/over-threshold pricing toggle); each `pass_types` entry includes its `slug` for `/buy/{slug}` deep-links. Configurable recurring entries also include `pricing_mode`, billing cadence, the immutable active pricing version, quantity range/step, volume tiers, unlimited option, and change-cycle policy. Every entry carries `registration_fee_u30_amount` / `registration_fee_o30_amount`: the pass's own pre-offer registration fee per age band, priced exactly as the public offers feed prices it (identical when no age split applies). Optional `category`, `location_id`, `brand_id` filters apply to `pass_types`. Each entry's `purchase_channel` (native in-app payment vs web) counts a `pass_type_video_access` live/recording level as a digital grant. Send `X-App-Digital-Content: none` from a store build without in-app digital content: an entry that includes any in-person grant then reads `native`, and a digital-only entry stays `web`. Absent or `full` keeps today's rule (any digital grant means `web`). The shared cache varies on that header; a `none` response is private.

Parameters, scopes and examples

Required scopes

read:pricing

Path parameters

slugstring · required
Venue URL slug

Query parameters

categorystring
Filter by pass category
location_idstring
Filter by location
brand_idstring
Filter by brand

Response example

{
  "data": {
    "org": {
      "slug": "demo-studio",
      "name": "Demo Studio",
      "currency": "DKK",
      "timezone": "Europe/Copenhagen",
      "vat_exempt_age_threshold": 30
    },
    "pass_types": [
      {
        "id": "uuid",
        "name": "Unlimited Monthly",
        "slug": "unlimited-monthly",
        "category": "membership",
        "price_amount": 899,
        "currency": "DKK",
        "is_recurring": true,
        "billing_interval": "month",
        "billing_interval_count": 1,
        "pricing_mode": "flexible_quantity",
        "active_pricing_version_id": "uuid",
        "flexible_pricing_config": {
          "schema_version": 1,
          "minimum_quantity": 1,
          "maximum_quantity": 20,
          "quantity_step": 1,
          "tiers": [
            {
              "up_to": 20,
              "price_per_class": 95
            }
          ],
          "unlimited": {
            "enabled": true,
            "price": 1100
          },
          "change_policy": {
            "member_changes_enabled": true,
            "allowed_cycle_offsets": [
              1,
              2
            ],
            "default_cycle_offset": 1
          }
        }
      }
    ]
  },
  "error": null
}
GET/api/v1/venues/{slug}/offersPublic

Get active offers

The venue's live and scheduled flash-sale offers for one placement. `surface` is required and is one of `pricing_page` (the venue's own website), `global_app` (the Booking Bible consumer app) or `branded_app` (the venue’s own app, plan-gated). An absent or unknown `surface` returns an empty list rather than an error. Optional `brand_id` keeps only products whose pass type is venue-wide or linked to that brand, and drops a sale left with no eligible product. Each product carries the offer and normal prices for both VAT age bands, plus its canonical pre-waiver under-30 / 30+ registration-fee totals and the effective decimal VAT rate when Danish age pricing applies. A configurable (`pricing_mode: "flexible_quantity"`) product additionally carries a `flexible` block with its immutable published price book, the quantity vocabulary and the normal-price range: its scalar `price_amount` / `normal_price_*_amount` describe the CHEAPEST selection, and a product whose active published pricing version is missing or mismatched is omitted rather than quoted at a fallback price. Anonymous; a Bearer JWT is used only to apply the sale’s `exclude_active_pass_clients` rule to that client. Each offer includes `presentation` with `style` (clean, venue, or magic), `showRemainingCapacity`, and `showTimeToEnd`; absent legacy presentation defaults to clean with both visibility flags false. `capacity` contains `maximum`, `claimed`, and `remaining`; maximum and remaining are null for an unbounded offer. Render capacity and deadline only when the corresponding presentation flag is true. These are display values, not reserved inventory or checkout authority. Pass the offer UUID as `flash_sale_id` when requesting the canonical checkout quote. An optional eligibility decision is a provisional buyer-specific preview, or unresolved for an anonymous viewer; it is never admission or a payment guarantee. Canonical checkout preview, intent creation, and settlement revalidate it. Each product carries `purchase_channel` (native | web) from the same caller-aware rule as venue pricing: a `pass_type_video_access` level counts as digital, and with `X-App-Digital-Content: none` a product with any in-person grant is native while a digital-only product stays web. The response varies on that header.

Parameters, scopes and examples

Required scopes

read:pricing

Path parameters

slugstring · required
Venue URL slug

Query parameters

surfacestring · required
Placement: pricing_page | global_app | branded_app
brand_idstring
Keep only offers valid for this brand

Response example

{
  "data": [
    {
      "id": "uuid",
      "venue": {
        "id": "uuid",
        "slug": "demo-studio",
        "name": "Demo Studio"
      },
      "name": "Autumn offer",
      "promo_code": "AUTUMN",
      "presentation": {
        "style": "clean",
        "showRemainingCapacity": false,
        "showTimeToEnd": false
      },
      "capacity": {
        "maximum": 100,
        "claimed": 12,
        "remaining": 88
      },
      "placements": [
        "pricing_page"
      ],
      "status": "active",
      "products": [
        {
          "id": "uuid",
          "name": "Monthly Membership",
          "slug": "monthly-membership",
          "currency": "DKK",
          "is_recurring": true,
          "pricing_mode": "flexible_quantity",
          "price_amount": 169,
          "normal_price_u30_amount": 169,
          "normal_price_o30_amount": 211.25,
          "offer_price_u30_amount": 275,
          "offer_price_o30_amount": 275,
          "registration_fee_amount": 275,
          "registration_fee_u30_amount": 275,
          "registration_fee_o30_amount": 299,
          "vat_rate_effective": 0.25,
          "registration_fee_waived": true,
          "registration_fee_mode": "waive",
          "registration_fee_discount_type": null,
          "registration_fee_discount_value": null,
          "registration_fee_u30_offer_amount": 0,
          "registration_fee_o30_offer_amount": 0,
          "addon_charge_mode": "pro_rate",
          "registration_fee_only": false,
          "flexible": {
            "pricing_version_id": "uuid",
            "kind": "recurring_allowance",
            "selection_required": true,
            "minimum_quantity": 1,
            "maximum_quantity": 12,
            "quantity_step": 1,
            "unlimited_available": true,
            "option_ids": null,
            "normal_price_u30_min": 169,
            "normal_price_u30_max": 999,
            "normal_price_o30_min": 211.25,
            "normal_price_o30_max": 1249
          }
        }
      ]
    }
  ],
  "error": null
}
GET/api/v1/venues/{slug}/offers/{id}Public

Get a public flash-sale offer

Returns one usable published offer with the same product, published Flexible pricing, presentation and capacity fields as the offer list. Invalid IDs and unavailable offers return 404. `surface` defaults to public_link, the direct-link placement; pricing_page, global_app and plan-gated branded_app are also accepted. The route does not accept brand_id. A visible offer or remaining-capacity value does not grant purchase eligibility: the authenticated canonical checkout still validates the buyer, selected product, price version and current offer availability. An optional public eligibility decision is provisional, not final admission. Products carry the same caller-aware `purchase_channel` as the list (`X-App-Digital-Content`); the response varies on that header.

Parameters, scopes and examples

Required scopes

read:pricing

Path parameters

slugstring · required
Venue URL slug
idstring · required
Flash-sale UUID

Query parameters

surfacestring
Placement: public_link | pricing_page | global_app | branded_appDefault: public_link

Response example

{
  "data": {
    "id": "uuid",
    "venue": {
      "id": "uuid",
      "slug": "demo-studio",
      "name": "Demo Studio"
    },
    "name": "Autumn offer",
    "presentation": {
      "style": "clean",
      "showRemainingCapacity": false,
      "showTimeToEnd": false
    },
    "capacity": {
      "maximum": null,
      "claimed": 12,
      "remaining": null
    }
  },
  "error": null
}
GET/api/v1/venues/{slug}/classesPublic

Get class types

Class catalog with descriptions, difficulty levels, durations, and included services.

Parameters, scopes and examples

Required scopes

read:classes

Path parameters

slugstring · required
Venue URL slug
GET/api/v1/venues/{slug}/instructorsPublic

Get instructors

Instructor profiles with bios, photos, and specialties.

Parameters, scopes and examples

Required scopes

read:instructors

Path parameters

slugstring · required
Venue URL slug
GET/api/v1/venues/{slug}/locationsPublic

Get locations

Physical locations with rooms, capacity, opening hours, amenities, and Google Maps integration.

Parameters, scopes and examples

Required scopes

read:locations

Path parameters

slugstring · required
Venue URL slug
GET/api/v1/venues/{slug}/brandsPublic

List brands

Active brands at this venue. Each entry includes identity (name, slug, description), theming (colors, logo, hero), social links, and a class_types_count for quick summary rendering.

Parameters, scopes and examples

Required scopes

read:brands

Path parameters

slugstring · required
Venue URL slug
GET/api/v1/venues/{slug}/brands/{brandSlug}Public

Get brand detail

Full brand record plus class_types tagged to this brand and the pass_types available for it (respecting pass_type_brands restrictions — passes with no brand-junction rows are venue-wide and are included).

Parameters, scopes and examples

Required scopes

read:brands

Path parameters

slugstring · required
Venue URL slug
brandSlugstring · required
Brand slug within the venue
GET/api/v1/geoPublic

Geo prefill for the signup form

Anon utility that reads Vercel's request-geo headers (`x-vercel-ip-country`/`x-vercel-ip-city`) so a client can prefill signup's optional `country`/`city` fields from the caller's own IP before submitting POST /api/v1/auth/signup. `country` is an ISO 3166-1 alpha-2 code; `city` is URI-decoded free text. Either is `null` when the header is absent (e.g. local dev). No DB touch; never cached (per-caller response).

Parameters, scopes and examples

Response example

{
  "data": {
    "country": "DK",
    "city": "Copenhagen"
  },
  "error": null
}
GET/api/v1/venues/{slug}/gift-cards/{code}/balancePublic

Public gift-card balance by code

PUBLIC (no auth) balance lookup for a venue gift card, for a storefront "check your balance" widget. Org resolved from {slug}; lookup scoped to that org's cards by the FULL generated code OR the printed physical barcode (same fallback `redeemGiftCard` uses). Returns the minimal `{ code, remaining_amount, currency, status, expires_at }` — never purchaser/recipient PII. Enumeration-hardened: an unknown code, a cross-org code under the wrong slug, and a cancelled card all return the SAME generic 404. IP rate-limited (20/min).

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug
codestring · required
Full gift-card code or the printed physical barcode

Response example

{
  "data": {
    "code": "GIFT-XXXX",
    "remaining_amount": 350,
    "currency": "DKK",
    "status": "active",
    "expires_at": null
  },
  "error": null
}
GET/api/v1/venues/{slug}/gift-cards/{code}/previewPublic

Public gift-card preview by code

PUBLIC (no auth) gift-card preview so a BRANDED storefront can show a recipient what they were gifted ("Alex sent you a 3-month membership") before prompting signup/redeem — instead of bouncing them to BB's /gift/redeem/[code] venue portal. Accepts the generated code OR the printed physical barcode. Returns `{ code, gift_type, sender_name, gift_description, pass_name, amount, currency, status, expires_at }` — the sender's display name + a human gift description only, NEVER recipient/purchaser contact info, the personal message, or the redeemer. Enumeration-hardened: unknown code, wrong slug, and a cancelled card all return the SAME generic 404. IP rate-limited (20/min). Redeem itself is member-authenticated (POST /api/v1/gift-cards/redeem).

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug
codestring · required
Full gift-card code or the printed physical barcode

Response example

{
  "data": {
    "code": "GIFT-XXXX",
    "gift_type": "pass",
    "sender_name": "Alex",
    "gift_description": "Unlimited Monthly (3 months)",
    "pass_name": "Unlimited Monthly",
    "amount": 1500,
    "currency": "DKK",
    "status": "active",
    "expires_at": null
  },
  "error": null
}
POST/api/v1/venues/{slug}/inquiriesBearer token

Submit a booking inquiry

Member-authenticated request-a-booking for a service that accepts inquiries (`accepts_inquiries: true` on the public services list). Free-text preferred time, not a real slot — the venue converts it to a real appointment once a time is agreed. `Idempotency-Key` is optional but MUST be a UUID when sent (400 `INVALID_IDEMPOTENCY_KEY` otherwise); it is the database ingest request id. Every 201 and every 503 `INQUIRY_RECONCILE_FAILED` returns an `Idempotency-Key` RESPONSE header (mirrored as `error.details.request_id` on the 503) carrying the identity the inquiry was accepted under — your key when you sent one, the server-generated UUID when you did not. Retry a 503 with that exact value as `Idempotency-Key`: it replays the accepted inquiry (no second row) and re-drives only what is still missing. Retrying without it mints a new identity and files a duplicate inquiry. Rate-limited 10/min. Errors: 503 INQUIRIES_DISABLED (kill switch off), 404 NOT_FOUND (venue), 422 SERVICE_NOT_ACCEPTING_INQUIRIES, 422 VALIDATION_FAILED, 409 IDEMPOTENCY_KEY_REUSE_MISMATCH (same key, different answers — nothing written), 500 SUBMIT_FAILED (`details.reason` passthrough).

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug

Inquiry details

Request body

{
  "service_id": "uuid",
  "preferred_time": "Tuesdays or Thursdays after 17:00",
  "message": "Looking for a 90-minute deep tissue session.",
  "contact_name": "Jane Doe",
  "contact_email": "jane@example.com",
  "contact_phone": "+4520123456"
}

Response example

{
  "data": {
    "id": "uuid"
  },
  "error": null
}
GET/api/v1/me/inquiriesBearer token

My booking inquiries

The caller's own booking inquiries across every venue, newest first. Status is mapped to plain language (never the raw form_submissions enum): 'Sent — waiting for the venue', 'The venue replied', or 'Closed'; archived/spam/deleted rows are never returned.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "venue": {
        "slug": "hot-yoga-cph",
        "name": "Hot Yoga Copenhagen"
      },
      "service_name": "Deep Tissue Massage",
      "preferred_time": "Tuesdays or Thursdays after 17:00",
      "status": "Sent — waiting for the venue",
      "status_key": "sent",
      "created_at": "2026-08-10T12:00:00Z"
    }
  ],
  "error": null
}
GET/api/v1/venues/{slug}/addonsPublic

Booking add-on catalog

Products surfaced as booking add-ons (show_at_booking), filtered by the venue's per-category toggle and grouped by category. Public.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue URL slug

Response example

{
  "data": {
    "addons": [
      {
        "id": "uuid",
        "name": "Bottled Water",
        "price": 25,
        "currency": "DKK",
        "category_id": "uuid"
      }
    ],
    "categories": [
      {
        "category_id": "uuid",
        "category_name": "Water & Drinks",
        "addons": []
      }
    ],
    "enabled_category_ids": []
  }
}
Authentication24 documented operations
POST/api/v1/auth/loginPublic

Login

Authenticate with email and password. Returns JWT access token and refresh token. Invalid credentials return 401 INVALID_CREDENTIALS; provider outage or timeout returns 503 AUTH_UNAVAILABLE; rate limits return 429 RATE_LIMITED. Provider failures do not trigger imported-account recovery.

Parameters, scopes and examples

Credentials

Request body

{
  "email": "user@example.com",
  "password": "password123"
}

Response example

{
  "data": {
    "session": {
      "access_token": "eyJ...",
      "refresh_token": "xxx",
      "expires_at": 1234567890
    },
    "user": {
      "id": "uuid",
      "email": "user@example.com",
      "first_name": "John"
    }
  }
}
POST/api/v1/auth/oauth/id-tokenPublic

Social login

Exchange a Google or Apple identity token for a Booking Bible session. Existing email/password accounts are unchanged. New accounts require a venue (organization_id or organization_slug). Unclaimed imported emails are not auto-linked.

Parameters, scopes and examples

Provider identity token

Request body

{
  "provider": "google",
  "id_token": "eyJ...",
  "organization_id": "uuid"
}
POST/api/v1/auth/signupPublic

Register

Create account and optionally join a venue. Supports referral codes and UTM tracking.

Parameters, scopes and examples

Registration

Request body

{
  "email": "user@example.com",
  "password": "password123",
  "first_name": "John",
  "last_name": "Doe",
  "organization_slug": "acme-studio"
}
POST/api/v1/auth/signup/confirmPublic

Confirm partner signup

Exchange a signup confirmation token for a session on an explicitly owner-configured partner receiver for a validated organization and brand. Requires signed signup provenance, active tenant membership, password and MFA checks. Never accepts recovery or mobile magic-link credentials. Rate-limited 5/10 min per IP.

Parameters, scopes and examples

Partner signup confirmation

Request body

{
  "token_hash": "confirmation-token-from-email",
  "organization_slug": "acme-studio",
  "brand_id": "11111111-1111-4111-8111-111111111111"
}
POST/api/v1/auth/magic-linkPublic

Magic link

Send a passwordless login link. Mobile requests require an S256 device challenge and use a verified HTTPS callback.

Parameters, scopes and examples

Magic link request

Request body

{
  "email": "user@example.com",
  "code_challenge": "DwBzhbb51LfusnSGBa_hqYSgo7-j8BTQnip4TOnlzRo",
  "code_challenge_method": "S256"
}
POST/api/v1/auth/magic-link/verifyPublic

Verify magic link

Exchange a device-bound mobile token_hash or a 6-digit email OTP for a session. Mobile token hashes require the callback request_id and device-held code_verifier. Rate-limited 5/10 min per IP.

Parameters, scopes and examples

Bound mobile token hash or email OTP

Request body

{
  "token_hash": "token-hash-from-url",
  "type": "email",
  "request_id": "11111111-1111-4111-8111-111111111111",
  "code_verifier": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}

Response example

{
  "data": {
    "session": {
      "access_token": "jwt",
      "refresh_token": "rt",
      "expires_at": 1234567890,
      "token_type": "bearer"
    },
    "user": {
      "id": "uuid",
      "email": "user@example.com",
      "active_organization_id": "uuid"
    }
  }
}
POST/api/v1/auth/signup/precheck-emailPublic

Signup email precheck

Check whether an email already has an account before signup. Returns a state hint (absent | active | password_never_used | imported_unclaimed | unknown). Rate-limited 20/min per IP. Constant-time floor of 250ms to prevent enumeration.

Parameters, scopes and examples

Email to check

Request body

{
  "email": "user@example.com"
}

Response example

{
  "data": {
    "state": "absent"
  }
}
GET/api/v1/auth/mfa/factorsBearer token

List MFA factors

List the caller's enrolled TOTP MFA factors.

Parameters, scopes and examples

Response example

{
  "data": {
    "factors": [
      {
        "id": "uuid",
        "friendly_name": "Authenticator App",
        "factor_type": "totp",
        "status": "verified",
        "created_at": ""
      }
    ],
    "count": 1
  }
}
POST/api/v1/auth/mfa/enrollBearer token

Begin MFA enrollment

Start TOTP enrollment. Returns factor_id, QR code (SVG data URI), and raw secret. Factor is unverified until /mfa/verify confirms a valid code.

Parameters, scopes and examples

Friendly name for the authenticator device

Request body

{
  "friendly_name": "Authenticator App"
}

Response example

{
  "data": {
    "factor_id": "uuid",
    "qr_code": "data:image/svg+xml,...",
    "secret": "ABCDEF...",
    "uri": "otpauth://totp/..."
  }
}
POST/api/v1/auth/mfa/challengeBearer token

Mint MFA challenge

Create a TOTP challenge for one of the caller's factors. Returns {id, expires_at}; pass the id as challenge_id to /auth/mfa/verify. Repeating mints a fresh challenge (intended resend). Rate-limited 5/min per user.

Parameters, scopes and examples

Factor to challenge

Request body

{
  "factor_id": "uuid"
}

Response example

{
  "data": {
    "id": "uuid",
    "challenge_id": "uuid",
    "expires_at": 1764950400
  }
}
POST/api/v1/auth/mfa/verifyBearer token

Verify MFA code

Verify a 6-digit TOTP code (enrollment confirmation or login challenge). Accepts {type: totp, factor_id, code}, {type: backup_code, code}, or the challenge-bound {challenge_id, code} (factor resolved from the preceding /mfa/challenge). Rate-limited 5/5 min per user.

Parameters, scopes and examples

TOTP code or backup code

Request body

{
  "type": "totp",
  "factor_id": "uuid",
  "code": "123456"
}

Response example

{
  "data": {
    "verified": true
  }
}
DELETE/api/v1/auth/mfa/unenrollBearer token

Remove MFA factor

Unenroll an MFA factor for the caller.

Parameters, scopes and examples

Factor to remove

Request body

{
  "factor_id": "uuid"
}

Response example

{
  "data": {
    "removed": true,
    "factor_id": "uuid"
  }
}
POST/api/v1/auth/mfa/backup-codesBearer token

Generate MFA backup codes

Generate 10 single-use backup codes for MFA recovery. Previous unused codes are invalidated. Codes are shown once in plaintext — only hashes are stored.

Parameters, scopes and examples

Response example

{
  "data": {
    "backup_codes": [
      "ABCD1234EF",
      "..."
    ],
    "warning": "Save these codes securely. They will not be shown again.",
    "count": 10
  }
}
POST/api/v1/auth/mfa/resetBearer or API key

Admin MFA reset

Admin-initiated MFA reset for a user. Unenrolls all factors and invalidates backup codes. Permission: admin.users.manage.

Parameters, scopes and examples

Required scopes

write:users

Target user + reason

Request body

{
  "user_id": "uuid",
  "reason": "Lost authenticator app"
}

Response example

{
  "data": {
    "user_id": "uuid",
    "factors_removed": 2,
    "backup_codes_invalidated": true
  }
}
POST/api/v1/auth/refreshPublic

Refresh token

Exchange refresh token for new access and refresh tokens.

Parameters, scopes and examples

Refresh token

Request body

{
  "refresh_token": "xxx"
}
POST/api/v1/auth/logoutBearer token

Log out current session

Revoke the refreshable Supabase session represented by the caller JWT. The access JWT remains valid until its encoded expiry.

Parameters, scopes and examples

Response example

{
  "data": {
    "ok": true
  }
}
POST/api/v1/auth/password/forgotPublic

Forgot password

Send a single-use 6-digit password verification code. Honours venue branding when an org is identified and never reveals whether the email exists. No reset link is generated.

Parameters, scopes and examples

Recovery request

Request body

{
  "email": "user@example.com",
  "organization_slug": "yoga-bible"
}
POST/api/v1/auth/password/resetPublic

Reset password

Verify the single-use numeric code and permanently save a new password in one operation. Recovery tokens and reset links are not accepted. When MFA is required or assurance cannot be checked, success returns session:null and sign_in_required:true; mfa_required:true identifies a verified MFA requirement. Sign in normally with the saved password to complete verification.

Parameters, scopes and examples

Reset request

Request body

{
  "email": "user@example.com",
  "code": "482913",
  "new_password": "a-strong-new-password"
}

Response example

{
  "data": {
    "message": "Your password has been saved permanently.",
    "session": {
      "access_token": "jwt",
      "refresh_token": "refresh-token",
      "token_type": "bearer"
    }
  }
}
POST/api/v1/me/passwordBearer token

Change my password

Authenticated password change that still requires a fresh single-use numeric code. Submit the code and new password together; current-password-only and session-only changes are rejected. A successful save can return session:null with sign_in_required:true (and mfa_required:true when verified); normal sign-in must satisfy configured MFA before a recovery session is released.

Parameters, scopes and examples

Code-verified password change

Request body

{
  "code": "482913",
  "new_password": "a-strong-new-password"
}

Response example

{
  "data": {
    "message": "Your password has been saved permanently.",
    "session": {
      "access_token": "jwt",
      "refresh_token": "refresh-token",
      "token_type": "bearer"
    }
  }
}
POST/api/v1/auth/handoff-codeBearer token

Mint SSO handoff code

NAMASTE-GATES-01 — mint a one-time SSO handoff code (60s TTL, single-use, SHA-256 hashed at rest) bound to an allowlisted destination domain. The destination site exchanges it at /auth/handoff-exchange for a fresh session.

Parameters, scopes and examples

Destination domain (allowlist: namasteonline.com, namasteoffline.com, namastestudios.dk [redirect period], localhost)

Request body

{
  "target_domain": "namasteoffline.com"
}

Response example

{
  "data": {
    "code": "base64url-code",
    "expires_at": "2026-07-11T10:00:60Z",
    "target_domain": "namasteoffline.com"
  }
}
POST/api/v1/auth/handoff-exchangePublic

Exchange SSO handoff code

NAMASTE-GATES-01 — consume a one-time handoff code (atomic single-use) and receive a fresh Supabase session for the bound user. Same session shape as /auth/login.

Parameters, scopes and examples

The one-time code

Request body

{
  "code": "base64url-code"
}

Response example

{
  "data": {
    "session": {
      "access_token": "jwt",
      "refresh_token": "xxx",
      "expires_at": 1780000000,
      "token_type": "bearer"
    },
    "user": {
      "id": "uuid",
      "email": "user@example.com"
    },
    "target_domain": "namasteoffline.com"
  }
}
POST/api/v1/auth/device/codePublic

Mint TV device code pair

TV-DEVICE-AUTH-01 — RFC 8628-style device authorization (mint side). An input-constrained device (TV) receives a 256-bit device_code (its poll credential) plus a short user_code (shown as XXXX-XXXX + QR). Both are SHA-256 hashed at rest, bound to one 10-minute expiry, single-use.

Parameters, scopes and examples

Response example

{
  "data": {
    "device_code": "base64url-device-code",
    "user_code": "ABCD-EFGH",
    "verification_uri": "https://namasteonline.com/tv",
    "verification_uri_complete": "https://namasteonline.com/tv?code=ABCD-EFGH",
    "expires_in": 600,
    "interval": 5
  }
}
POST/api/v1/auth/device/approveBearer token

Approve TV device sign-in

TV-DEVICE-AUTH-01 — a SIGNED-IN member submits the user_code shown on the TV (normalized: uppercase, dashes/spaces stripped). Binds the pending device code to the caller so the TV poll returns a session. Every failure (unknown / expired / attempts-capped) is the same generic 400 INVALID_CODE; per-code attempts<5 cap.

Parameters, scopes and examples

The short code shown on the TV

Request body

{
  "user_code": "ABCD-EFGH"
}

Response example

{
  "data": {
    "approved": true
  }
}
POST/api/v1/auth/device/tokenPublic

Poll TV device code for session

TV-DEVICE-AUTH-01 — the TV polls with its device_code (every `interval` seconds). 400 AUTHORIZATION_PENDING until approved; 400 EXPIRED_TOKEN / 403 ACCESS_DENIED / 400 INVALID_CODE are terminal. On approval the code is consumed atomically (single-use) and a fresh Supabase session is returned — same shape as /auth/login.

Parameters, scopes and examples

The TV's device_code

Request body

{
  "device_code": "base64url-device-code"
}

Response example

{
  "data": {
    "session": {
      "access_token": "jwt",
      "refresh_token": "xxx",
      "expires_at": 1780000000,
      "token_type": "bearer"
    },
    "user": {
      "id": "uuid",
      "email": "user@example.com"
    }
  }
}
Passes31 documented operations
GET/api/v1/passesBearer or API key

My passes

List active passes across all venues with usage stats. Each pass carries is_default (the member's chosen default at that venue), usable and unusable_reason (clips_exhausted | expired | paused | past_due | not_eligible | not_found, evaluated on the venue-local today; a renewing membership whose current cycle is spent stays usable; class-specific restrictions are decided at booking time).

Parameters, scopes and examples

Required scopes

read:bookings
GET/api/v1/passes/{id}Bearer token

Pass detail

Single pass detail with usage stats, freeze/binding state, configurable recurring selection/allowance, and the pass-type + venue. Owner-scoped: cross-user reads return 404. FLEX-PARITY-01 — a top-level `allowance` block ({kind, quantity, class_allowance, interval, interval_count, label}) names the purchased tier of a Flexible (configurable-quantity) membership, e.g. "1 class / month"; null for a fixed pass. `pass_type.slug`/`pricing_mode` are additive.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Response example

{
  "data": {
    "id": "uuid",
    "status": "active",
    "clips_remaining": 8,
    "allowance": {
      "kind": "quantity",
      "quantity": 1,
      "class_allowance": 1,
      "interval": "month",
      "interval_count": 1,
      "label": "1 class / month"
    },
    "pass_type": {
      "id": "uuid",
      "name": "Flexible Membership",
      "slug": "flexible-membership",
      "pricing_mode": "flexible_quantity"
    }
  },
  "error": null
}
GET/api/v1/me/passes/{id}/configurationBearer token

Get my Flexible membership configuration

Returns the authenticated member’s frozen selection, current published recurring choices, pricing version, and any pending renewal change. Owner-scoped and available only when member changes are enabled by the venue.

Parameters, scopes and examples

Path parameters

idstring · required
Issued pass UUID
POST/api/v1/me/passes/{id}/configurationBearer token

Preview or schedule my Flexible membership change

Preview a venue-approved quantity/unlimited change for renewal 1–24, or schedule it with the exact unexpired quote fingerprint. The change takes effect only after the target renewal invoice is paid.

Parameters, scopes and examples

Path parameters

idstring · required
Issued pass UUID

Preview or schedule a future allowance change

Request body

{
  "action": "preview",
  "selection": {
    "kind": "quantity",
    "quantity": 8
  },
  "effective_cycle_offset": 1
}
DELETE/api/v1/me/passes/{id}/configurationBearer token

Cancel my pending Flexible membership change

Cancels the authenticated member’s pending future allowance change without altering the current frozen entitlement.

Parameters, scopes and examples

Path parameters

idstring · required
Issued pass UUID
GET/api/v1/me/passes/{id}/addonsBearer token

Get add-ons for my Flexible membership

Returns the active, pending and currently eligible recurring extras for an authenticated member’s active monthly Flexible pass, with server-priced monthly amounts.

Parameters, scopes and examples

Path parameters

idstring · required
Issued pass UUID
POST/api/v1/me/passes/{id}/addonsBearer token

Preview or buy a recurring Flexible add-on

Preview the Stripe-calculated charge through the next membership renewal, then buy with that quote and an Idempotency-Key. The extra starts after a confirmed paid invoice and renews with the membership. Pending or scheduled subscription changes are refused.

Parameters, scopes and examples

Path parameters

idstring · required
Issued pass UUID

Preview or purchase one published extra

Request body

{
  "action": "preview",
  "key": "shower_facilities"
}
GET/api/v1/pass-types/{id}Public

Pass-type catalog detail

Pass-type catalog detail used by checkout — price, duration, benefits, binding tiers, eligible class types, configurable kind, and the immutable published Flexible pass configuration when enabled. Active public items need no authentication; a hidden exhausted-credit target requires member JWT authentication plus its clips_empty_offer_id capability. `purchase_channel` follows the same caller-aware rule as venue pricing: a video-access level counts as a digital grant, and `X-App-Digital-Content: none` makes an entry with any in-person grant `native` while a digital-only entry stays `web`.

Parameters, scopes and examples

Path parameters

idstring · required
Pass-type id

Query parameters

clips_empty_offer_idstring
Opaque clips-empty path UUID. Revalidated against the authenticated member’s exhausted, still-valid source pass before a hidden target is returned.

Response example

{
  "data": {
    "id": "uuid",
    "name": "10-clip card",
    "price_amount": 1299,
    "currency": "DKK",
    "category": "clip_card"
  },
  "error": null
}
POST/api/v1/passes/purchaseBearer token

Purchase pass

The existing compatibility policy still rejects direct flash sales for Flexible class packs and time passes; one-time direct disclosure applies to supported fixed products only. Direct Flexible proofs also carry purchase_obligation_fingerprint: preserve it unchanged. The canonical preview reports purchase_obligation availability and all-tender minimum_total_payable. Supported recurring schedules expose intro_through_date and either reachable self-service earliest_cancellation_effective_on or a distinct cancellation {kind:conditional_contractual_minimum, request_method:contact_studio, condition:timely_valid_notice, earliest_possible_end_on}. Staff-managed facts are conditional on timely valid notice, not cancellation confirmation. Render this distinction before acknowledgement. One-time totals come from final canonical pricing and omit cancellation dates. Unsupported shapes report unavailable without guessed facts. These are purchase-time disclosures, not a replacement for existing live cancellation policy. Initiate pass purchase. Returns a PaymentIntent or SetupIntent client_secret (`client_secret_type` identifies which) with the provider-frozen customer, ephemeral key, Connect account, merchant country, and regional revision. Customer credentials are paired and may both be null only for a supported generic-sheet/no-customer result. Optional binding_months must identify a current server-side tier and is priced by the same canonical resolver as checkout preview; unavailable tiers return 422 rather than falling back. Flexible recurring passes require selection_kind=quantity with quantity, or selection_kind=unlimited; Flexible class/time passes require selection_kind=option with option_id. A recurring Flexible pass accepts only a Flash Sale backed by one shared introductory amount, which applies regardless of the chosen allowance. Fixed passes retain promo codes, gift cards, and account credits. Optional flash_sale_id (UUID) selects a direct venue offer and cannot be combined with promo_code. Offer price, linked promotion, dates, buyer eligibility, caps and compatible passes are server-authoritative; unavailable offers return 422 FLASH_SALE_UNAVAILABLE without charging normal price. Flexible purchases must send the complete preview flexible_quote, including its optional flash_sale_id and flash_sale_rule_fingerprint; stale or changed quotes return a typed QUOTE_* error. Reuse the same Idempotency-Key only for the same purchase choices. The pricing response remains canonical: charged_today is payable; optional direct_offer {flash_sale_id, name, normal_amount, offer_amount} is display-only fixed one-time metadata in minor units. Existing requests without flash_sale_id retain generic checkout behavior. When an offer applies, the response also carries hold_expires_at (ISO 8601 UTC instant the reservation lapses, or null when no offer hold applies) and server_time (ISO 8601 UTC, the server clock at response time), always together. Compute the countdown once as hold_expires_at minus server_time and run it locally; never compare hold_expires_at against the device clock. A lapsed hold that can still be retried returns 409 OFFER_HOLD_EXPIRED; a lapsed hold with no way to restart (capacity gone, the per-client limit reached, or a late charge already refunded) returns 409 OFFER_CONTACT_VENUE, which never offers a restart and instead points the buyer to the venue desk. A first attempt with no reservation ever held, against an offer that is already fully booked, returns 409 OFFER_SOLD_OUT with plain copy and no charge attempted — never the raw promo-code refusal reason. A canonical customer eligibility refusal or unresolved decision returns 409 CUSTOMER_ELIGIBILITY with a safe decision in error.details.eligibility; this is final admission for this purchase attempt, unlike a provisional public preview. Show the reason in EN/DA and current options at the trusted venue. This route mints a native in-app Stripe payment, so a pass type whose catalog purchase_channel is web (any digital entitlement: online classes, course, video library, digital materials) is refused before any charge work with 409 PURCHASE_CHANNEL_WEB_ONLY and error.details {purchase_channel: 'web'}; send the buyer to the venue website instead (App Store 3.1.1 / Google Play payments). The channel is the one the catalog serves to the same caller: a `pass_type_video_access` level counts as digital, and a request with `X-App-Digital-Content: none` (store build without in-app digital content) may buy a pass with any in-person grant; digital-only passes stay refused.

Parameters, scopes and examples

Purchase. Optional flash_sale_id: UUID (mutually exclusive with promo_code). Optional flexible_quote: unchanged preview quote; required for Flexible purchases. Direct-offer quote authority includes flash_sale_id and flash_sale_rule_fingerprint. Fixed direct-offer purchases require direct_purchase_quote: the unchanged five-minute signed preview proof {version:1, organization_id, user_id, pass_type_id, flash_sale_id, authority_fingerprint, credit_applied_minor, payable_minor, issued_at, expires_at, fingerprint}. Both tender fields are nonnegative integer minor units; payable_minor equals preview charged_today. Before new payment work, the canonical engine verifies the reviewed price, terms and exact account-credit/cash split. Changed balances cannot silently increase the payment. Direct gift_card_code is rejected unless equivalently quoted; generic gift cards are unchanged. Missing, invalid, expired or changed proof returns QUOTE_REQUIRED, QUOTE_INVALID, QUOTE_EXPIRED or QUOTE_STALE (409 at adapter preflight, 422 at native core); an already committed matching operation retains its frozen result.

Request body

{
  "pass_type_id": "uuid",
  "organization_id": "uuid",
  "binding_months": 12,
  "selection_kind": "quantity",
  "quantity": 7
}

Response example

{
  "data": {
    "client_secret": "pi_xxx_secret_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4",
    "amount": 1299,
    "currency": "DKK",
    "breakdown": {
      "base": 1499,
      "binding_discount": -100,
      "promo": -50,
      "credits": -50,
      "total": 1299
    },
    "hold_expires_at": "2026-09-15T09:15:34.000Z",
    "server_time": "2026-09-15T09:12:34.000Z"
  }
}
POST/api/v1/gift-cards/redeemBearer token

Redeem a gift card

MEMBER (JWT) redeem endpoint so a branded storefront can host the whole redeem flow on its own domain. Applies the gift `{ code }` — generated code OR printed physical barcode — to the caller's account: a custom-amount gift credits the balance (`{ type:"credit", amount, newBalance }`); a pass gift creates + activates a pass (`{ type:"pass", passId }`). Atomic SELECT FOR UPDATE claim — two concurrent calls can never both redeem. A logged-out recipient must sign up / log in first (that creates/links the BB member); this endpoint is member-only by design. Org from the caller's active membership (X-Organization-ID header or single membership). Errors: 401 UNAUTHORIZED, 403 NO_ORG / MODULE_DISABLED, 400 VALIDATION_ERROR, 404 INVALID_CODE, 409 ALREADY_REDEEMED / EXPIRED / NOT_AVAILABLE, 500 REDEEM_FAILED.

Parameters, scopes and examples

Gift card code to redeem onto the caller's account

Request body

{
  "code": "YB-GIFT-AB12"
}

Response example

{
  "data": {
    "type": "credit",
    "amount": 500,
    "newBalance": 500
  },
  "error": null
}
GET/api/v1/gift-cards/purchasesBearer token

My gift purchase history

Authenticated buyer-only gift purchases, scoped to the active organization. Historical access remains when new gift sales are disabled. Optional page (positive integer, default 1) returns up to 50 records ordered newest first: {id, amount, currency, status, paid, code, purchased_at, recipient_name, message, expires_at, delivery_method, scheduled_send_at, delivery}. Delivery is a channel-to-status map from durable provider evidence: accepted is not delivered, failed includes a verified bounce. Code is null until webhook-confirmed payment. Recipient email, buyer identity and public bearer tokens are never returned. Private no-store response.

Parameters, scopes and examples

Query parameters

pageinteger
Positive page number, default 1. Each page contains up to 50 purchases.
GET/api/v1/gift-cardsBearer token

My gift cards

Gift cards the caller purchased or received (buyer, redeemer, or addressed recipient email). Without scope=account, scoped to the active org and returning an array. With scope=account, returns `{cards, failed_organization_ids}` across current and former venues; each card includes organization_id and organization_name. A branded x-organization-slug narrows the account read to that venue. Cards include `{id, code, initial_amount, balance, currency, status, recipient_email, recipient_name, message, expires_at, created_at}` with status `active|redeemed|expired|void`. Owned history stays readable when new gift-card sales are disabled.

Parameters, scopes and examples

Query parameters

scopestring
Use account for an owner-scoped cross-venue wallet; omit for the legacy active-venue array.
POST/api/v1/gift-cards/purchaseBearer token

Purchase gift card

Buy a gift card (custom amount or a gifted pass) for a recipient. Creates a Stripe one-time PaymentIntent and returns client_secret plus customer_id + ephemeral_key for the Stripe Payment Sheet. Fixed gifts retain their existing contract. A published Flexible pass is available only when is_giftable is not false and flexible_gift_supported is true; flexible_gift_funding_modes currently supports prepaid only. Send preview:true with the published flexible_selection, optional flexible_addons and prepaid duration to receive the canonical unknown-recipient gross amount (this endpoint reports amount in major units), flexible_gift disclosure and an opaque flexible_quote without creating a provider intent or gift card. The explicit purchase must echo that flexible_quote unchanged. The recipient authenticates when redeeming the gift, and the prepaid pass starts then; it does not auto-renew or charge the recipient for the funded period. Gated on the gift_cards module. Idempotency-Key supported.

Parameters, scopes and examples

Gift card purchase. Additive Flexible fields: preview?: boolean; flexible_selection?: {kind:"quantity",quantity:number}|{kind:"unlimited"}|{kind:"option",optionId:string}; flexible_addons?: Array<{key:string,quantity:number}>; flexible_quote?: opaque server-signed preview object echoed exactly. Existing fixed-gift inputs and behavior are unchanged.

Request body

{
  "gift_type": "pass",
  "organization_id": "uuid",
  "pass_type_id": "uuid",
  "duration_months": 3,
  "preview": true,
  "flexible_selection": {
    "kind": "quantity",
    "quantity": 8
  },
  "flexible_addons": [
    {
      "key": "towel_service",
      "quantity": 1
    }
  ],
  "sender_name": "Alex",
  "recipient_name": "Jordan",
  "recipient_email": "jordan@example.com",
  "delivery_method": "email",
  "personal_message": "Enjoy a class on me!"
}

Response example

{
  "data": {
    "gift_card_id": "uuid",
    "code": "YB-GIFT-AB12",
    "client_secret": "pi_xxx_secret_xxx",
    "amount": 500,
    "currency": "DKK",
    "gift_type": "custom_amount",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx"
  }
}
POST/api/v1/checkout/giftcard/payment-intentPublic

Start native gift card checkout

Anonymous (or logged-in) native gift-card checkout — phase 1. Mints a Stripe PaymentIntent for a gift card and returns client_secret so the buyer can mount Stripe Elements in a modal. Two gift kinds (exactly one of the two fields): a CUSTOM-AMOUNT gift via `amount` (smallest currency unit, min 5000, max 5000000), or a PASS-BASED gift via `pass_type_id` (GIFT-PASS-API-01 — must be an active, giftable, non-intro pass type of this org; price is server-resolved via calculateGiftPrice, optional `duration_months` 1–120 prepays a recurring membership). Fixed gifts retain their existing contract. A published Flexible pass is available only when is_giftable is not false and flexible_gift_supported is true; flexible_gift_funding_modes currently supports prepaid only. Send preview:true with the published flexible_selection, optional flexible_addons and prepaid duration to receive the canonical unknown-recipient gross amount in minor units, flexible_gift disclosure and an opaque flexible_quote without creating a provider intent or gift card. The explicit purchase must echo that flexible_quote unchanged. The recipient authenticates when redeeming the gift, and the prepaid pass starts then; it does not auto-renew or charge the recipient for the funded period. The gift_cards row is created only on confirm, so an abandoned payment leaves no orphan. Anonymous callers must pass a Turnstile token. VAT is accounted at redemption (multi-purpose voucher) so vat_amount is 0. Gated on the gift_cards module. Rate-limited 10/min.

Parameters, scopes and examples

Gift card checkout. Additive Flexible fields: preview?: boolean; flexible_selection?: {kind:"quantity",quantity:number}|{kind:"unlimited"}|{kind:"option",optionId:string}; flexible_addons?: Array<{key:string,quantity:number}>; flexible_quote?: opaque server-signed preview object echoed exactly. Existing fixed-gift inputs and behavior are unchanged.

Request body

{
  "organization_slug": "hot-yoga-cph",
  "pass_type_id": "uuid",
  "duration_months": 3,
  "preview": true,
  "flexible_selection": {
    "kind": "quantity",
    "quantity": 8
  },
  "flexible_addons": [
    {
      "key": "towel_service",
      "quantity": 1
    }
  ],
  "recipient_email": "jordan@example.com",
  "recipient_name": "Jordan",
  "sender_name": "Alex",
  "message": "Enjoy a class on me!",
  "giver_email": "alex@example.com"
}

Response example

{
  "data": {
    "client_secret": "pi_xxx_secret_xxx",
    "payment_intent_id": "pi_xxx",
    "amount": 50000,
    "currency": "DKK",
    "vat_amount": 0,
    "gift_type": "custom_amount"
  }
}
POST/api/v1/checkout/giftcard/confirmPublic

Confirm native gift card checkout

The sender-enabled source uses SQL93 durable recipient authority; source readiness is not a deployment or provider-delivery claim. Recipient email/SMS requires positively identified Vercel production runtime; preview, development, missing or mismatched identity is held. The 423 maintenance contract below also applies if the same-schema recipient pause is restored. Native gift-card checkout — phase 2. Finalizes the original PaymentIntent gift (custom-amount or pass-based), canonical accounting and buyer receipt. Temporary recipient maintenance returns 423 GIFT_RECIPIENT_DELIVERY_HELD with data:null,error:{code,message} when immediate delivery is held. Payment/gift may already be durably confirmed: preserve the original intent, show held, never pay again and never claim sent or activated. A scheduled gift may return200 before its scheduled delivery is held;200 is not proof of recipient delivery. Idempotent on the original intent; ordinary success returns the last4 code, masked recipient email, gift_type and separate durable delivery channel statuses when available. Provider accepted is not delivered; a missing status stays unknown. organization_slug is recommended for direct-charge venues. The authenticated recurring SetupIntent branch preserves the original giver/account/card and uses the same held contract without claiming a payment was taken. Its held error.details adds giver_receipt_state (succeeded, busy or manual_reconciliation) and payment_collected:false. 409 GIFT_CONFIRMATION_PROCESSING or GIFT_GIVER_RECONCILIATION_REQUIRED also retain the original SetupIntent; they never authorize a new checkout or ambiguous historical resend. Durable delivery incomplete returns409 GIFT_RECIPIENT_DELIVERY_PENDING or GIFT_RECIPIENT_DELIVERY_REVIEW_REQUIRED, with purchase_preserved:true and retry_same_confirmation in error.details. Only pending permits rechecking the same confirmation; review never authorizes another purchase or automatic resend. Recurring responses additionally preserve giver_receipt_state and payment_collected:false. Accepted channels are not proof of delivered messages, and all original activation requirements must complete before recipient-dependent confirmation succeeds.

Parameters, scopes and examples

Confirm gift card payment

Request body

{
  "payment_intent_id": "pi_xxx",
  "organization_slug": "hot-yoga-cph"
}

Response example

{
  "data": null,
  "error": {
    "code": "GIFT_RECIPIENT_DELIVERY_PENDING",
    "message": "Your gift is preserved, but its confirmation is not complete. Keep this checkout and check its confirmation again. Do not purchase again.",
    "details": {
      "purchase_preserved": true,
      "retry_same_confirmation": true
    }
  }
}
GET/api/v1/me/passes/{id}/pauseBearer token

Get pass pause capability and policy

Owner-scoped, read-only pause capability for an issued pass. Resolves the current venue-local date and timezone, earliest allowed start, next billing date and notice deadline, currency, pause policy and termination boundary. can_pause plus reason/reason_code is authoritative for availability. preview_required identifies a subscription whose configured notice or calendar-month policy requires a reviewed token; clients must not calculate policy, billing amounts or date bounds independently.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Response example

{
  "data": {
    "pass_id": "uuid",
    "recurring_flexible": true,
    "preview_required": true,
    "can_pause": true,
    "reason": null,
    "reason_code": null,
    "today": "2026-09-10",
    "earliest_start_date": "2026-09-10",
    "next_billing_date": "2026-10-01",
    "next_billing_notice_deadline": "2026-09-24",
    "timezone": "UTC",
    "currency": "USD",
    "policy": {
      "notice_days": 7,
      "min_days": 14,
      "max_days": null,
      "max_months": 3,
      "max_pauses_per_year": 2,
      "pause_fee_minor": 0,
      "extends_binding": true
    },
    "termination": {
      "self_service_allowed": false,
      "mode": "after_n_cycles",
      "cycles": 1
    }
  },
  "error": null
}
POST/api/v1/me/passes/{id}/pauseBearer token

Pause pass

Preview or commit an owner-scoped pass pause for an inclusive venue-local date range. preview:true performs no mutation and returns the canonical resolved policy, dates and financial cycle schedule. A normal commit retains the existing pause response. An opted-in subscription with preview_required:true requires the exact preview_token from the reviewed preview; a missing token returns 409 with error.details.code PAUSE_PREVIEW_REQUIRED, while changed or expired review authority returns 409 PREVIEW_STALE. Other fixed and legacy pause behavior remains unchanged. The server enforces notice, calendar-duration, annual allowance, binding and billing rules; clients must not calculate credit or charge amounts. Idempotency-Key supported for commit.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Inclusive pause window. Use preview:true without preview_token to review; for a commit with preview_required:true, resend the same dates and exact preview_token.

Request body

{
  "start_date": "2026-10-08",
  "end_date": "2026-10-21",
  "reason": "Holiday",
  "preview": true
}

Response example

{
  "data": {
    "pass_id": "uuid",
    "start_date": "2026-10-08",
    "end_date": "2026-10-21",
    "resumes_on": "2026-10-22",
    "days": 14,
    "preview_token": "opaque-review-token",
    "policy": {
      "notice_days": 7,
      "min_days": 14,
      "max_days": null,
      "max_months": 3,
      "max_pauses_per_year": 2,
      "pause_fee_minor": 0,
      "extends_binding": true
    },
    "financial": {
      "currency": "USD",
      "billing_interval": "month",
      "billing_interval_count": 1,
      "total_credit_minor": 14000,
      "next_charge_date": "2026-11-01",
      "next_charge_amount_minor": 17000,
      "cycles": [
        {
          "cycle_start": "2026-10-01",
          "cycle_end": "2026-11-01",
          "service_days": 31,
          "frozen_days": 14,
          "normal_amount_minor": 31000,
          "credit_amount_minor": 14000,
          "reduced_amount_minor": 17000,
          "target_charge_date": "2026-11-01",
          "target_charge_amount_minor": 17000,
          "target_kind": "carry_forward"
        }
      ]
    }
  },
  "error": null
}
POST/api/v1/me/passes/{id}/resumeBearer token

Resume pass

Resume a paused pass and lift any Stripe billing pause. Owner-scoped. Idempotency-Key supported.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id
POST/api/v1/me/passes/{id}/cancel-renewBearer token

Cancel pass auto-renew

Confirmed owner-scoped cancellation alias for /terminate. Requires acknowledged=true, accepts reason, preview_token and optional cycle from the termination preview. Enforces current venue, brand, product and purchased rules; provider synchronization fails closed. Idempotency-Key supported.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id
POST/api/v1/me/passes/{id}/start-earlierBearer token

Start a deferred membership earlier

Move a deferred (pending_activation) membership start to today or an earlier future date: re-anchors Stripe billing, charges the first membership payment, and activates the pass. Owner-scoped. Idempotency-Key supported; rate-limited 5/min. Returns payment_status succeeded | requires_action (confirm with client_secret; the invoice.paid path then activates) | pending. Errors: PASS_NOT_FOUND, FORBIDDEN, ALREADY_STARTED, IN_PROGRESS, INVALID_START_DATE, PAYMENT_FAILED, STRIPE_UNAVAILABLE.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

New start date (must be earlier than the current start)

Request body

{
  "new_start_date": "2026-07-03",
  "class_instance_id": "uuid (optional)"
}

Response example

{
  "data": {
    "pass": {
      "id": "uuid",
      "status": "active",
      "start_date": "2026-07-03"
    },
    "payment_status": "succeeded",
    "client_secret": null
  }
}
GET/api/v1/me/passes/{id}/termination-previewBearer token

Preview member membership termination

Owner-scoped read-only cancellation summary from stored pass brand, product, purchased terms and venue rules. Optional cycle selects a server-offered later end date. summary.cycleChoices contains {cycles,effectiveAtIso,effectiveDateVenueLocal}; empty when unavailable. Includes final scheduled payment, commitment refusal and preview_token; confirm with the same cycle and token. Omitted cycle preserves default notice.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Query parameters

cycleinteger
Optional offered billing cycle, 0–12; 0 only for an immediate default
POST/api/v1/me/passes/{id}/terminateBearer token

Terminate a recurring membership

Confirmed owner-scoped membership termination. Enforces venue allow_member_cancel, minimum membership age, binding period, required reason and the configured termination boundary. Stripe synchronization is fail-closed and Idempotency-Key is supported.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Explicit acknowledgement, optional/venue-required reason, and selected preview token/cycle. Omitted cycle retains default notice. A revoked later choice returns CANCELLATION_CHOICE_UNAVAILABLE; changed dates or terms return PREVIEW_STALE.

Request body

{
  "acknowledged": true,
  "reason": "Moving away",
  "cycle": 2,
  "preview_token": "64-character preview fingerprint"
}
GET/api/v1/me/passes/{id}/extensionBearer token

Get pass extension quote

Return the authenticated member’s venue-scoped self-extension policy and live quote: proposed expiry, price/currency, configured duration, remaining extension allowance, clips, a machine-readable unavailable_reason, and the pass type’s purchase_channel (native | web, the same value the catalog serves to this caller, including the `X-App-Digital-Content` rule). A paid extension with purchase_channel web must be sold on the venue website, not in the app. Requires X-Organization-ID and fails closed on invalid venue configuration.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Response example

{
  "data": {
    "pass_id": "uuid",
    "pass_name": "10-Class Clip Card",
    "current_end_date": "2026-07-31",
    "proposed_end_date": "2026-08-14",
    "extension_price": 100,
    "currency": "DKK",
    "extension_period": 2,
    "extension_unit": "weeks",
    "extension_count": 0,
    "max_extensions": 2,
    "can_extend": true,
    "unavailable_reason": null,
    "payment_required": true,
    "purchase_channel": "native"
  }
}
POST/api/v1/me/passes/{id}/extension/payment-intentBearer token

Prepare pass extension payment

Revalidates the venue’s live self-extension policy and creates a durable operation before any processor call. Paid responses include PaymentSheet customer/ephemeral-key credentials in the exact frozen Stripe namespace; direct mode returns stripe_account_id. Free responses still return operation_id but do not mutate the pass. A paid extension of a pass type whose purchase_channel is web is refused before any idempotency replay, operation claim or processor call with 409 PURCHASE_CHANNEL_WEB_ONLY and error.details {purchase_channel: 'web'}; the channel follows the catalog's caller-aware `X-App-Digital-Content` rule. Requires X-Organization-ID and Idempotency-Key.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Response example

{
  "data": {
    "operation_id": "uuid",
    "status": "requires_payment",
    "payment_required": true,
    "amount": 100,
    "currency": "DKK",
    "proposed_end_date": "2026-08-14",
    "payment_intent_id": "pi_xxx",
    "client_secret": "pi_xxx_secret_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_xxx",
    "stripe_account_id": "acct_xxx (direct mode; otherwise null)",
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  }
}
POST/api/v1/me/passes/{id}/extension/confirmBearer token

Confirm pass extension

Authoritatively rechecks owner, tenant, venue policy, maximum count, hard end, frozen Stripe provenance and payment status under database locks. Paid success atomically records payment, fee, audit, pass, and operation; explicit post-charge conflicts are idempotently refunded. Nonterminal 202 statuses are finalizing or refund_pending and are safe to retry. The Stripe webhook shares this reconciler. Idempotency-Key is required.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Durable operation reference plus PI reference for paid extensions

Request body

{
  "operation_id": "uuid",
  "payment_intent_id": "pi_xxx (omit when free)"
}

Response example

{
  "data": {
    "extended": true,
    "status": "applied",
    "operation_id": "uuid",
    "new_end_date": "2026-08-14",
    "payment_required": true,
    "amount": 100,
    "currency": "DKK"
  }
}
POST/api/v1/me/passes/{id}/pay-renewalBearer token

Pay my overdue membership renewal

Settles the outstanding renewal of the caller’s past_due or suspended membership. Optional JSON body { payment_method_id, payment_id }: payment_id is the original local renewal UUID from the outstanding item and prevents collecting a later debt. Legacy bodies without it remain accepted. payment_method_id (v1.2) is a saved card (pm_… or a legacy card_… source) that becomes the subscription’s default and the customer’s default before the open renewal invoice is paid with it, so this and every later renewal charge it. Without a body the card the member set as default is used when it differs from the subscription’s card, else the subscription’s card. Returns payment_id and an additive receipt with operation_id, payment_id, status (settled, failed, pending) and settled. Keep both IDs through all retries and bank outcomes. Paid requires a scoped durable receipt; requires_action with client_secret and stripe_account for 3-D Secure (then call …/pay-renewal/confirm); processing while the processor still works on it (do not pay again). Every refusal carries a machine code and error.details.next_step: 422 PAYMENT_FAILED for a real card decline (next_step update_card, details.decline_code); 422 NO_PAYMENT_METHOD when there is no usable saved card; 422 CARD_NOT_AVAILABLE when the sent card is not saved on this membership’s customer; An unproven closed attempt stays pending; 400 VALIDATION_ERROR for a malformed id; unclassified processor failures return processing and require confirmation of the original payment; legacy 502 RETRY_FAILED is also an uncertain result (next_step null); 422 PAYMENT_NOT_COLLECTABLE or CARD_PAYMENTS_UNAVAILABLE when only the venue can take it (next_step contact_venue); 422 NOTHING_OUTSTANDING (next_step null). Owner scoped, 5/min. Optional X-Organization-Slug scopes the call to one venue (branded apps, brand sites); an unknown slug returns nothing and never another venue’s rows.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Optional payment_id binds the original local renewal UUID before collection. Optional Idempotency-Key atomically reserves the authenticated actor, owning venue and exact body; mismatches refuse. The durable operation retains its exact original provider body and key; bounded retries may replay only that command. Unknown outcomes remain processing and require original-operation/payment confirmation. Optional (v1.2) payment_method_id: a saved card from GET /api/v1/me/payment-methods (pm_… or a legacy card_… source). Pinned on the subscription and the customer, then charged for this renewal.

Request body

{
  "payment_method_id": "pm_1AbCdEfGhIjKlMnOpQrStUv",
  "payment_id": "11111111-2222-4333-8444-555555555555"
}
POST/api/v1/me/passes/{id}/pay-renewal/confirmBearer token

Confirm my renewal payment after 3-D Secure

Verifies the exact original renewal payment with the processor and its scoped durable receipt. Retain payment_id and the additive receipt.operation_id from the outstanding item/pay response before setup/default/challenge, and send it on every confirmation including SDK cancel/error. An active membership is not payment proof. Without payment_id, legacy confirmation examines the latest scoped payment and accepts only a verified subscription_cycle invoice; purchase invoices and native PI-only rows cannot prove a renewal. Returns settled true only after the exact receipt, otherwise false, plus receipt.status when an operation exists. Only provider-proven terminal decline permits selecting a new card; pending never permits a replacement operation. Owner scoped, 5/min. Optional X-Organization-Slug scopes the call to one venue (branded apps, brand sites); an unknown slug returns nothing and never another venue’s rows.

Parameters, scopes and examples

Path parameters

idstring · required
Pass id

Optional payment_id and operation_id bind the original renewal and durable recovery operation. Return receipt.status settled, failed or pending; unknown outcomes require the same operation. Empty legacy bodies remain accepted conservatively.

Request body

{
  "payment_id": "11111111-2222-4333-8444-555555555555"
}
POST/api/v1/passes/shares/acceptBearer token

Accept a pass share

Accept a pending pass-share invitation by token. Verifies the caller’s email matches the invite recipient, then grants booking access by appending the caller to `passes.shared_with` (respecting `pass_types.max_sharers`) and converges the share into the `pass_shares` table. Emits `pass.share_accepted`. Member-JWT. Original Idempotency-Key is required. Exact token/body/key replays the immutable accepted or closed receipt; unknown outcomes retain the original request.

Parameters, scopes and examples

Accept share

Request body

{
  "token": "a1b2c3…"
}

Response example

{
  "data": {
    "pass_id": "uuid",
    "invite_id": "uuid",
    "shared": true
  },
  "error": null
}
POST/api/v1/passes/shares/accept/readBearer token

Read original pass invitation acceptance

Authenticated recipient only. Use the exact original {token} body and Idempotency-Key. Existing acceptance wins closure. No inline notification. Errors never authorize replacement.

Parameters, scopes and examples

Exact original invitation token

Request body

{
  "token": "original-token"
}

Response example

{
  "data": {
    "shared": false,
    "receipt": null
  },
  "error": null
}
POST/api/v1/passes/shares/accept/closeBearer token

Close original pass invitation acceptance

Authenticated recipient only. Use the exact original {token} body and Idempotency-Key. Existing acceptance wins closure. No inline notification. Errors never authorize replacement.

Parameters, scopes and examples

Exact original invitation token

Request body

{
  "token": "original-token"
}

Response example

{
  "data": {
    "shared": false,
    "receipt": null
  },
  "error": null
}
PUT/api/v1/me/default-passBearer token

Choose my default pass

Member-JWT. Sets which of my passes a venue uses first for bookings, or clears it with pass_id null ("let the system choose"). The venue is the pass's own venue; organization_id (required when clearing) and the optional X-Organization-Slug scope must agree with it (400 ORGANIZATION_MISMATCH / ORGANIZATION_REQUIRED). Another member's or another venue's pass is 404 PASS_NOT_FOUND; a pass that is not active, past_due, pending_activation or paused is 422 PASS_NOT_SELECTABLE; no active membership is 404 MEMBERSHIP_NOT_FOUND. Audited as member.default_pass_set. Without a default, bookings use passes in good standing first, then clip cards before unlimited passes, then the soonest-expiring, then the fewest clips left. When the default is used up or cannot be used for a class, POST /api/v1/bookings answers 409 DEFAULT_PASS_UNUSABLE.

Parameters, scopes and examples

Default pass choice

Request body

{
  "pass_id": "uuid",
  "organization_id": "uuid"
}

Response example

{
  "data": {
    "organization_id": "uuid",
    "default_pass_id": "uuid",
    "previous_default_pass_id": null
  }
}
Video2 documented operations
GET/api/v1/video-catalogBearer token

List video-on-demand catalog

Published VODs and class replays for the caller's venue. Visibility public + members only; pass_restricted items are accessible via /video-catalog/:id once the pass check passes. Signed Mux playback URLs valid for 2 hours. A request with `X-App-Digital-Content: none` is refused with 403 IN_APP_DIGITAL_CONTENT_DISABLED before any lookup or signed URL.

Parameters, scopes and examples

Query parameters

pageinteger
Page numberDefault: 1
limitinteger
Items per page (max 50)Default: 20
categorystring
Filter by category (class_recording | tutorial | workshop)

Response example

{
  "data": [
    {
      "id": "uuid",
      "title": "Vinyasa Flow — 12 June",
      "category": "class_recording",
      "duration_seconds": 3600,
      "playback_url": "https://stream.mux.com/abc.m3u8?token=...",
      "thumbnail_url": "https://image.mux.com/abc/thumbnail.jpg?token=...",
      "recorded_at": "2026-06-12T09:00:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 42,
    "has_more": true
  }
}
GET/api/v1/video-catalog/{id}Bearer token

Get VOD detail

Single video detail with signed playback URL. Pass_restricted videos require a qualifying active pass (returns 403 PASS_REQUIRED otherwise). A request with `X-App-Digital-Content: none` is refused with 403 IN_APP_DIGITAL_CONTENT_DISABLED before any lookup or signed URL.

Parameters, scopes and examples

Path parameters

idstring · required
Video library entry UUID

Response example

{
  "data": {
    "id": "uuid",
    "title": "Vinyasa Flow — 12 June",
    "category": "class_recording",
    "duration_seconds": 3600,
    "playback_url": "https://stream.mux.com/abc.m3u8?token=...",
    "thumbnail_url": "https://image.mux.com/abc/thumbnail.jpg?token=...",
    "visibility": "members",
    "view_count": 17,
    "instructor": {
      "id": "uuid",
      "display_name": "Sarah",
      "avatar_url": null
    },
    "class_type": {
      "id": "uuid",
      "name": "Vinyasa Flow",
      "slug": "vinyasa-flow"
    }
  }
}
Forms3 documented operations
GET/api/v1/me/formsBearer token

List my intake forms

The caller's required/pending intake forms for their active org. Each entry is annotated with whether the member already submitted (the pre-booking form gate's source of truth). Returns [] when the `forms` module is disabled.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "slug": "new-client-intake",
      "name": "New Client Intake",
      "description": "Tell us about your practice and any injuries.",
      "required": true,
      "submitted": false,
      "submission_id": null,
      "submitted_at": null
    }
  ]
}
GET/api/v1/forms/{id}Bearer token

Get form schema

Render schema (fields, steps, submit label) plus the venue's configured `legal_basis` (`consent` | `contract` | `legitimate_interest` | `legal_obligation`) for a single published form. Use `legal_basis` to render the matching privacy notice and, for a `consent` form, to present its required consent checkbox as the gate it is — a consent-basis submission is refused unless that box was ticked. Scoped to the active org — forms in other orgs return 404.

Parameters, scopes and examples

Path parameters

idstring · required
Form UUID

Response example

{
  "data": {
    "id": "uuid",
    "slug": "new-client-intake",
    "name": "New Client Intake",
    "description": "Tell us about your practice and any injuries.",
    "required": true,
    "legal_basis": "consent",
    "schema": {
      "version": 1,
      "fields": [],
      "steps": null,
      "submit_label": "Submit"
    },
    "thank_you": {}
  }
}
POST/api/v1/forms/{id}/submitBearer token

Submit a form

Submit `{ answers }` for a published form. Validates required fields + types, persists a submission stamped with the caller, and routes it into the unified inbox. `Idempotency-Key` is optional but MUST be a UUID when sent (400 `INVALID_IDEMPOTENCY_KEY` otherwise) — it is both the HTTP replay token and the database ingest request id. The same key with the same answers replays the original response; the same key with different answers writes nothing and returns 409 `IDEMPOTENCY_KEY_REUSE_MISMATCH`, so mint a new key whenever the answers change. Every 201 and every 503 `SUBMIT_RECONCILE_FAILED` returns an `Idempotency-Key` RESPONSE header (mirrored as `error.details.request_id` on the 503) carrying the identity the submission was accepted under — your key when you sent one, the server-generated UUID when you did not. Retry a 503 with that exact value as `Idempotency-Key`: it replays the accepted submission and re-drives only the missing delivery. Retrying without it mints a new identity and files a duplicate. 422 with `details.missing[]` on required-field failures; 422 `CONSENT_REQUIRED` when a consent-basis form was sent without its consent box ticked; 409 `FORM_CONSENT_MISCONFIGURED` when the form itself cannot lawfully collect.

Parameters, scopes and examples

Path parameters

idstring · required
Form UUID

Answer map keyed by field key

Request body

{
  "answers": {
    "full_name": "Jane Doe",
    "email": "jane@example.com",
    "injuries": "None"
  }
}

Response example

{
  "data": {
    "submission_id": "uuid",
    "form_id": "uuid",
    "status": "submitted",
    "thank_you": {}
  }
}
Appointments19 documented operations
GET/api/v1/appointmentsBearer token

List my appointments

Returns the authenticated member’s appointments with the exact updated_at concurrency token required for cancellation and the owning venue’s id, name, slug and timezone for local date/time presentation, including retained history after membership ends. Supports upcoming/past direction, status, venue narrowing and cursor pagination.

Parameters, scopes and examples

Query parameters

directionstring
upcoming | pastDefault: upcoming
statusstring
Appointment status
organization_idstring
Optional venue UUID narrowing
POST/api/v1/appointmentsBearer token

Book my appointment

Creates a free, pass-covered, or pay-at-venue member appointment. Retries recover the matching durable operation before current availability, named selection or payment policy; keep the same Idempotency-Key and booking details. Recovery is scoped to the authenticated actor and resolved organization, never a user-only HTTP cache. New Any requests retain unnamed intent in their operation hash while storing the concrete assignment. A legacy concrete-only hash cannot establish earlier Any intent and returns a conflict rather than guessing. Fresh named choices require active assignment/membership, selectable_for_named_booking and client_picks policy; disabled names return 409 NAMED_PROVIDER_NOT_SELECTABLE. Send provider_id=any for automatic assignment without removing hidden staff capacity. Explicit room_id is separate from provider_id. Paid-at-booking appointments use the checkout endpoints below. When the venue mode is client_choice, omitted payment_choice defaults to online; venue is allowed only when the canonical quote permits it. X-Organization-ID and a stable Idempotency-Key are required; client communication follows the locked member-transactional policy rather than staff-selectable channels.

GET/api/v1/appointments/{id}Bearer token

Get my appointment

Returns one appointment owned by the authenticated member, including its updated_at concurrency token, rescheduled_to_id replacement pointer, visit_id for a composed visit, and the owning venue’s id, name, slug and timezone for local date/time presentation, including retained history after membership ends. Pending or discarded visit legs are hidden. Open the whole-visit endpoint when visit_id is present. The optional organization_id query narrows the owned result to one venue, including retained history after an offering is removed. Follow replacement pointers using the same venue scope; each target is independently authorized.

Parameters, scopes and examples

Path parameters

idstring · required
Appointment UUID

Query parameters

organization_idstring
Optional venue UUID narrowing; never grants access to another member’s appointment
DELETE/api/v1/appointments/{id}Bearer token

Cancel my appointment

Atomically cancels one owned current appointment. Clients must send the exact rendered updated_at token; a missing token returns 400 VALIDATION_ERROR and a stale token returns 409 STALE_TARGET. Refresh the displayed appointment before retrying. Configured venue self-service cutoffs return SELF_SERVICE_CUTOFF when the remaining time is strictly less than the cutoff; exactly the cutoff remains eligible. Paid/deposit appointments return REFUND_REQUIRED and package legs return PACKAGE_REQUIRES_STAFF until the venue handles the whole-visit/refund workflow. Composed-visit legs return GROUPED_VISIT_REQUIRES_VISIT_ACTION; use the whole-visit routes. X-Organization-ID and a stable Idempotency-Key are required.

Parameters, scopes and examples

Path parameters

idstring · required
Appointment UUID

Caller-rendered concurrency snapshot

Request body

{
  "expected_updated_at": "2026-08-28T09:15:30.000Z",
  "reason": "Plans changed"
}
GET/api/v1/me/appointments/{id}/cancellation-previewBearer token

Preview my appointment cancellation

Read-only preview of the consequence of cancelling one owned appointment right now. The window and fee come from the service row (services.cancellation_window_hours / cancellation_fee_amount, defaults 24 / 0) — the exact pair the cancel RPC enforces — so the number shown matches the number charged. An appointment with a paid deposit or a linked payment is blocked with blocked_reason "refund_required" rather than previewing a self-service refund; a terminal appointment is blocked "not_cancellable". A Combo Package leg is blocked "package_requires_staff", with package_linked=true, because its payment and change workflow belong to the whole package. The separate optional venue self_service_cutoff_hours restricts when clients may act, independently of late fees: blocked_reason "self_service_cutoff" means staff must help. Exactly the configured number of hours remains eligible. A null cutoff preserves ordinary behavior unless malformed configuration produces a blocked decision; clients must use can_cancel/blocked_reason as authority. Composed-visit legs return can_cancel=false, blocked_reason "grouped_visit_requires_visit_action", visit_linked=true and visit_id; preview and manage the whole visit instead. Pending and discarded legs return 404. Honours the optional x-organization-slug tenant scope; an appointment outside the resolved scope, or belonging to another client, returns 404.

Parameters, scopes and examples

Path parameters

idstring · required
Appointment UUID

Response example

{
  "data": {
    "appointment_id": "uuid",
    "can_cancel": true,
    "blocked_reason": null,
    "cancellation_window_hours": 24,
    "self_service_cutoff_hours": null,
    "package_linked": false,
    "visit_linked": false,
    "visit_id": null,
    "is_late": true,
    "will_charge": true,
    "fee_amount": 250,
    "currency": "DKK",
    "refund_expected": false,
    "message": "You are inside the venue’s cancellation window, so a late-cancellation fee applies."
  }
}
GET/api/v1/appointments/quoteBearer token

Preview appointment payment policy

Returns the authenticated member’s server-authoritative effective service price, deposit, amount due at booking, remaining venue balance, payment timing, and payment_at_booking_mode (venue | online | client_choice). Named choices require active service assignment and venue-membership permission. Any or omitted requests return provider_id=null without changing the concrete internal quote. Keep Any intent in subsequent requests. An any provider request resolves through public availability, including active assignments and membership, venue-local hours, service options, location and occupancy, before pricing. Optional payment_choice=online|venue is honoured only when the venue mode is client_choice; omitted choice defaults to online so older clients keep paying at booking. A configured deposit still requires the deposit online. Requires X-Organization-ID.

Parameters, scopes and examples

Query parameters

service_idstring · required
Service id
provider_idstring
Selectable provider UUID or any; omitted is Any. Unnamed responses return provider_id=null
start_timestring · required
ISO appointment start
location_idstring
Optional location UUID; required for a location-restricted service. Must match availability and checkout.
variant_idstring
Optional service option UUID; the same option must be used for availability and checkout.
pass_idstring
Optional owned pass UUID; only validated service coverage affects the quote.
payment_choicestring
Optional online | venue. Omitted = pay now when the venue lets the client decide.
requested_currencystring
Optional uppercase ISO currency for an enabled home-country service/option book. Requires an uncovered service and named selectable professional (facilities use Any). Returns selected_currency_quote for exact acceptance. Service country enablement remains closed pending cross-surface qualification.

Response example

{
  "data": {
    "service_id": "uuid",
    "provider_id": "uuid",
    "currency": "DKK",
    "price_amount": 500,
    "deposit_amount": 100,
    "amount_due_at_booking": 100,
    "outstanding_after_booking": 400,
    "payment_required": true,
    "payment_timing": "at_booking",
    "payment_at_booking_mode": "online",
    "payment_checkout_available": true
  }
}
POST/api/v1/appointments/checkoutBearer token

Prepare paid appointment checkout

Claims a durable, tenant-bound operation before creating an account-pinned Stripe PaymentIntent. Accepts a selectable provider UUID or any. Fresh operations check public selection before claim/payment; existing operations keep the frozen provider after admin settings change. Returns PaymentSheet credentials and the frozen operation quote, not current venue policy. New unnamed operations return quote.provider_id=null while retaining the actual provider internally. Retry the same intent/key; changing named/any intent or an explicitly supplied payment choice conflicts. Older operations without snapshot metadata remain resumable. X-Organization-ID and a stable Idempotency-Key are required.

Parameters, scopes and examples

Exact live slot and provider selection. provider_id may be a concrete UUID or "any"; the server freezes one provider and its effective price before payment. Optional payment_choice online|venue follows shared quote policy. A choice requiring no online payment returns APPOINTMENT_PAYMENT_NOT_REQUIRED; use unpaid create after review. Optional selected_currency is {currency, acceptedQuote}, where acceptedQuote is the exact selected_currency_quote returned by review. Retain the complete original body and key on every retry. Home-country only; enabled owned service/option books and migration-backed claim required. Finer-than-two-decimal currencies, professional custom prices, offers, passes, bundles and composed visits refuse. Activation remains closed pending all consumer qualification.

Request body

{
  "service_id": "uuid",
  "provider_id": "uuid",
  "start_time": "2026-08-07T10:00:00.000Z",
  "location_id": "uuid"
}

Response example

{
  "data": {
    "operation_id": "uuid",
    "status": "payment_pending",
    "client_secret": "pi_xxx_secret_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  }
}
POST/api/v1/appointments/checkout/confirmBearer token

Finalize paid appointment

Retrieves the exact account-scoped PaymentIntent, requires processor status succeeded, creates the appointment idempotently, and atomically links payment/accounting. Legacy operations recheck live policy; accepted selected-currency operations use their frozen original price/tax while canonical scheduling and eligibility remain authoritative. Slot conflicts are compensated with the original idempotent refund; 202 finalizing states retain the original key. The Stripe webhook uses the same reconciler.

Parameters, scopes and examples

Durable appointment checkout operation

Request body

{
  "operation_id": "uuid"
}

Response example

{
  "data": {
    "booked": true,
    "status": "applied",
    "operation_id": "uuid",
    "appointment": {
      "appointment_id": "uuid",
      "status": "confirmed"
    }
  }
}
GET/api/v1/venues/{slug}/availabilityPublic

Read public appointment times with named or Any intent

The slug resolves the authoritative venue. A named provider must be an active assigned venue member allowed by the service policy and named-booking flag. Any or omitted provider keeps all operational capacity and returns one deterministically priced slot per instant with provider_id=any, provider_name empty and provider_selection_intent=any. Named slots retain their permitted UUID/name and provider_selection_intent=named. Room-only slots remain opt-in and use provider_id=null, resource_kind=room and a real room_id, never an aliased provider. Failed reads return 503, denied names 409; responses are private/no-store. Staff fulfillment and existing booking history use separate authorized operational reads.

Parameters, scopes and examples

Path parameters

slugstring · required
Public venue slug

Query parameters

service_idstring · required
Active service UUID in this venue
datestring · required
Venue-local calendar date YYYY-MM-DD
provider_idstring
Selectable professional UUID or any; omitted means Any
location_idstring
Selected service location UUID
variant_idstring
Selected service option UUID
include_facilityboolean
Explicit room-slot opt-in; default false
GET/api/v1/venues/{slug}/services/{serviceSlug}Public

Read a venue service and its named booking choices

Public, rate-limited service detail. The venue slug resolves the owning organization; active service and provider assignments are scoped to that venue. providers contains only active members selectable_for_named_booking when the service policy is client_picks (null policy retains that default). any_available and admin_assigns return no named choices. Team-page listing is independent. Provider-read failure returns 503 PROVIDERS_UNAVAILABLE, not an unfiltered list. This endpoint does not determine Any capacity, quote pricing, booking authorization or historical provider identity.

Parameters, scopes and examples

Path parameters

slugstring · required
Public venue slug
serviceSlugstring · required
Active service slug within that venue
POST/api/v1/appointments/{id}/manage-paidBearer token

Manage my eligible paid appointment

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Send expected_updated_at exactly as returned by GET; stale snapshots return 409 STALE_TARGET. Available only when appointment detail returns paid_self_service_available. Atomically attaches the existing appointment and verified payment to a complete-visit management record; returns data {visit_id}. It does not book or charge again. The original checkout must have frozen explicit owner-configured self-service refund terms. Legacy payments, prepaid pass/series/bundle bookings and unsupported payment evidence retain staff management. The venue cutoff still applies to subsequent cancellation or movement. Checkout quote refund_terms describes the accepted single-appointment terms before payment.

Parameters, scopes and examples

Path parameters

idstring · required
Owned appointment UUID
POST/api/v1/appointments/visits/planBearer token

Plan my complete appointment visit

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send date (YYYY-MM-DD), ordered services, optional provider_id/location_id and, for a new booking, optional provider_selection_intent. With intent "any" (or provider_id "any") candidates return provider_id "any", an empty provider_name and provider_selection_intent "any": times and prices stay real, the professional is assigned by the venue and can include people not offered for named choice. With intent "named" and a provider_id, every selected service must allow that professional by name (409 NAMED_PROVIDER_NOT_SELECTABLE, 503 PROVIDER_SELECTION_UNAVAILABLE); candidates carry that UUID and provider_selection_intent "named". Without intent the earlier concrete candidate contract is unchanged. An owned from_visit_id move never takes intent. One service may probe capability; new bookable candidates require at least two. An owned confirmed visit adopted from an eligible paid single appointment may retain its one service when moving. An owned from_visit_id may seed the original services with services: [] for a move or rebooking. Returns capability, max_services/max_services_per_visit, refund_terms and server-calculated candidates with provider_id, location_id/location_name, start_time/end_time, total_price_amount, amount_due_at_booking, currency and each leg. An existing confirmed visit can be fulfilled while new sales are closed.

POST/api/v1/appointments/visitsBearer token

Book my complete unpaid appointment visit

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Services are an ordered array of 2–8 {service_id, variant_id?}; send provider_id, concrete location_id, start_time and the planner’s expected_total_price_amount, expected_amount_due_at_booking and expected_currency. The server calculates prices, buffers and availability again. New bookings may add provider_selection_intent: "any" (provider_id "any"; the venue assigns the professional the plan priced) or "named" (a professional the venue allows clients to pick by name on every selected service; otherwise 409 NAMED_PROVIDER_NOT_SELECTABLE, 503 PROVIDER_SELECTION_UNAVAILABLE when the check cannot be read). A concrete provider_id without intent keeps the earlier contract and is not treated as a named pick. Intent, when sent, is part of the idempotent request and cannot change on retry. Returns data as the complete visit detail. All legs become visible together. A required deposit/payment returns 402 PAYMENT_REQUIRED; use checkout.

POST/api/v1/appointments/visits/checkoutBearer token

Prepare payment for my complete visit

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Services are an ordered array of 2–8 {service_id, variant_id?}; send provider_id, concrete location_id, start_time and the planner’s expected_total_price_amount, expected_amount_due_at_booking and expected_currency. The server calculates prices, buffers and availability again. New bookings may add provider_selection_intent: "any" (provider_id "any"; the venue assigns the professional the plan priced) or "named" (a professional the venue allows clients to pick by name on every selected service; otherwise 409 NAMED_PROVIDER_NOT_SELECTABLE, 503 PROVIDER_SELECTION_UNAVAILABLE when the check cannot be read). A concrete provider_id without intent keeps the earlier contract and is not treated as a named pick. Intent, when sent, is part of the idempotent request and cannot change on retry. Freezes the accepted composition, currency, deposit amount, refund terms and payment execution. Returns operation_id, client_secret, customer_id, ephemeral_key, stripe_account_id and quote for PaymentSheet/web confirmation. Credentials are transient. already_booked=true is a successful replay. Payment covers the whole visit; unavailable fulfillment is durably refunded.

POST/api/v1/appointments/visits/checkout/confirmBearer token

Confirm my complete paid appointment visit

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Send operation_id from checkout. Returns data {status:"booked",visit}. 202 VISIT_FINALIZE_RETRY or VISIT_REFUND_PENDING remains pending; 409 PAYMENT_REQUIRES_ACTION requires returning to payment with the same checkout key. PAYMENT_CANCELLED and VISIT_PAYMENT_REFUNDED are terminal. A webhook and periodic worker reconcile payment independently of the client.

GET/api/v1/appointments/visits/{id}Bearer token

Read my complete appointment visit

Requires the member JWT and verifies client ownership independently of active membership. Legacy requests retain X-Organization-ID narrowing. Optional scope=owned ignores the active header and accepts an explicit organization_id UUID filter; merged identities remain confined to their authorized venue. Returns organization_id and an owning organization {id, slug, timezone} relation, plus the full itinerary, exact updated_at, payment_status, amount_paid_minor, frozen refund_terms, cancellation preview and rescheduled_from_visit_id/rescheduled_to_visit_id. Historical visits remain accessible when a venue removes an offering. Provider credentials and internal payment keys are never returned.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
DELETE/api/v1/appointments/visits/{id}Bearer token

Cancel my complete appointment visit

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Send expected_updated_at exactly as returned by GET; stale snapshots return 409 STALE_TARGET. Optional reason. Cancels every leg in one transaction and returns visit fields plus cancellation_result under data. An optional venue self-service cutoff is checked against the first service after row locks; exactly the cutoff remains eligible. Paid/deposit cancellation requires the accepted venue terms to permit self-service. Refund failure remains VISIT_REFUND_PENDING and the worker retries the same refund.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
POST/api/v1/appointments/visits/{id}/rescheduleBearer token

Move my complete appointment visit

Requires the member JWT and X-Organization-ID. Every visit is scoped to the organization and its owning client. Send a stable UUID Idempotency-Key. Retry the identical request with that key after transport failure or a 202 pending response; never create a second payment attempt to poll the first. Send expected_updated_at exactly as returned by GET; stale snapshots return 409 STALE_TARGET. Send provider_id, location_id, start_time and the planner’s expected_total_price_amount, expected_amount_due_at_booking, expected_currency. The original service/variant order is retained. An overlapping move releases old slots and confirms every new leg in the same transaction; any conflict preserves the complete original visit. A price/deposit change requires a new quote. Returns the replacement visit detail with reciprocal history pointers.

Parameters, scopes and examples

Path parameters

idstring · required
Whole appointment visit UUID
GET/api/v1/me/treatmentsBearer token

Read my treatment history

Requires the member JWT and X-Organization-ID. Returns contract_version 1, records[] (appointment, service name, provider display name, products used with quantity/unit, dates) and patch_tests[] (date, expiry, result, product). Formulas, staff notes, photos, internal ids, costs and compensation are never returned; the platform DSR export remains the portability path.

Courses7 documented operations
GET/api/v1/admin/courses/{courseId}/booking-accessAPI key

Get a cohort booking allowance

Returns the venue-scoped class and optional-workshop resources, booking limits, and validity windows granted to participants in one course cohort.

Parameters, scopes and examples

Required scopes

read:courses

Path parameters

courseIdstring · required
Course cohort UUID
PATCH/api/v1/admin/courses/{courseId}/booking-accessAPI key

Update a cohort booking allowance

Atomically resolves all selected resources within the API-key venue, updates independent class/workshop allowance buckets, and reconciles active participants.

Parameters, scopes and examples

Required scopes

write:courses

Path parameters

courseIdstring · required
Course cohort UUID
POST/api/v1/admin/courses/{courseId}/booking-access/overridesAPI key

Override participant booking allowances

Applies or resets class/workshop limits and validity windows for one or many participants in the same tenant-scoped course cohort.

Parameters, scopes and examples

Required scopes

write:courses

Path parameters

courseIdstring · required
Course cohort UUID
POST/api/v1/courses/enrollment-statusAPI key

Course enrollment status for a venue site

COURSE-ONLINE-01 — read-only roster status for one course of the API-key venue: participant contact, enrollment/payment status and attendance mode (`in_person` | `online`). Optional `X-Organization-ID` must equal the key venue (403 otherwise). Filters: `attendance_mode`, `enrollment_ids` (1-100). Keyset pagination by enrollment id, 100 per page; pass `meta.next_cursor` back as `cursor` while `meta.has_more` is true. `updated_at` is the last change time (rows untouched since migration 20261020000264 report their latest lifecycle timestamp). Another venue's course is 404.

Parameters, scopes and examples

Required scopes

read:course_enrollments

course_id (required), attendance_mode?, enrollment_ids? (<=100), cursor?

Request body

{
  "course_id": "00000000-0000-4000-8000-000000000103",
  "attendance_mode": "online"
}

Response example

{
  "data": [
    {
      "enrollment_id": "00000000-0000-4000-8000-000000000102",
      "course_id": "00000000-0000-4000-8000-000000000103",
      "user_id": "00000000-0000-4000-8000-000000000201",
      "email": "synthetic.participant@example.com",
      "first_name": "Synthetic",
      "last_name": "Participant",
      "enrollment_status": "enrolled",
      "payment_status": "deposit_paid",
      "attendance_mode": "online",
      "updated_at": "2026-09-29T10:00:00.000Z"
    }
  ],
  "error": null,
  "meta": {
    "limit": 100,
    "has_more": true,
    "next_cursor": "ZTowMDAwMDAwMC0..."
  }
}
GET/api/v1/courses/{id}/purchase/resumeBearer token

Resume an existing frozen course payment

Member JWT and venue membership required. Retrieves the caller-owned linked PaymentIntent using its frozen account, customer, amount, currency, provider and regional context. No financial write, claim reset, PaymentIntent creation, or idempotency-key change. Optional plan, purchaser_type and attendance_mode must match the frozen enrollment. A successful null response means no checkout exists; read failures never authorize a new purchase.

Parameters, scopes and examples

Path parameters

idstring · required
Course id

Response example

{
  "data": {
    "client_secret": "pi_existing_secret",
    "payment_intent_id": "pi_existing",
    "enrollment_id": "uuid",
    "stripe_account_id": null,
    "amount": 2800,
    "currency": "EUR",
    "plan": "full",
    "purchaser_type": "individual",
    "attendance_mode": "in_person"
  },
  "error": null
}
POST/api/v1/courses/managed-by-pass/application-enrollmentAPI key

Add a paid-claim website applicant to a managed course roster

Trusted server-to-server bridge for venue application forms. Resolves the API-key tenant, the pass type's managed course, the applicant client/membership, and an optional localized track name; then creates or annotates an active roster enrollment and books its upcoming course sessions. Self-reported paid_deposit/paid_full values are retained as claims requiring reconciliation and never fabricate or overwrite BookingBible payment ledger state. API-key only (write:members), rate-limited, Idempotency-Key required.

Parameters, scopes and examples

Required scopes

write:members

Accepted external course application

Request body

{
  "pass_type_id": "00000000-0000-4000-8000-000000000101",
  "application_id": "YB-260806-1234",
  "email": "synthetic.applicant@example.com",
  "first_name": "Synthetic",
  "last_name": "Applicant",
  "phone": "+4511111111",
  "track_name": "Weekday Program",
  "payment_choice": "paid_deposit"
}

Response example

{
  "data": {
    "created": true,
    "enrollment_id": "00000000-0000-4000-8000-000000000102",
    "course_id": "00000000-0000-4000-8000-000000000103",
    "track_id": "00000000-0000-4000-8000-000000000104",
    "track_name": "Weekday Program",
    "payment_status": "unpaid",
    "booked_sessions": 18,
    "total_sessions": 18
  },
  "error": null
}
POST/api/v1/courses/{id}/purchaseBearer token

Buy a paid course

Pay for a course enrollment (early-bird-aware price, or the deposit when required). Requires an Idempotency-Key header and returns a Stripe PaymentIntent client_secret + customer_id + ephemeral_key + stripe_account_id for the Payment Sheet. The enrollment is created `unpaid`; on `payment_intent.succeeded` it flips to paid/deposit_paid and its sessions are booked (deduped on the payment-intent id). Gated on membership + venue legal docs. Member-JWT. COURSE-SUITE — the body additionally accepts optional `plan` (payment-plan id), `purchaser_type` (`individual`|`company`), and `company` details (name/VAT/address) for VAT-by-purchaser + debtor invoicing. A supplied plan must exactly match a currently offered server-side plan; only an omitted property uses legacy/default behavior. The GET `/api/v1/courses/{id}` course detail additionally returns a `staff` array — `[{ role, name, title_label, photo_url, show_on_landing_page }]` — for the landing-page teaching team (COURSE-SUITE-02 multi-trainer). CV3-03 — the GET detail also returns `payment_plans` (`{ plans: [{ id, kind, installment_count? }], collection_method }`, the normalized plan OPTIONS this purchase route accepts as `plan`) and, for an authenticated Bearer caller with an enrollment, `viewer_enrollment` (`{ id, enrollment_status, payment_status, payment_plan, amount_paid, total_amount, balance, installments: [{ installment_number, amount, due_date, status }] }`; the response is always `Cache-Control: private, no-store`). COURSE-O30-01 — in a Danish age-split venue a course with a 30+ price is charged by the buyer age band: under 30 (date of birth + complete VAT evidence, 0 % VAT) pays `price`, everyone else (including a missing DOB/evidence or a company) pays the 30+ `price_o30_override` with standard VAT inside it; the band is chosen before early bird, proration, offers and the deposit/instalment split. The GET detail adds `age_band_prices` (`{ under30, over30, earlyBirdApplied }` or null) and, for a signed-in caller, `viewer_age_band_status` (`under_30`|`over_30`|`missing_dob`|`evidence_required`). New error: 503 `AGE_PRICING_UNAVAILABLE` when the band cannot be verified. COURSE-ONLINE-01 — optional body `attendance_mode` (`in_person` default | `online` on a hybrid course with an online price; own price book and online VAT category; an online seat never grants a studio place); 422 `ATTENDANCE_MODE_UNAVAILABLE`, 409 `ATTENDANCE_MODE_CONFLICT`, online seats full → 409 `COURSE_FULL`; the response echoes `attendance_mode`. The GET detail adds `attendance_options: [{ mode, available, price, early_bird_price, currency, age_band_prices, viewer_price?, spots_left, vat_rate }]` and `viewer_enrollment.attendance_mode`. `/courses/{id}/sessions` also carries additive per-session `course_identifiers` (`{course_id, label, tone, course_name}`, public courses only; `[]` when none), so a workshop page can tell which programme (for example 8W or 18W) each interleaved date belongs to; the venue course list adds `kind` (`course`|`workshop`) to each item. GET detail and `/courses/{id}/sessions` also add `online_replay: { enabled, until, open }` (replays for online participants, enforced by BB). COURSE-ONLINE-O30-01 — online group instruction follows the same Danish under-30 / 30+ rule: a hybrid course or workshop with an online 30+ price (`online_price_o30_override`, optional `online_early_bird_price_o30_override`) charges an online seat / online drop-in (`POST /api/v1/workshops/{id}/occurrences/{instanceId}/purchase` with `attendance_type: online`) by the buyer band — under 30 (DOB + VAT evidence, 0 % VAT) pays `online_price`, everyone else pays the 30+ online price with the online VAT category rate inside it; without an online 30+ price the online seat keeps its single online price for every age. Additive DTO fields: `attendance_options[mode=online].age_band_prices` (`{ under30, over30, earlyBirdApplied }` for the online book, else null) and a band-aware `viewer_price`; `viewer_age_band_status` is also returned when only the online book is split; `/courses/{id}/sessions` `workshop.prices` adds `physical_age_band_prices` and `online_age_band_prices` (`{ under30, over30 }` or null). Top-level `age_band_prices` stays the in-studio book. See docs/api/NAMASTE_SITES_API.md section 17.

Parameters, scopes and examples

Path parameters

idstring · required
Course id

Response example

{
  "data": {
    "client_secret": "pi_xxx_secret_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4",
    "payment_intent_id": "pi_xxx",
    "enrollment_id": "uuid",
    "amount": 1500,
    "currency": "DKK"
  },
  "error": null
}
Webhooks1 documented operation
POST/api/v1/webhooks/apple-app-storePublic

Apple App Store Server Notification V2

Receives Apple-signed App Store Server Notifications V2 (Production and Sandbox URL). Both the outer notification and nested transaction signatures are verified (x5c chain to Apple’s roots, bundle id, app id, environment). Each notificationUUID is stored and applied once: renewals extend the pass and record an App Store sale, expiry, refund and revoke end it, a reversed refund restores it. Refunds become venue refunds. Sandbox never records revenue. Returns 500 on a transient failure so Apple retries.

Parameters, scopes and examples

App Store Server Notifications V2 signed payload

Request body

{
  "signedPayload": "<Apple signed notification JWS>"
}
Calendar1 documented operation
GET/api/v1/calendarBearer or API key

Unified calendar feed

List every event on the unified calendar (classes, appointments, private events, streams, blocked time, instructor unavailability, blackouts, room rentals, maintenance, staff shifts, open gym) in a date range. JWT (any staff role) returns the org feed; API key with read:calendar returns the same. Filters: room_id, staff_id, location_id, brand_id, sources (comma-separated), only_blocking.

Parameters, scopes and examples

Required scopes

read:calendar

Query parameters

startstring · required
YYYY-MM-DD inclusive
endstring · required
YYYY-MM-DD inclusive
room_idstring
Restrict to one room
staff_idstring
Restrict to one staff member
location_idstring
Restrict to one location
brand_idstring
Restrict to one brand
sourcesstring
Comma-separated subset of: class, appointment, private_event, stream, blocked_time, unavailability, blackout, room_rental, maintenance, staff_shift, open_gym
only_blockingstring
true to return only events with is_blocking=true
Maintenance2 documented operations
GET/api/v1/maintenanceAPI key

List maintenance slots

List maintenance slots in a date range. API key with read:maintenance scope. Filters: start, end (ISO datetime), room_id, status, limit (1-200, default 50).

Parameters, scopes and examples

Required scopes

read:maintenance

Query parameters

startstring
ISO datetime, inclusive lower bound on start_time
endstring
ISO datetime, inclusive upper bound on start_time
room_idstring
Restrict to one room
statusstring
scheduled | in_progress | completed | overdue | cancelled | deferred
limitinteger
Max rows (1-200)Default: 50
POST/api/v1/maintenanceAPI key

Schedule maintenance

Create a maintenance slot. API key with write:maintenance. Body: maintenance_type (preventive | corrective | inspection | deep_clean | equipment | renovation), title, start_time, end_time, plus optional priority, room_id, equipment_id, blocks_room (default true), assigned_staff_id, vendor_name, vendor_contact, estimated_cost, notes. Idempotency-Key header honored. When blocks_room is true and a room is set, conflicts against classes / appointments / private events / streams / room rentals / other maintenance return 409 with the conflict list. Emits maintenance.scheduled.

Parameters, scopes and examples

Required scopes

write:maintenance

Maintenance creation payload

Request body

{
  "maintenance_type": "deep_clean",
  "title": "Quarterly studio deep clean",
  "start_time": "2026-05-01T20:00:00Z",
  "end_time": "2026-05-01T22:00:00Z",
  "room_id": "uuid",
  "priority": "normal",
  "blocks_room": true
}
Staff40 documented operations
GET/api/v1/staff/scheduleBearer token

My teaching schedule

Instructor's classes. Optional scope=own|partner|all; every row includes origin venue metadata and origin.timezone so apps bucket collaboration classes in the owning venue's local day.

GET/api/v1/staff/earningsBearer token

My earnings

Compensation, tips, and commissions broken down by period and class. tips_settled_via_collaboration is additive visibility for gratuities paid on a practitioner statement and is deliberately excluded from tips_received and total.

GET/api/v1/staff/classes/{id}/rosterBearer token

Class roster

View attendee list for a class the instructor is assigned to. Returns class and booking updated_at CAS tokens, venue-local day_state, and historical_capabilities. include_historical_records=true additionally exposes terminal roster rows and requires scheduling.manage_history. Guest rows include guest_host_name when a host is linked. Each attendee also carries the additive notification_availability block ({email|sms|push: {available, reason_code, reason}}) so a staff notify picker can enable a channel and state the precise reason an unusable channel is disabled; it is null for a guest booking or a degraded read, and a null block leaves every channel disabled (fail-closed, never an unintended send). The same block additively carries booking_removal and waitlist_promote ({clients: {email|sms|push: {available, reason_code, reason, recipient_id, reachable, total, blocked, reach_label}}}). Each attendee carries online_attendance: null for in-studio bookings; for streaming bookings { state: watching | attended | not_yet | null, playback_state, first_played_at, last_heartbeat_at }, the same server-computed block as the admin check-in roster.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID
POST/api/v1/staff/classes/{id}/roster/{bookingId}Bearer token

Check in one attendee (staff roster)

Check one attendee into a class the caller is assigned to. Delegates to the same check-in core as the admin check-in route. Requires staff_portal.roster.view plus booking.checkin (any class in the venue) or staff_portal.check_in.own_classes (assigned or substitute instructor only). Client notification is default-silent: only an explicit non-empty notify.channels selection delivers, and it requires notifications.send plus a per-channel availability preflight before the mutation. A past venue day is refused with HISTORICAL_CORRECTION_REQUIRED — use the historical-corrections route. A streaming (online) booking is attended automatically when its stream plays: it is refused with 409 ONLINE_ATTENDANCE_AUTO unless the body sets mark_attended_override: true (the audited "Mark attended" override). Idempotency-Key is honored.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID
bookingIdstring · required
Class booking ID

Optional canonical client-notify selection (omit to stay silent); mark_attended_override for a streaming booker

Request body

{
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {}
  },
  "mark_attended_override": false
}
POST/api/v1/staff/classes/{id}/roster/{bookingId}/noshowBearer token

Mark one attendee a no-show (staff roster)

Mark one attendee of an assigned class a no-show, applying the venue no-show consequence. Delegates to the same no-show core as the admin route. Requires staff_portal.roster.view plus bookings.mark_no_show or staff_portal.check_in.own_classes; an explicit user denial of bookings.mark_no_show vetoes the own-class alternative. Default-silent client notification with notifications.send authorization and a per-channel preflight before the fee-producing write. A past venue day is refused with HISTORICAL_CORRECTION_REQUIRED. Idempotency-Key is honored so a retry replays instead of charging twice.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID
bookingIdstring · required
Class booking ID

Canonical client-notify selection

Request body

{
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}
GET/api/v1/staff/classes/{id}/roster/{bookingId}/attendanceBearer token

Attendance correction context (staff roster)

Current status, outstanding no-show fee, venue fee amount, clip consumption, currency and per-channel notification availability for one attendee of an assigned class. Also returns booking_updated_at, can_check_in_after_cutoff, ended_class_confirmation_required, credit_applicable and no_show_forfeits_clip. Requires staff_portal.roster.view and class assignment. Read-only; a degraded availability read returns notification_availability: null rather than failing the correction sheet.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID
bookingIdstring · required
Class booking ID
POST/api/v1/staff/classes/{id}/roster/{bookingId}/attendanceBearer token

Correct attendance / undo (staff roster)

Move one attendee of an assigned class to checked_in, no_show, confirmed (undo) or removed. Delegates to the same attendance-override core as the admin route and parses the same field set. Requires staff_portal.roster.view plus the target-specific grant: booking.checkin or staff_portal.check_in.own_classes for checked_in/confirmed, bookings.mark_no_show or staff_portal.check_in.own_classes for no_show, booking.cancel_member or staff_portal.cancel.own_classes for removed. Explicit user denials of the primary action veto own-class alternatives; org-wide check-in actors must hold the specific no-show/removal grant. refund_fee is refused with REFUND_REVIEW_REQUIRED (fee refunds go through the reviewed refund path). Default-silent client notification with notifications.send authorization and a per-channel preflight before the correction. A past venue day is refused with HISTORICAL_CORRECTION_REQUIRED. UUID Idempotency-Key REQUIRED, expected_updated_at and ended_class_confirmed are part of the reviewed body; changed retry intent returns 409 IDEMPOTENCY_KEY_REUSE_MISMATCH. Success preserves operationId, bookingUpdatedAt, creditDelta, seatDelta and effectsStatus; pending/held effects do not undo the correction.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID
bookingIdstring · required
Class booking ID

Target status plus the canonical client-notify selection

Request body

{
  "to": "confirmed",
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {
      "email": true
    }
  }
}
POST/api/v1/staff/classes/{id}/roster/bulkBearer token

Bulk check-in / no-show / undo (staff roster)

Apply check_in, no_show or undo to up to 200 selected attendees of an assigned class, one per-booking core call each. Requires staff_portal.roster.view plus the action grant (booking.checkin or staff_portal.check_in.own_classes; bookings.mark_no_show or staff_portal.check_in.own_classes for no_show). Explicit user denials of the primary action veto own-class alternatives. Results are truthful per booking: succeeded lists only bookings whose write returned success and failed[] carries each id with its own reason, including a booking whose selected notification channel is unavailable (that booking is not mutated). A selection containing a booking outside this class rejects the whole batch with BOOKING_SCOPE_MISMATCH before anything is attempted. Default-silent client notification with notifications.send authorization. Explicit successful check_in choices dispatch attendance_corrected from the booking-owning venue; failed, empty or legacy-only requests never dispatch. A past venue day is refused with HISTORICAL_CORRECTION_REQUIRED. UUID Idempotency-Key REQUIRED. Check-in skips streaming (online) bookings, whose attendance is recorded on stream playback, and lists them in an additive skipped array ({ booking_id, code: ONLINE_ATTENDANCE_AUTO, reason }) instead of failing them.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID

Action, roster booking ids, and the canonical client-notify selection

Request body

{
  "action": "check_in",
  "booking_ids": [
    "00000000-0000-4000-8000-0000000000b1"
  ],
  "notify": {
    "audience": {
      "clients": true
    },
    "channels": {}
  }
}
POST/api/v1/staff/classes/{id}/roster/contactBearer token

Message selected attendees (staff roster)

Send one email or SMS to an explicit subset of an assigned class's roster through the canonical consent/suppression-aware bulk senders. Requires staff_portal.roster.view plus members.contact or staff_portal.contact.own_classes, and the can_view_client_contact_info membership toggle. Submitted booking_ids are intersected server-side with this venue's contactable roster for this class; stale, cancelled and foreign ids are dropped and counted in skipped. Caller-supplied contact data is never accepted. Subject is required for email. Empty intersection returns 422 NO_RECIPIENTS. Idempotency-Key is honored so a retry cannot fan out twice.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID

Channel, roster booking ids, and message

Request body

{
  "channel": "email",
  "booking_ids": [
    "00000000-0000-4000-8000-0000000000b1"
  ],
  "subject": "Class update",
  "message": "Hi {{first_name}} — here is an update about your class."
}
GET/api/v1/staff/classes/{id}/roster/{bookingId}/historical-correctionsBearer token

Review a past class roster correction

Staff JWT, staff_portal.roster.view, scheduling.manage_history and ordinary correction permissions. Enforces assigned-instructor and tenant/class/booking binding. Query operation plus status (omit for invalidate). Returns the same signed data.preview contract as the admin booking review; no attendance or financial mutation.

Parameters, scopes and examples

Required scopes

staff_portal.roster.viewscheduling.manage_history

Path parameters

idstring · required
Class UUID
bookingIdstring · required
Booking UUID
POST/api/v1/staff/classes/{id}/roster/{bookingId}/historical-correctionsBearer token

Correct a past class roster record

Assigned staff historical correction through the canonical admin class-booking engine, with additive roster/history/ordinary permissions. Idempotency-Key must be a UUID. expected_class_updated_at and expected_updated_at are compare-and-set tokens. Existing-booking reasons and legacy REWRITE are optional. GET review_token is required for explicit fee refund/waive, consumed clip return or client/instructor Email/SMS/Push selections; finances and notifications are default-silent. Extra billing/pass/send permissions are checked before mutation. Shared notification_batch_id defers instructor delivery to the canonical admin batch-finalization endpoint and produces one summary per instructor/channel. Returns exact CAS and separate financial/delivery outcomes; failed or ambiguous sends remain held.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID
bookingIdstring · required
Class booking ID

Bounded historical roster correction with UUID Idempotency-Key

Request body

{
  "operation": "class_booking.correct_attendance_state",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_class_updated_at": "2026-08-20T09:00:00.000Z",
  "history_reason": "Signed paper roster confirms this correction",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-20T10:00:00.000Z",
  "intent": {
    "status": "checked_in"
  }
}
POST/api/v1/staff/classes/{id}/historical-correctionsBearer token

Correct an assigned past class instance

Provider-scoped atomic correction for a past class. The active provider must be assigned to the existing class; retrocreate must assign that provider directly, and assignment corrections must retain them. Assignment-only corrections preserve linked operational and financial records; other linked-class corrections require specialist review. Requires schedule.view_own, scheduling.manage_history, scheduling.manage, UUID Idempotency-Key, CAS evidence for existing rows, past effective_at, and typed REWRITE. Existing tenant, location and instructor erasure guards remain enforced. Notifications are always silent.

Parameters, scopes and examples

Path parameters

idstring · required
Class instance ID

Provider-scoped historical class-instance correction

Request body

{
  "operation": "class_instance.correct_lifecycle_state",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_status": "scheduled",
  "history_reason": "Signed teaching log confirms that this class was completed",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-20T10:00:00.000Z",
  "intent": {
    "status": "completed"
  }
}
GET/api/v1/staff/coursesBearer token

List courses (staff)

Courses in the venue with enrolled/capacity counts and the next session. Admin/manager/reception see all; instructors only the courses they staff.

GET/api/v1/staff/courses/{id}Bearer token

Course detail + roster (staff)

Course facts, roster (track chips, payment status, attendance), sessions, and the caller’s per-course permissions.

Parameters, scopes and examples

Path parameters

idstring · required
Course ID
POST/api/v1/staff/courses/{id}/sessions/{sessionId}/attendanceBearer token

Mark course-session attendance

Mark or unmark a participant present for a course session ({user_id, present}). Idempotent; writes the same attendance store the web roster uses. Supports Idempotency-Key.

Parameters, scopes and examples

Path parameters

idstring · required
Course ID
sessionIdstring · required
Class instance (session) ID

Attendance mark

Request body

{
  "user_id": "00000000-0000-0000-0000-000000000aaa",
  "present": true
}
POST/api/v1/staff/courses/{id}/messageBearer token

Message course participants

Send an email or SMS to course participants (audiences: enrolled, waitlisted, all, by track, by payment status, hand-picked). Requires course-manage scope; rate-limited; supports Idempotency-Key (retries never double-send).

Parameters, scopes and examples

Path parameters

idstring · required
Course ID

Message

Request body

{
  "channel": "email",
  "subject": "Bring a mat tomorrow",
  "message": "Hi everyone — please bring your own mat to tomorrow’s session.",
  "audience": {
    "kind": "enrolled"
  }
}
GET/api/v1/staff/availabilityBearer token

List my unavailable dates

Calling staff member's current and future unavailable dates for the selected venue. Permission: staff_portal.availability.

POST/api/v1/staff/availabilityBearer token

Set availability

Add or update unavailable dates for the calling staff member. Permission: staff_portal.availability.

Parameters, scopes and examples

Availability

Request body

{
  "unavailable_dates": [
    {
      "date": "2026-04-20",
      "reason": "Vacation"
    },
    {
      "date": "2026-04-21",
      "reason": "Vacation"
    }
  ]
}
DELETE/api/v1/staff/availabilityBearer token

Remove an unavailable date

Remove one unavailable date owned by the calling staff member. Permission: staff_portal.availability.

Parameters, scopes and examples

Query parameters

datestring
Unavailable date to remove (YYYY-MM-DD)
GET/api/v1/staff/availability/windowsBearer token

List recurring availability windows

Calling user's active recurring availability windows. Each row carries day_of_week, start_time, end_time, location_id, recurrence_type, recurrence_interval, and effective_from/until.

Parameters, scopes and examples

Query parameters

include_historicalstring
Include inactive/protected history; requires availability.manage_historyDefault: false
POST/api/v1/staff/availability/windowsBearer token

Create a recurring availability window

Create a new recurring availability window for the calling user. Body: day_of_week (0=Sun..6=Sat), start_time, end_time, optional location_id, recurrence_type (weekly|biweekly|triweekly|custom), recurrence_interval (1..12), effective_from, effective_until, notes.

Parameters, scopes and examples

Window payload

Request body

{
  "day_of_week": 1,
  "start_time": "09:00:00",
  "end_time": "12:00:00",
  "location_id": null,
  "recurrence_type": "weekly",
  "recurrence_interval": 1
}
PATCH/api/v1/staff/availability/windows/{id}Bearer token

Update a recurring availability window

Strict partial update of a current/future window using the exact updated_at token returned by GET. Caller must own the window; another instructor requires staff.edit. A stale token returns 409 STALE_TARGET. Existing or target ranges touching venue-local history fail closed until the dedicated executor is installed.

Parameters, scopes and examples

Path parameters

idstring · required
Window ID

Concurrency token plus one or more changed window fields

Request body

{
  "expected_updated_at": "2026-08-28T09:15:30.000Z",
  "start_time": "10:00:00"
}
DELETE/api/v1/staff/availability/windows/{id}Bearer token

Soft-delete a recurring availability window

Sets is_active=false on a current/future window using the exact updated_at token returned by GET, after tenant and owner-or-staff.edit authorization. A stale token returns 409 STALE_TARGET. Historical ranges fail closed until the dedicated executor is installed.

Parameters, scopes and examples

Path parameters

idstring · required
Window ID

Caller-rendered concurrency snapshot

Request body

{
  "expected_updated_at": "2026-08-28T09:15:30.000Z"
}
POST/api/v1/staff/availability/windows/{id}/historical-correctionsBearer token

Correct my availability history window

Atomic, immutable-ledger correction for a past recurring availability window owned by the active staff member. Requires availability.manage_history, staff_portal.availability, UUID Idempotency-Key, expected_updated_at plus expected_is_active for existing rows, a past effective_at, and typed REWRITE. Notifications are always silent.

Parameters, scopes and examples

Path parameters

idstring · required
Availability window ID

Self-owned historical availability correction

Request body

{
  "operation": "availability_window.invalidate",
  "expected_updated_at": "2026-08-20T09:00:00.000Z",
  "expected_is_active": true,
  "history_reason": "Approved rota confirms that this window did not apply",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-10T10:00:00.000Z",
  "intent": {}
}
GET/api/v1/staff/substitute-poolBearer token

Read substitute-pool opt-in

Returns { enabled, updated_at } for the calling user's active org.

PUT/api/v1/staff/substitute-poolBearer token

Toggle substitute-pool opt-in

Set whether the calling user is available to be auto-suggested as a substitute. Body: { enabled: boolean }. Emits substitute_pool.opt_in_changed.

Parameters, scopes and examples

Opt-in state

Request body

{
  "enabled": true
}
GET/api/v1/staff/appointmentsBearer token

My appointments

Cursor-paginated list of the calling provider's appointments, including venue currency and the same client name and 80-character provider-note preview shown in the web staff list. Contact details are not exposed. Query params: cursor (opaque next_cursor; legacy ISO timestamps are temporarily accepted), limit (1..100, default 25), status (one of the appointment status strings), direction (upcoming|past, default upcoming).

Parameters, scopes and examples

Query parameters

cursorstring
Opaque next_cursor returned by the previous page
limitnumber
Page size (1..100)Default: 25
statusstring
Optional status filter
directionstring
upcoming | pastDefault: upcoming
GET/api/v1/staff/appointments/{id}Bearer token

My assigned appointment detail

Provider-scoped detail with updated_at CAS evidence, server-authoritative historical review flags, privacy-gated client contact fields, and exact per-channel notification availability. The route always binds provider_id to the caller.

PATCH/api/v1/staff/appointments/{id}Bearer token

Act on my assigned appointment

Provider-scoped check_in, start, complete, no_show, cancel, or reschedule. Requires the action-specific appointments.*_own permission, exact expected_updated_at, and Idempotency-Key. Client notifications are default-silent and require explicit notification_channels plus notifications.send and server preflight. Past/terminal mutations fail closed until the appointment historical executor is installed.

Parameters, scopes and examples

Exact provider lifecycle intent and caller-rendered concurrency snapshot

Request body

{
  "action": "cancel",
  "expected_updated_at": "2026-08-28T09:15:30.000Z",
  "reason": "Client requested",
  "notification_channels": []
}
POST/api/v1/staff/appointments/{id}/historical-correctionsBearer token

Correct my assigned historical appointment

Provider-scoped form of the dedicated atomic appointment-history command. Requires staff_portal.appointments, appointments.manage_history, the ordinary operation permission, a UUID Idempotency-Key, REWRITE attestation, and exact expected_updated_at plus expected_status CAS for existing rows. The existing appointment and any retrocreate or assignment target must remain assigned to the active provider. Corrections are always silent. Returns 503 HISTORICAL_EXECUTOR_UNAVAILABLE without table-call fallback until correct_appointment_historical is installed.

Parameters, scopes and examples

Required scopes

appointments.manage_history

Path parameters

idstring · required
Appointment UUID

The same closed appointment correction body as the admin route

Request body

{
  "operation": "appointment.correct_no_show_state",
  "expected_updated_at": "2026-08-28T09:15:30.000Z",
  "expected_status": "confirmed",
  "history_reason": "Signed appointment record confirms this correction",
  "history_confirmation_token": "REWRITE",
  "effective_at": "2026-08-27T11:00:00.000Z",
  "intent": {
    "status": "no_show"
  }
}
GET/api/v1/staff/shiftsBearer token

My staff shifts

Calling staff member's own non-instructor shifts (reception, cleaning, manager, front desk) in a date range. Use ?from=&to= ISO datetimes; defaults to next 14 days.

Parameters, scopes and examples

Query parameters

fromstring
Start ISO datetimeDefault: now
tostring
End ISO datetimeDefault: +14 days
POST/api/v1/staff/shifts/clock-inBearer token

Clock in

Clock in to an own staff shift. Allowed from 15 min before scheduled start through 30 min after. Sets status to in_progress and stamps clock_in_at. Emits shift.clock_in.

Parameters, scopes and examples

Shift to clock in to

Request body

{
  "shift_id": "uuid"
}
POST/api/v1/staff/shiftsAPI key

Create a staff shift

Create a non-instructor staff shift. API-key only (write:staff). Body: start_time, end_time, optional staff_id, shift_type (regular | overtime | on_call | training | meeting), break_minutes, role_required, location_id, hourly_rate, notes. Idempotency-Key header honored. Emits shift.created (and shift.assigned if a staff_id is set).

Parameters, scopes and examples

Required scopes

write:staff

Shift creation payload

Request body

{
  "start_time": "2026-05-01T09:00:00Z",
  "end_time": "2026-05-01T17:00:00Z",
  "staff_id": "uuid",
  "shift_type": "regular",
  "break_minutes": 30,
  "role_required": "reception"
}
GET/api/v1/staff/shifts/{id}Bearer or API key

Get a staff shift

Fetch a single staff shift by id. JWT for the assignee or admin/manager, or API key with read:staff scope.

Parameters, scopes and examples

Required scopes

read:staff

Path parameters

idstring · required
Shift UUID
PATCH/api/v1/staff/shifts/{id}Bearer or API key

Update a staff shift

Partially update a shift. JWT (admin/manager) or API key with write:staff. Re-assigning staff_id emits shift.assigned.

Parameters, scopes and examples

Required scopes

write:staff

Path parameters

idstring · required
Shift UUID

Partial shift update

Request body

{
  "start_time": "2026-05-01T10:00:00Z",
  "staff_id": "uuid"
}
DELETE/api/v1/staff/shifts/{id}Bearer or API key

Cancel a staff shift

Soft-cancel a shift (sets status=cancelled, preserves audit/payroll references). JWT (admin/manager) or API key with write:staff. Use ?reason= to attach a cancellation reason to the audit row.

Parameters, scopes and examples

Required scopes

write:staff

Path parameters

idstring · required
Shift UUID

Query parameters

reasonstring
Cancellation reason (free text)
POST/api/v1/staff/clockBearer token

Clock in or out (unified)

Unified clock-in/out endpoint. JWT only — resolves the staff member from the session token. Body: { action: "in" | "out", shift_id }. On clock-out the response includes actual_hours and total_pay. Emits shift.clock_in or shift.clock_out.

Parameters, scopes and examples

Clock action

Request body

{
  "action": "in",
  "shift_id": "uuid"
}
POST/api/v1/staff/shifts/clock-outBearer token

Clock out

Clock out of an in-progress staff shift. Computes actual_hours, actual_break_minutes, and total_pay (when hourly_rate is set). Returns warnings for break/EU compliance issues. Emits shift.clock_out.

Parameters, scopes and examples

Shift to clock out of

Request body

{
  "shift_id": "uuid"
}
GET/api/v1/staff/time-offBearer token

My time-off requests

Latest 100 own time-off requests across all statuses.

POST/api/v1/staff/time-offBearer token

Request time off

Submit a new time-off request. Always created with status=pending. Manager approval/decline happens via the admin panel. Emits time_off.requested.

Parameters, scopes and examples

Time-off request

Request body

{
  "start_date": "2026-05-12",
  "end_date": "2026-05-19",
  "time_off_type": "vacation",
  "notes": "Family trip"
}
Leads2 documented operations
POST/api/v1/leadsPublic

Capture a lead

Create or update a lead for the venue. Public rate-limited (10 req/min/IP) or API-key authenticated (write:leads). One lead per (organization_id, lower(email)), race-proof: concurrent posts converge on one row, provided fields populate blanks and existing non-null values are preserved (first touch wins, including brand_id). Optional brand_id must be an active brand of the resolved organization (422 BRAND_NOT_FOUND). source is one of exit_intent, landing_page, referral, manual, import, api, website_form, newsletter, embed_form, offer_popup. An optional consent block (marketing_email / marketing_offer_email / marketing_sms / marketing_push, granted: true only, policy_version, mechanism, the exact prompt_text, locale; source_ip / user_agent honoured only from an API-key caller; without an API key only checkbox or button, else 400 CONSENT_MECHANISM_NOT_ALLOWED) is recorded through the canonical consent engine against the lead, with the prompt text kept verbatim, before lead.created fires, and echoed as consent_id; if it cannot be recorded the call answers 500 CONSENT_RECORD_FAILED, the lead is kept and lead.created is not sent. An unauthenticated caller always receives the same shape ({ email, source, accepted, consent_id }) whether or not the email was already a lead; an API-key caller receives the stored row with brand_id and created (true only when this call created the row). A 500 never carries database detail. Fires the lead_captured analytics event and emits a lead.created webhook with the full record (including brand_id) plus an attribution object (utm_*, fbclid, gclid, landing_page, referrer).

Parameters, scopes and examples

Required scopes

write:leads

Lead payload. Org resolves from API key > X-Organization-ID header > subdomain > organization_id.

Request body

{
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Doe",
  "phone": "+4512345678",
  "source": "website_form",
  "brand_id": "00000000-0000-4000-8000-000000000011",
  "utm_source": "meta",
  "utm_medium": "cpc",
  "utm_campaign": "spring_2026",
  "utm_content": "hero_banner",
  "utm_term": "pilates copenhagen",
  "fbclid": "abc123",
  "landing_page_url": "https://harbor-movement.example/pricing",
  "referrer_url": "https://facebook.com",
  "metadata": {
    "form_id": "newsletter_signup"
  },
  "consent": {
    "consent_type": "marketing_email",
    "granted": true,
    "policy_version": "2026-09",
    "mechanism": "checkbox",
    "prompt_text": "Yes, email me news and offers.",
    "locale": "en"
  }
}

Response example

{
  "data": {
    "id": "uuid",
    "email": "jane@example.com",
    "source": "website_form",
    "status": "new",
    "brand_id": "00000000-0000-4000-8000-000000000011",
    "created": true,
    "consent_id": "00000000-0000-4000-8000-000000000801",
    "created_at": "2026-04-17T12:00:00Z"
  },
  "error": null
}
GET/api/v1/leadsAPI key

List leads

List leads for the API key's organization. API key only (JWT not permitted). Requires the read:leads scope.

Parameters, scopes and examples

Required scopes

read:leads

Query parameters

searchstring
Search by email, first_name, or last_name
sourcestring
Filter by source (website_form, exit_intent, referral, etc.)
statusstring
Filter by status (new, contacted, converted, unsubscribed)
pageinteger
Page numberDefault: 1
limitinteger
Items per page (max 100)Default: 20
Events5 documented operations
POST/api/v1/eventsPublic

Track an analytics event

Record a server-side analytics event into user_events. Public rate-limited (60 req/min/IP) or API-key authenticated (write:events). For conversion event names (purchase, subscribe, refund, lead_captured) we additionally fire Meta CAPI + GA4 MP when the venue has pixel credentials configured.

Parameters, scopes and examples

Required scopes

write:events

Event payload. UTM + click-id + page URL get merged into event_properties.

Request body

{
  "event_name": "purchase",
  "user_id": "uuid",
  "email": "jane@example.com",
  "value": 599,
  "currency": "DKK",
  "transaction_id": "pi_abc123",
  "utm_source": "meta",
  "utm_medium": "cpc",
  "utm_campaign": "spring_2026",
  "fbclid": "abc123",
  "page_url": "https://harbor-movement.example/pricing",
  "properties": {
    "content_name": "Unlimited Monthly"
  }
}

Response example

{
  "data": {
    "id": "uuid",
    "event_name": "purchase",
    "created_at": "2026-04-17T12:00:00Z"
  },
  "error": null
}
GET/api/v1/events/{id}Bearer or API key

Community event detail

Returns a single community event with RSVP going-count and the caller’s own RSVP. Accepts a booking id or the event-type slug.

Parameters, scopes and examples

Path parameters

idstring · required
Event booking id or event-type slug

Response example

{
  "data": {
    "id": "uuid",
    "title": "Summer Social",
    "start_time": "iso",
    "going_count": 12,
    "my_rsvp": null
  },
  "error": null
}
POST/api/v1/events/{id}/rsvpBearer token

RSVP to a community event

Creates or updates the verified member’s RSVP for a scheduled community-event UUID through the shared web/member core. Requires active owning-venue membership, enabled community_events and an upcoming RSVP-only event. Send X-Organization-ID for the event venue and a stable Idempotency-Key; identical retries preserve the request and response. Guest limits and capacity may return waitlist. Returns data.rsvp with the effective status. The canonical SQL operation serializes capacity and commits the RSVP and replay receipt together. REQUEST_MISMATCH is 409; REQUEST_UNCONFIRMED is 503 and requires the original key/body.

Parameters, scopes and examples

Path parameters

idstring · required
Event booking id

Status + optional guest count + notes.

Request body

{
  "status": "going",
  "guest_count": 1,
  "notes": "Bringing my partner"
}

Response example

{
  "data": {
    "rsvp": {
      "id": "uuid",
      "status": "going",
      "guest_count": 0
    }
  },
  "error": null
}
GET/api/v1/venues/{slug}/eventsPublic

Venue event catalog or scheduled community sessions

Without view, returns the public event-type catalog: active, website-visible types with id, name, slug, description, short_description, category, pricing_model, base_price, per_person_price, min/max_participants, default_duration_minutes, image_url, location_type, featured, is_active and the purchase_channel routing fields. view=community_sessions returns upcoming scheduled occurrences of active, website-visible community types when the venue module is enabled. Session id and slug are the booking UUID; organization_id, organization_slug and timezone identify the owning venue. Attendance includes guests. No booking contact/payment/admin fields are returned. In both modes purchase_channel follows the caller-aware `X-App-Digital-Content` rule (a `both` location is native only for `none`); the catalog cache varies on that header.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue slug

Query parameters

viewstring
community_sessions for the native scheduled-event DTO

Response example

{
  "data": [
    {
      "id": "session-uuid",
      "slug": "session-uuid",
      "organization_id": "venue-uuid",
      "organization_slug": "studio",
      "timezone": "Europe/Paris",
      "title": "Community social",
      "rsvp_only": true,
      "rsvp_count": 3
    }
  ],
  "error": null
}
GET/api/v1/venues/{slug}/events/{eventSlug}Public

Venue event detail

With view=community_sessions, eventSlug must be a scheduled-session UUID owned by this venue, with the same public visibility and module gates as the session list. Returns 404 for unavailable sessions; read failure is an error, not an empty event. Without view, retains the legacy event-type slug detail and pricing tiers.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue slug
eventSlugstring · required
Event-type slug or, in community_sessions mode, scheduled-session UUID

Query parameters

viewstring
community_sessions for the native scheduled-event DTO

Response example

{
  "data": {
    "id": "session-uuid",
    "slug": "session-uuid",
    "organization_id": "venue-uuid",
    "organization_slug": "studio",
    "timezone": "Europe/Paris",
    "rsvp_only": true,
    "title": "Community social"
  },
  "error": null
}
Open Gym4 documented operations
POST/api/v1/open-gym/checkinBearer or API key

Check in to open gym

Create an open-gym session for a client. Validates an active pass with allow_open_gym=true and the access schedule. JWT users self-check in; API keys must include user_id.

Parameters, scopes and examples

Required scopes

write:bookings

Optional location, source, pass override, and notes.

Request body

{
  "location_id": "uuid",
  "source": "qr"
}

Response example

{
  "data": {
    "id": "uuid",
    "user_id": "uuid",
    "checked_in_at": "2026-04-18T10:00:00Z",
    "source": "qr"
  },
  "error": null
}
POST/api/v1/open-gym/checkoutBearer or API key

End an open-gym session

Mark an active open-gym session as checked out.

Parameters, scopes and examples

Required scopes

write:bookings

Session id to close.

Request body

{
  "session_id": "uuid"
}

Response example

{
  "data": {
    "ok": true
  },
  "error": null
}
GET/api/v1/open-gym/activeBearer or API key

List active open-gym sessions

Returns all currently checked-in open-gym sessions for the authenticated org.

Parameters, scopes and examples

Required scopes

read:bookings

Query parameters

location_idstring
Filter to a specific location

Response example

{
  "data": [
    {
      "id": "uuid",
      "user_id": "uuid",
      "checked_in_at": "iso"
    }
  ],
  "error": null
}
GET/api/v1/open-gym/statusBearer token

My open-gym status

Returns whether open gym is enabled for the org and the caller’s active (not-yet-checked-out) session, if any. Requires a user token.

Parameters, scopes and examples

Response example

{
  "data": {
    "enabled": true,
    "checked_in": true,
    "active_session": {
      "id": "uuid",
      "checked_in_at": "iso"
    }
  },
  "error": null
}
Waivers3 documented operations
GET/api/v1/waivers/requiredBearer token

List waivers the client must sign

Returns the latest version of each active required waiver for the authenticated client in the validated X-Organization-ID venue, including the exact markdown body to display and the latest version the client signed. `body_md` is canonical; `body` is the native-app compatibility alias with the same value.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "template_id": "uuid",
      "slug": "liability",
      "title": "Liability waiver",
      "body_md": "# Liability waiver v2",
      "body": "# Liability waiver v2",
      "version": 2,
      "latest_signed_version": 1,
      "needs_sign": true
    }
  ],
  "error": null
}
GET/api/v1/waivers/{templateId}Public

Waiver template detail

Returns a single active waiver template (title + markdown body + version) for display before signing. Public-readable, IP-throttled.

Parameters, scopes and examples

Path parameters

templateIdstring · required
Waiver template id

Response example

{
  "data": {
    "id": "uuid",
    "slug": "liability-2026",
    "title": "Liability waiver",
    "body_md": "# ...",
    "version": 2
  },
  "error": null
}
POST/api/v1/waivers/signBearer token

Sign a waiver

Submit the authenticated client’s signature for a waiver template in the validated X-Organization-ID venue. Idempotent via Idempotency-Key.

Parameters, scopes and examples

Template id + typed name + optional signature SVG / minor guardian fields.

Request body

{
  "template_id": "uuid",
  "typed_name": "John Doe",
  "signature_svg": "<svg/>"
}

Response example

{
  "data": {
    "id": "uuid",
    "template_id": "uuid",
    "signed_at": "iso",
    "version": 2
  },
  "error": null
}
Discounts2 documented operations
GET/api/v1/discounts/verificationsBearer token

List my discount verifications

Returns the authenticated client’s submitted discount verifications and their status.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "type_slug": "student",
      "status": "approved",
      "expires_at": "iso"
    }
  ],
  "error": null
}
POST/api/v1/discounts/verificationsBearer token

Submit a discount verification

Submit proof for a discount eligibility type. Email-domain and DOB-rule types auto-approve.

Parameters, scopes and examples

Eligibility type slug and optional document URL.

Request body

{
  "type_slug": "student",
  "document_url": "https://..."
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "pending"
  },
  "error": null
}
Challenges4 documented operations
GET/api/v1/challengesBearer or API key

List active challenges

Returns all active challenges for the current venue.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "name": "April 30",
      "kind": "individual"
    }
  ],
  "error": null
}
GET/api/v1/challenges/{id}Bearer or API key

Challenge detail

Returns a single active challenge with participant count and the caller’s own enrollment (if authed as a user).

Parameters, scopes and examples

Path parameters

idstring · required
Challenge id

Response example

{
  "data": {
    "id": "uuid",
    "name": "April 30",
    "kind": "individual",
    "participant_count": 42,
    "my_enrollment": null
  },
  "error": null
}
POST/api/v1/challenges/{id}/enrollBearer or API key

Enroll in a challenge

Enrolls the caller in the challenge. Optionally joins a team.

Parameters, scopes and examples

Path parameters

idstring · required
Challenge id

Optional team_id to join.

Request body

{
  "team_id": "uuid"
}

Response example

{
  "data": {
    "id": "uuid",
    "current_count": 0
  },
  "error": null
}
POST/api/v1/challenges/{id}/photosBearer or API key

Submit a challenge photo

Uploads (by URL) a photo to the caller’s enrollment. For transformation challenges set is_before or is_after.

Parameters, scopes and examples

Path parameters

idstring · required
Challenge id

Photo URL, optional caption, and marker flags.

Request body

{
  "enrollment_id": "uuid",
  "url": "https://...",
  "is_before": false,
  "is_after": false
}

Response example

{
  "data": {
    "id": "uuid",
    "url": "https://..."
  },
  "error": null
}
Clubs10 documented operations
GET/api/v1/clubsBearer token

List clubs

Returns active public/member clubs in the selected venue plus active invite-only memberships. is_member is true only for active membership; membership_status preserves pending/invited/left/removed.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "slug": "morning-runners",
      "name": "Morning Runners",
      "is_member": false,
      "membership_status": "pending"
    }
  ],
  "error": null
}
GET/api/v1/clubs/{id}Bearer token

Club detail

Returns a single visible club with member count and the caller’s membership status. Accepts a club id or slug.

Parameters, scopes and examples

Path parameters

idstring · required
Club id or slug

Response example

{
  "data": {
    "id": "uuid",
    "slug": "morning-runners",
    "name": "Morning Runners",
    "member_count": 18,
    "is_member": false,
    "membership_status": "pending",
    "my_membership": null
  },
  "error": null
}
POST/api/v1/clubs/{id}/joinBearer token

Join a club

Joins a club. Subject to visibility + auto-approve rules.

Parameters, scopes and examples

Path parameters

idstring · required
Club id or slug

Response example

{
  "data": {
    "club_id": "uuid",
    "status": "active"
  },
  "error": null
}
POST/api/v1/clubs/{id}/leaveBearer token

Leave a club

Removes the caller's own club membership. Idempotency-Key supported; leaving a club you are not in is a no-op success. Emits club.member_left (audit + webhook).

Parameters, scopes and examples

Path parameters

idstring · required
Club id

Response example

{
  "data": {
    "ok": true
  },
  "error": null
}
DELETE/api/v1/clubs/{id}/membershipBearer token

Leave a club (membership alias)

REST-shaped alias for POST /clubs/{id}/leave used by the mobile clubs contract — identical behavior (Idempotency-Key, no-op success when not a member, club.member_left emit).

Parameters, scopes and examples

Path parameters

idstring · required
Club id

Response example

{
  "data": {
    "ok": true
  },
  "error": null
}
GET/api/v1/clubs/{id}/membersBearer token

List active club members

Returns active members for a club in the selected venue. Invite-only rosters require the caller to be an active club member. Display names and avatars respect each member’s public-profile and privacy settings.

Parameters, scopes and examples

Path parameters

idstring · required
Club id

Response example

{
  "data": [
    {
      "user_id": "uuid",
      "display_name": "Member",
      "avatar_url": null,
      "role": "member",
      "is_admin": false
    }
  ],
  "error": null
}
GET/api/v1/clubs/suggestionsBearer token

List club suggestions (admin)

Admin queue of client-submitted club suggestions. Filter by status.

Parameters, scopes and examples

Query parameters

statusstring
pending | approved | rejected | changes_requested

Response example

{
  "data": [
    {
      "id": "uuid",
      "name": "Early Birds",
      "status": "pending"
    }
  ],
  "error": null
}
POST/api/v1/clubs/suggestionsBearer token

Suggest a new club

Members can suggest leading a new club when the venue has opted into suggestions. The suggestion appears in the admin review queue; this endpoint does not send email.

Parameters, scopes and examples

Club details + why the suggester wants to lead.

Request body

{
  "name": "Early Birds",
  "category": "running",
  "description": "Morning runners group",
  "why_lead": "I run every morning and want company"
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "pending"
  },
  "error": null
}
POST/api/v1/clubs/suggestions/{id}/approveBearer or API key

Approve a club suggestion (admin)

Approves a suggestion, creates the club, auto-assigns the suggester as leader, and posts to the feed. The member sees the status in My suggestions; this endpoint does not send email.

Parameters, scopes and examples

Path parameters

idstring · required
Suggestion id

Optional reviewer notes.

Request body

{
  "notes": "Looks great — approved."
}

Response example

{
  "data": {
    "club_id": "uuid"
  },
  "error": null
}
POST/api/v1/clubs/suggestions/{id}/rejectBearer or API key

Reject a club suggestion (admin)

Rejects the suggestion and saves the reason for the member to read in My suggestions; this endpoint does not send email.

Parameters, scopes and examples

Path parameters

idstring · required
Suggestion id

Rejection reason.

Request body

{
  "reason": "Too niche for our community right now."
}

Response example

{
  "data": {
    "status": "rejected"
  },
  "error": null
}
Chat1 documented operation
GET/api/v1/chat/channels/{id}Bearer or API key

Chat channel detail

Returns a single chat channel summary (channel row + unread_count + last_message_at) for the authenticated org. Staff role (or chat.read scope) required.

Parameters, scopes and examples

Required scopes

chat.read

Path parameters

idstring · required
Channel id

Response example

{
  "data": {
    "id": "uuid",
    "name": "#general",
    "channel_type": "public",
    "unread_count": 3,
    "last_message_at": "iso"
  },
  "error": null
}
Promotions4 documented operations
POST/api/v1/promotions/codes/lookupAPI key

Read a personally issued code

A read with the code in the request body (codes never travel in URLs). The state of a code issued by the issue endpoint: its status, deadline (valid_until / deadline_set_at), redemption time, contact and the caller's metadata, plus in_checkout (a checkout holds it), usable and unavailable_reason (redeemed, voided, expired, cancelled, promotion_inactive or promotion_ended) and the server time. Other codes of the venue are never visible (404 CODE_NOT_FOUND). API key only with read:promo_codes or write:promo_codes, 120 requests/min per key.

Parameters, scopes and examples

Required scopes

read:promo_codes

The issued code

Request body

{
  "code": "ACME-7KQ2M9XW"
}

Response example

{
  "data": {
    "assignment_id": "00000000-0000-4000-8000-000000000701",
    "promo_code_id": "00000000-0000-4000-8000-000000000101",
    "code": "ACME-7KQ2M9XW",
    "batch_id": "00000000-0000-4000-8000-000000000031",
    "promotion_id": "00000000-0000-4000-8000-000000000021",
    "brand_id": "00000000-0000-4000-8000-000000000011",
    "lead_id": "00000000-0000-4000-8000-000000000501",
    "metadata": {
      "cta_url": "https://www.acme-yoga.example/offer/t1",
      "generation": 1
    },
    "contact_email": "jane@example.com",
    "contact_name": "Jane Doe",
    "code_status": "allocated",
    "issued_at": "2026-09-24T10:00:00Z",
    "valid_until": null,
    "deadline_set_at": null,
    "redeemed_at": null,
    "usable": true,
    "unavailable_reason": null,
    "server_time": "2026-09-24T10:00:00Z"
  },
  "error": null
}
POST/api/v1/promotions/batches/{batchId}/issueAPI key

Issue the next free code of a batch to one contact

Atomically hands the next unissued code of a ready, non-partner, unexported batch that the venue flagged for personal issuance (batch metadata personal_issuance = true) to one contact and activates it. Optional initial_valid_minutes (1–43200) lets an unopened code expire by itself; lead_id must belong to the same email. The same contact (case-insensitive email) always receives the same code (200, reused: true); the batch size is the budget (409 BATCH_EXHAUSTED). Redemption requires the buyer to use the same email. Caller metadata is stored verbatim and echoed in responses and promo_code.* webhooks. Emits promo_code.issued for a new code. API key only (write:promo_codes), 60 requests/min per key, Idempotency-Key required (409 IDEMPOTENCY_CONFLICT when reused for another contact or batch).

Parameters, scopes and examples

Required scopes

write:promo_codes

Path parameters

batchIduuid · required
Promotion code batch id

The contact the code is issued to

Request body

{
  "contact_email": "jane@example.com",
  "contact_name": "Jane Doe",
  "brand_id": "00000000-0000-4000-8000-000000000011",
  "lead_id": "00000000-0000-4000-8000-000000000501",
  "initial_valid_minutes": 10080,
  "metadata": {
    "cta_url": "https://www.acme-yoga.example/offer/t1",
    "generation": 1
  }
}

Response example

{
  "data": {
    "assignment_id": "00000000-0000-4000-8000-000000000701",
    "promo_code_id": "00000000-0000-4000-8000-000000000101",
    "code": "ACME-7KQ2M9XW",
    "batch_id": "00000000-0000-4000-8000-000000000031",
    "promotion_id": "00000000-0000-4000-8000-000000000021",
    "brand_id": "00000000-0000-4000-8000-000000000011",
    "lead_id": "00000000-0000-4000-8000-000000000501",
    "metadata": {
      "cta_url": "https://www.acme-yoga.example/offer/t1",
      "generation": 1
    },
    "contact_email": "jane@example.com",
    "contact_name": "Jane Doe",
    "code_status": "allocated",
    "issued_at": "2026-09-24T10:00:00Z",
    "valid_until": null,
    "deadline_set_at": null,
    "redeemed_at": null,
    "reused": false,
    "remaining": 41
  },
  "error": null
}
POST/api/v1/promotions/codes/deadlineAPI key

Set an issued code's deadline once

The code travels in the request body. The first call sets valid_until = now + minutes (1–1440, chosen by the caller) unless an earlier deadline already exists, and stamps deadline_set_at; every later call changes nothing and returns the stored values (applied: false), so a page refresh cannot restart the clock. Only codes issued by the issue endpoint are visible (404 CODE_NOT_FOUND otherwise); a used, voided or expired code answers 422 CODE_UNAVAILABLE. Every redemption path enforces valid_until. Emits promo_code.deadline_set when applied. API key only (write:promo_codes), 120 requests/min per key.

Parameters, scopes and examples

Required scopes

write:promo_codes

The issued code (body only) and the deadline length in minutes

Request body

{
  "code": "ACME-7KQ2M9XW",
  "minutes": 30
}

Response example

{
  "data": {
    "promo_code_id": "00000000-0000-4000-8000-000000000101",
    "code": "ACME-7KQ2M9XW",
    "valid_until": "2026-09-24T10:30:00Z",
    "deadline_set_at": "2026-09-24T10:00:00Z",
    "applied": true,
    "changed": true,
    "server_time": "2026-09-24T10:00:00Z"
  },
  "error": null
}
POST/api/v1/promotions/codes/sendAPI key

Email an issued code to its contact

Sends the brand-styled promo_code_personal email to the code's own contact (the recipient is never caller-supplied) through the canonical templated pipeline: suppression and marketing unsubscribes honoured, RFC 8058 footer, notifications_log row, sender resolved brand → venue → platform. The caller supplies the https landing URL and may supply its own subject, plain-text message and button label (rendered verbatim, escaped); Booking Bible-authored words are English. With include_code false the email carries the call to action only. When the code belongs to a lead, that lead needs an active marketing_offer_email or marketing_email consent (else 422 CONSENT_REQUIRED); at most 3 emails per code per 24 hours (429 SEND_LIMIT_REACHED); an archived stored brand falls back to the venue sender. Answers 200 with status sent or suppressed, 502 EMAIL_SEND_FAILED when the pipeline failed (retry with the same Idempotency-Key), 422 CODE_UNAVAILABLE for a dead code. Emits promo_code.sent. API key only (write:promo_codes), 30 requests/min per key, Idempotency-Key required.

Parameters, scopes and examples

Required scopes

write:promo_codes

The issued code (body only), the landing URL and optional tenant copy

Request body

{
  "code": "ACME-7KQ2M9XW",
  "include_code": false,
  "cta_url": "https://www.acme-yoga.example/offer/t1",
  "locale": "en",
  "subject": "Your personal offer",
  "message": "Thanks for your interest — here is your code.",
  "cta_label": "See your offer"
}

Response example

{
  "data": {
    "status": "sent",
    "reason": null,
    "notification_id": "00000000-0000-4000-8000-000000000901",
    "message_id": "re_123",
    "content_language": "en"
  },
  "error": null
}
Barcodes7 documented operations
GET/api/v1/barcodes/lookupBearer or API key

Look up a barcode at POS

Resolve a scanned barcode to a mapping (pass, product, gift card). Exact match first, then longest prefix match.

Parameters, scopes and examples

Required scopes

pos.sell

Query parameters

codestring · required
The scanned barcode string

Response example

{
  "data": {
    "mapping": {
      "id": "uuid",
      "barcode": "7340999000001",
      "target_type": "pass_type",
      "target_id": "uuid",
      "target_label": "HYC Monthly Unlimited",
      "is_active": true
    },
    "match_type": "exact"
  },
  "error": null
}
GET/api/v1/barcodes/mappingsBearer or API key

List barcode mappings

List all barcode mappings for the venue.

Parameters, scopes and examples

Required scopes

pos.manage_barcodes

Query parameters

target_typestring
Filter by target type
batch_namestring
Filter by batch name
active_onlyboolean
Only active mappings
limitinteger
Max rows (≤500)Default: 100

Response example

{
  "data": {
    "data": [],
    "total": 0
  },
  "error": null
}
POST/api/v1/barcodes/mappingsBearer or API key

Create a barcode mapping

Map a barcode to a pass type, product, or gift card. Supports Idempotency-Key.

Parameters, scopes and examples

Required scopes

pos.manage_barcodes

Mapping details

Request body

{
  "barcode": "7340999000001",
  "match_type": "exact",
  "target_type": "pass_type",
  "target_id": "uuid",
  "target_label": "HYC Monthly Unlimited"
}

Response example

{
  "data": {
    "id": "uuid"
  },
  "error": null
}
PATCH/api/v1/barcodes/mappings/{id}Bearer or API key

Update a barcode mapping

Update label, target, activation state, or batch for an existing mapping.

Parameters, scopes and examples

Required scopes

pos.manage_barcodes

Path parameters

idstring · required
Mapping id

Fields to update

Request body

{
  "target_label": "Updated label",
  "is_active": false
}

Response example

{
  "data": {
    "id": "uuid"
  },
  "error": null
}
DELETE/api/v1/barcodes/mappings/{id}Bearer or API key

Deactivate a barcode mapping

Soft-delete a mapping so it no longer resolves. History is preserved.

Parameters, scopes and examples

Required scopes

pos.manage_barcodes

Path parameters

idstring · required
Mapping id

Response example

{
  "data": {
    "deactivated": true
  },
  "error": null
}
POST/api/v1/barcodes/bulk-scanBearer or API key

Bulk Scan Session — map a single scanned barcode to a preset target

Hot-path endpoint for the rapid-mapping Bulk Scan Session. Each call maps one scanned barcode. Returns status=created (new mapping), duplicate_in_session (same barcode+target already exists), requires_confirmation (different target, resend with allow_overwrite=true to proceed), or overwritten.

Parameters, scopes and examples

Required scopes

pos.manage_barcodes

One scan at a time

Request body

{
  "barcode": "7340999000042",
  "target_type": "pass_type",
  "target_id": "uuid",
  "target_label": "HYC Monthly Unlimited",
  "batch_name": "Blue cards — Summer 2026",
  "allow_overwrite": false
}

Response example

{
  "data": {
    "status": "created",
    "mapping": {
      "id": "uuid",
      "barcode": "7340999000042"
    }
  },
  "error": null
}
POST/api/v1/barcodes/importBearer or API key

Bulk import barcode mappings

Import up to 5000 mappings at once. Duplicates are skipped, per-row errors returned.

Parameters, scopes and examples

Required scopes

pos.manage_barcodes

Array of rows

Request body

{
  "rows": [
    {
      "barcode": "7340999000001",
      "target_type": "pass_type",
      "target_id": "uuid",
      "target_label": "HYC Monthly Unlimited"
    }
  ]
}

Response example

{
  "data": {
    "imported": 847,
    "skipped": 12,
    "errors": []
  },
  "error": null
}
Room Rentals7 documented operations
GET/api/v1/roomsBearer or API key

List rental-enabled rooms

Returns every room with rental_enabled=true and is_active=true for the resolved org. Same data as the public /spaces page.

Parameters, scopes and examples

Query parameters

organization_iduuid
Override the caller's default org.

Response example

{
  "data": {
    "count": 2,
    "rooms": []
  },
  "error": null
}
GET/api/v1/rooms/[slug]Bearer or API key

Room rental detail

Full room detail (gallery, amenities, rate table, terms). Same data as /spaces/[slug].

Parameters, scopes and examples

Path parameters

slugstring · required
Room slug.

Response example

{
  "data": {
    "id": "uuid",
    "name": "Studio A",
    "hourly_rate": 250
  },
  "error": null
}
GET/api/v1/rooms/[slug]/availabilityBearer or API key

Room availability slots

30-min availability grid for the requested date. Busy slots are coarsely flagged — blocking_source is redacted so PII never leaks.

Parameters, scopes and examples

Query parameters

datestring · required
YYYY-MM-DD

Response example

{
  "data": {
    "count": 24,
    "date": "2026-05-01",
    "slots": []
  },
  "error": null
}
GET/api/v1/room-rentalsAPI key

List rentals

List room rentals for the API key's org. Filter by status / room_id / renter_id / start_after / start_before.

Parameters, scopes and examples

Required scopes

read:room_rentals

Response example

{
  "data": {
    "count": 12,
    "rentals": []
  },
  "error": null
}
POST/api/v1/room-rentalsAPI key

Create rental

Create a room rental. Conflicts checked against every event source. Idempotency-Key honored.

Parameters, scopes and examples

Required scopes

write:room_rentals

Rental input

Request body

{
  "room_id": "uuid",
  "renter_id": "uuid",
  "start_time": "2026-05-01T10:00:00Z",
  "end_time": "2026-05-01T12:00:00Z",
  "pricing_model": "hourly"
}

Response example

{
  "data": {
    "id": "uuid",
    "total_amount": 500,
    "status": "pending"
  },
  "error": null
}
GET/api/v1/room-rentals/[id]API key

Rental detail

Full rental detail.

Parameters, scopes and examples

Required scopes

read:room_rentals

Path parameters

iduuid · required
Rental id.

Response example

{
  "data": {
    "id": "uuid",
    "status": "confirmed"
  },
  "error": null
}
DELETE/api/v1/room-rentals/[id]API key

Cancel rental

Cancel a rental. Optional ?reason= is recorded; ?waive_fee=1 skips the cancellation fee.

Parameters, scopes and examples

Required scopes

write:room_rentals

Path parameters

iduuid · required
Rental id.

Query parameters

reasonstring
Cancellation reason (free text).
waive_feestring
Set to "1" to skip the cancellation fee.

Response example

{
  "data": {
    "id": "uuid",
    "status": "cancelled",
    "cancellation_fee_amount": 250
  },
  "error": null
}
MCP2 documented operations
POST/api/mcpAPI key

MCP JSON-RPC endpoint

Model Context Protocol server (HTTP transport, JSON-RPC 2.0, protocol 2025-03-26). Authenticate with `X-API-Key`. Methods: `initialize`, `ping`, `resources/list`, `resources/read`, `tools/list`, `tools/call`. Read-only in v1. See `/developers/mcp` for the full guide.

Parameters, scopes and examples

Required scopes

read:scheduleread:membersread:bookingsread:passesread:reportsread:operations

JSON-RPC 2.0 request (or array for batching).

Request body

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}

Response example

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "search_schedule",
        "description": "…",
        "inputSchema": {
          "type": "object"
        }
      }
    ]
  }
}
GET/api/mcpPublic

MCP manifest

Tiny manifest so curl + browser tabs can sanity-check the endpoint exists. Real traffic is POST.

Parameters, scopes and examples

Response example

{
  "ok": true,
  "name": "booking-bible",
  "transport": "http",
  "protocol": "2025-03-26",
  "docs": "/docs/mcp"
}
Feature Toggles2 documented operations
GET/api/v1/admin/featuresBearer or API key

List feature modules + effective state

Returns every registered feature module with its resolved enabled/settings/source for the calling venue. Resolution honors the four-tier precedence (tenant override → group lock → venue → group default → plan → default). Mobile Business app uses this for parity with /admin/settings/features.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "key": "leaderboards",
      "label": "Leaderboards",
      "description": "Member-facing leaderboards by class type, period, and metric.",
      "category": "Community",
      "enabled": true,
      "source": "plan",
      "locked_by_group": false,
      "settings": {}
    }
  ]
}
PATCH/api/v1/admin/features/[moduleKey]Bearer or API key

Update a venue-level feature toggle

Flip enabled/settings for a feature module at the venue tier. Idempotency-Key supported. Returns 400 with `Locked by group: <paths>` when the venue tries to flip a group-locked toggle or write to a group-locked dot-path in `settings`. Audit-logged + emits `feature_toggle.changed` webhook.

Parameters, scopes and examples

Required scopes

write:settings

Path parameters

moduleKeystring · required
Module key from feature_modules.key

Partial update — only the fields you want to change.

Request body

{
  "enabled": true,
  "settings": {
    "window_days": 7
  }
}

Response example

{
  "data": {
    "ok": true
  },
  "error": null
}
Network17 documented operations
GET/api/v1/network/discoverBearer token

Discover network venues

Browse partner venues available to the member across the BOOKING BIBLE network. Each entry exposes a public summary plus the exact relationship status, active partnership id, and venue-level bookable flag for the organization selected by X-Organization-ID. Member-JWT.

Parameters, scopes and examples

Query parameters

searchstring
Filter venues by name, slug, or city
limitinteger
Max venues to returnDefault: 50

Response example

{
  "data": [
    {
      "id": "uuid",
      "name": "Northside Fitness Vesterbro",
      "slug": "northside-fitness-vesterbro",
      "city": "København",
      "country_code": "DK",
      "class_type_count": 12,
      "member_count": 140,
      "is_partnered": true,
      "partnership_status": "active",
      "active_partnership_id": "uuid",
      "is_bookable": true
    }
  ],
  "error": null
}
GET/api/v1/network/creditsBearer token

Network credits

The caller’s network visit credits for the current calendar month — how many partner-venue visits remain under the partnership terms. Member-JWT.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "user_id": "uuid",
      "partnership_id": "uuid",
      "period_start": "2026-06-01",
      "visits_used": 1,
      "max_visits": 4
    }
  ],
  "error": null
}
POST/api/v1/network/bookBearer token

Book a network class

Book a class at a partner venue using a network-eligible pass. Resolves the legal gate against the HOST venue’s documents before booking. Requires a caller-stable `Idempotency-Key` header; exact retries return the original booking and visit. Member-JWT.

Parameters, scopes and examples

Network booking

Request body

{
  "class_instance_id": "uuid",
  "pass_id": "uuid",
  "partnership_id": "uuid"
}

Response example

{
  "data": {
    "booking_id": "uuid",
    "status": "confirmed",
    "network_visit_id": "uuid"
  },
  "error": null
}
POST/api/v1/network/book/closeBearer token

Resolve an interrupted network booking before choosing again

Uses the original three-ID booking body and original Idempotency-Key. Atomically closes an uncommitted attempt so a delayed request cannot book it, or recovers the original completed booking. Never cancels an existing booking. Member-JWT only; no mutable catalog or legal preflight can replace the database proof.

Parameters, scopes and examples

Exact original network booking request

Request body

{
  "class_instance_id": "uuid",
  "pass_id": "uuid",
  "partnership_id": "uuid"
}

Response example

{
  "data": {
    "kind": "closed",
    "operation_id": "uuid",
    "request_hash": "sha256",
    "idempotency_key": "original-key",
    "request": {
      "class_instance_id": "uuid",
      "pass_id": "uuid",
      "partnership_id": "uuid"
    }
  },
  "error": null
}
GET/api/v1/network/partnershipsBearer or API key

List venue Network partnerships

Lists venue-to-venue Network partnerships for the authenticated venue. Bearer JWT requires network.view; API keys require read:network.

Parameters, scopes and examples

Required scopes

read:network

Response example

{
  "data": [
    {
      "id": "uuid",
      "status": "active",
      "partnership_kind": "network"
    }
  ],
  "error": null
}
POST/api/v1/network/partnershipsBearer or API key

Create a venue Network partnership request

Creates a venue-to-venue Network partnership request through the context-free Network mutation core. Bearer JWT requires network.manage; API keys require write:network.

Parameters, scopes and examples

Required scopes

write:network

Network partnership request

Request body

{
  "receiving_org_id": "uuid",
  "model": "pay_per_visit",
  "terms": {},
  "binding_months": 3
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "pending",
    "partnership_kind": "network"
  },
  "error": null
}
GET/api/v1/network/partnerships/{id}Bearer or API key

Read a venue Network partnership

Reads one venue-to-venue Network partnership after proving the caller belongs to either party. Bearer JWT requires network.view; API keys require read:network.

Parameters, scopes and examples

Required scopes

read:network

Path parameters

iduuid · required
Network partnership id

Response example

{
  "data": {
    "id": "uuid",
    "status": "active",
    "partnership_kind": "network"
  },
  "error": null
}
PATCH/api/v1/network/partnerships/{id}Bearer or API key

Update a venue Network partnership

Updates a venue-to-venue Network partnership through the commercial lifecycle. Bearer JWT requires network.manage. API keys with write:network may change lifecycle status, but agreement negotiation and terms revisions require a verified human JWT administrator and return 403 for API-key callers.

Parameters, scopes and examples

Required scopes

write:network

Path parameters

iduuid · required
Network partnership id

Network partnership action or terms update

Request body

{
  "action": "terminated",
  "reason": "Partnership ended"
}

Response example

{
  "data": {
    "id": "uuid",
    "status": "terminated",
    "partnership_kind": "network"
  },
  "error": null
}
DELETE/api/v1/network/partnerships/{id}Bearer or API key

Terminate a venue Network partnership

Terminates immediately only when binding and notice have both elapsed; otherwise schedules termination through the locked service RPC. Bearer JWT requires network.manage; API keys require write:network.

Parameters, scopes and examples

Required scopes

write:network

Path parameters

iduuid · required
Network partnership id

Network termination reason

Request body

{
  "reason": "Partnership ended"
}

Response example

{
  "data": {
    "id": "uuid",
    "scheduled_termination_date": "2026-09-26T00:00:00.000Z"
  },
  "error": null
}
GET/api/v1/network/partnerships/{id}/visitsBearer or API key

List visits for a venue Network partnership

Lists visit ledger rows for one venue-to-venue Network partnership after proving the caller belongs to either party. Bearer JWT requires network.view; API keys require read:network.

Parameters, scopes and examples

Required scopes

read:network

Path parameters

iduuid · required
Network partnership id

Query parameters

fromstring
Lower created_at bound
tostring
Upper created_at bound
limitinteger
Max rows to returnDefault: 50

Response example

{
  "data": [
    {
      "id": "uuid",
      "partnership_id": "uuid",
      "status": "completed"
    }
  ],
  "error": null
}
GET/api/v1/network/partnerships/{id}/settlementsBearer or API key

List settlements for a venue Network partnership

Lists network-only settlements for one venue-to-venue Network partnership after proving the caller belongs to either party. Bearer JWT requires network.view; API keys require read:network.

Parameters, scopes and examples

Required scopes

read:network

Path parameters

iduuid · required
Network partnership id

Query parameters

limitinteger
Max rows to returnDefault: 50

Response example

{
  "data": [
    {
      "id": "uuid",
      "settlement_kind": "network",
      "partnership_id": "uuid"
    }
  ],
  "error": null
}
GET/api/v1/admin/network/professionalsBearer token

List or look up professional collaborations

For a venue workspace, lists its professional collaborations with network.view. For a selected individual workspace, an active member may read only rows where the Bearer user is practitioner_user_id and that workspace is the receiving org; person history remains readable when teacher_settlements is off and does not require network.view. Person writes separately require active admin membership. Exact-email invitation lookup is venue-only and requires network.manage. Unknown workspace or database authority fails closed. Each row also carries `status_changed_at`, and venue-direction rows carry `reinvite_option` (`reopen` = ended or declined and Re-invite is possible, `cooldown` = declined under 30 days ago with `reinvite_available_at`, `resend` = pending, Resend invitation; null otherwise). The email lookup returns `existing_collaboration_id` + `existing_collaboration_status` for an open or active collaboration and `ended_collaboration` ({id, status, reopenable, retry_at}) for the latest ended or declined one, so clients offer Re-invite instead of a new invitation.

Parameters, scopes and examples

Query parameters

emailstring
Exact professional email lookup for an invitation

Response example

{
  "data": [
    {
      "id": "uuid",
      "direction": "venue",
      "status": "active",
      "venue_org_id": "uuid",
      "venue_name": "Northside Fitness",
      "practitioner_org_id": "uuid",
      "practitioner_org_name": "Alex Professional",
      "professional_name": "Alex Morgan",
      "membership_role": "instructor",
      "comp_model": "per_class_flat",
      "rate_per_class": 500
    }
  ],
  "error": null
}
POST/api/v1/admin/network/professionalsBearer token

Invite a professional

Creates a pending venue-to-professional collaboration with an explicit venue role and compensation model. Requires network.manage and a non-individual venue workspace. When the venue already has a collaboration with this professional (the venue-professional pair is unique regardless of status) it returns 422 `COLLABORATION_EXISTS` with details {partnership_id, status, reopenable, retry_at}: link an open collaboration, or use POST /reopen for an ended or declined one.

Parameters, scopes and examples

Professional collaboration invitation

Request body

{
  "practitioner_org_id": "uuid",
  "practitioner_user_id": "uuid",
  "membership_role": "instructor",
  "comp_model": "per_class_flat",
  "rate_per_class": 500
}

Response example

{
  "data": {
    "partnership_id": "uuid"
  },
  "error": null
}
PATCH/api/v1/admin/network/professionals/{id}Bearer token

Update a professional collaboration

The signed-in practitioner may accept, decline, or terminate their own collaboration when they are an active admin of its receiving individual workspace. These person actions use the canonical lifecycle without the venue network.manage or teacher_settlements gate. A venue with network.manage may set role, pause, resume, or terminate its relationship. A person cannot keep their former venue staff membership on termination.

Parameters, scopes and examples

One collaboration lifecycle action

Request body

{
  "action": "set_role",
  "membership_role": "manager"
}

Response example

{
  "data": {
    "updated": true
  },
  "error": null
}
DELETE/api/v1/admin/network/professionals/{id}Bearer token

Terminate a professional collaboration

Ends the relationship through the canonical termination core. The signed-in practitioner may end only their own row while an active admin of its receiving individual workspace; their venue membership is suspended. A venue with network.manage may retain former staff only by explicitly setting keep_membership true.

Parameters, scopes and examples

Optional termination reason and membership handling

Request body

{
  "reason": "Engagement ended",
  "keep_membership": false
}

Response example

{
  "data": {
    "membership_deactivated": true
  },
  "error": null
}
POST/api/v1/admin/network/professionals/{id}/reopenBearer token

Re-invite a professional (reopen an ended collaboration)

COLLAB-REINVITE-01. The selected venue workspace (network.manage, enforced before any read or write) reopens its own ended (terminated) or declined professional collaboration as pending, keeping the existing role and terms, and the professional receives a new request email. A professional workspace gets 403. The body must be empty: terms are edited afterwards on the pending collaboration. Declined collaborations can be reopened 30 days after the decision; pending or active ones refuse. Requires the platform `teacher_settlements` rollout (not the directory `collaboration_requests` switch) and the professional must still own their professional workspace. Consumes one unit of the venue daily collaboration-request budget (10 per rolling day, shared with directory requests). Audits `network.collaboration_reopened`; the email outcome is reported separately as `delivery`.

Parameters, scopes and examples

Path parameters

iduuid · required
Venue-owned collaboration id

Response example

{
  "data": {
    "partnership_id": "uuid",
    "status": "pending",
    "previous_status": "terminated",
    "delivery": "sent"
  },
  "error": null
}
POST/api/v1/admin/network/professionals/{id}/resend-invitationBearer token

Resend a pending collaboration invitation

COLLAB-REINVITE-01. The selected venue workspace (network.manage, enforced first) re-sends the request email for its own still-pending professional collaboration, for example when the first email never arrived. No lifecycle change. One resend per collaboration per 10 minutes; each resend consumes one unit of the venue daily collaboration-request budget. The body must be empty. Requires the `teacher_settlements` rollout. Audits `network.collaboration_invitation_resent` with the delivery outcome.

Parameters, scopes and examples

Path parameters

iduuid · required
Venue-owned collaboration id

Response example

{
  "data": {
    "partnership_id": "uuid",
    "status": "pending",
    "previous_status": "pending",
    "delivery": "sent"
  },
  "error": null
}
Relationships3 documented operations
GET/api/v1/me/relationshipsBearer token

List my relationships

List the caller’s relationships (bidirectional — both relationships the member created and ones pointing back at them), hydrated with the linked member’s profile. Unlocks family pricing, shared booking, and pass sharing. Member-JWT, org-scoped.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "user_id": "uuid",
      "related_user_id": "uuid",
      "related_name": "Sam Doe",
      "related_email": "sam@example.com",
      "relationship_type": "family_member",
      "status": "active",
      "related_profile": {
        "first_name": "Sam",
        "last_name": "Doe",
        "email": "sam@example.com",
        "phone": null
      }
    }
  ],
  "error": null
}
POST/api/v1/me/relationshipsBearer token

Add a relationship

Add a relationship. When `related_email` matches a member in the same venue, the relationship links to their profile and a mirror row is written so both members see it. Member-JWT. Original Idempotency-Key is required. Exact token/body/key replays the immutable accepted or closed receipt; unknown outcomes retain the original request.

Parameters, scopes and examples

Relationship

Request body

{
  "related_email": "sam@example.com",
  "related_name": "Sam Doe",
  "relationship_type": "family_member",
  "company_name": null
}

Response example

{
  "data": {
    "id": "uuid"
  },
  "error": null
}
DELETE/api/v1/me/relationshipsBearer token

Revoke a relationship

Revoke a relationship the caller is a party to (and its mirror row). Member-JWT.

Parameters, scopes and examples

Query parameters

relationship_idstring · required
Relationship id to revoke

Response example

{
  "data": {
    "id": "uuid",
    "status": "revoked"
  },
  "error": null
}
Private Events9 documented operations
GET/api/v1/venues/{slug}/event-typesPublic

List private-event types

Public catalog of bookable private-event types for a venue (active + shown on website). Used by the app inquiry screen. Cached, IP-throttled. `purchase_channel` derives from `location_type` (online/both web, in_studio/offsite native); with `X-App-Digital-Content: none` a `both` type reads `native`, and the cache varies on that header.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue slug

Response example

{
  "data": [
    {
      "id": "uuid",
      "name": "Private Group Yoga",
      "slug": "private-group-yoga",
      "tagline": "Book the studio for your team",
      "category": "corporate",
      "min_participants": 5,
      "max_participants": 30,
      "default_duration_minutes": 90,
      "pricing_model": "per_person",
      "base_price": 0,
      "per_person_price": 250,
      "currency": "DKK",
      "deposit_required": true,
      "deposit_amount": 1000
    }
  ],
  "error": null
}
GET/api/v1/venues/{slug}/event-types/{eventSlug}Public

Private-event type detail

Public detail for a single private-event type (active + shown on website). 404 for hidden/draft types. Cached, IP-throttled. `purchase_channel` follows the same caller-aware `X-App-Digital-Content` rule as the list.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue slug
eventSlugstring · required
Event-type slug

Response example

{
  "data": {
    "id": "uuid",
    "name": "Private Group Yoga",
    "slug": "private-group-yoga"
  },
  "error": null
}
POST/api/v1/private-events/inquireBearer token

Create a private-event inquiry

Submit a private-event inquiry as the authenticated member. Validates participant count against the event type’s min/max, computes pricing, and inserts a `private_event_bookings` row with `booked_by` set; emits `private_event.inquiry_created`. Member-JWT, org from X-Organization-ID. Idempotency-Key supported.

Parameters, scopes and examples

Inquiry

Request body

{
  "event_type_id": "uuid",
  "participant_count": 12,
  "start_time": "2026-07-01T17:00:00.000Z",
  "occasion": "Team offsite",
  "special_requests": "Mats for 12",
  "selected_addons": [
    {
      "name": "Smoothies",
      "price": 60,
      "qty": 12
    }
  ]
}

Response example

{
  "data": {
    "id": "uuid"
  },
  "error": null
}
GET/api/v1/private-eventsBearer token

List my private-event bookings

List the member’s private-event bookings (scoped by `contact_email` == the caller’s email). Member-JWT.

Parameters, scopes and examples

Response example

{
  "data": [
    {
      "id": "uuid",
      "status": "inquiry"
    }
  ],
  "error": null
}
GET/api/v1/private-events/{id}Bearer token

Private-event booking detail

Detail of one of the member’s private-event bookings. Member-JWT.

Parameters, scopes and examples

Path parameters

idstring · required
Booking id

Response example

{
  "data": {
    "id": "uuid",
    "status": "inquiry"
  },
  "error": null
}
DELETE/api/v1/private-events/{id}Bearer token

Cancel a private-event booking

Cancel one of the member’s private-event bookings. Member-JWT.

Parameters, scopes and examples

Path parameters

idstring · required
Booking id

Response example

{
  "data": {
    "id": "uuid",
    "status": "cancelled"
  },
  "error": null
}
POST/api/v1/private-events/{id}/payBearer token

Get a deposit/full payment intent for a booking

PROMPT_11 — returns the Stripe `client_secret`, frozen `merchant_country_code`, and `stripe_account_id` (`acct_*` for a direct Connect PI, otherwise null) for the booking’s deposit/full charge so the member can initialize Stripe Elements in the exact payment context. Customer/ephemeral-key credentials are returned only when this member owns the customer frozen by the first payment operation; admin-created or another accepted booker identity receives a safe generic sheet with null customer credentials. Idempotent (reuses the frozen PaymentIntent execution created at confirmation). `{ skipped: true }` when the event type’s payment_mode is `none`. Member-JWT; ownership by contact_email.

Parameters, scopes and examples

Path parameters

idstring · required
Booking id

Response example

{
  "data": {
    "client_secret": "pi_..._secret_...",
    "amount": 250,
    "currency": "DKK",
    "payment_kind": "deposit",
    "customer_id": "cus_...",
    "ephemeral_key": "ek_...",
    "stripe_account_id": "acct_... (direct mode; otherwise null)",
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4"
  },
  "error": null
}
POST/api/v1/private-events/{id}/approveBearer token

Member approves a quoted booking and gets Payment Sheet credentials

The member’s “Approve & pay” CTA: the booker confirms a quote the venue sent (status `quoted`) and receives the same frozen PaymentIntent, `merchant_country_code`, and `stripe_account_id` (`acct_*` only for direct Connect; otherwise null). Customer/ephemeral-key credentials are returned only to the user id that owns the frozen Stripe customer; another accepted booking identity receives a generic sheet with null customer credentials. Member-JWT; ownership by `booked_by` or `contact_email`. Idempotency-Key header REQUIRED — a retry re-enters the repairable confirmation pipeline and returns the same frozen execution (no duplicate PI, account drift, or regional drift). Honors the event type’s payment_mode via the shared helper; `{ skipped: true }` when payment_mode is `none` (invoice path).

Parameters, scopes and examples

Path parameters

idstring · required
Booking id

Response example

{
  "data": {
    "status": "confirmed",
    "client_secret": "pi_..._secret_...",
    "payment_intent_id": "pi_...",
    "customer_id": "cus_...",
    "ephemeral_key": "ek_...",
    "stripe_account_id": "acct_... (direct mode; otherwise null)",
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4",
    "amount": 250,
    "currency": "DKK",
    "payment_kind": "deposit"
  },
  "error": null
}
GET/api/v1/widget/private-sessionsPublic

Private-sessions widget catalog

PROMPT_11 — public catalog of a venue’s active, publicly-listed private-session types for the embeddable widget (/embed/private-sessions). Cross-origin access is governed by the platform dynamic CORS allowlist (venue custom domains). Cached, IP-throttled.

Parameters, scopes and examples

Query parameters

venuestring · required
Venue slug

Response example

{
  "data": {
    "venue": {
      "slug": "harbor-movement",
      "name": "Harbor Movement"
    },
    "event_types": [
      {
        "id": "uuid",
        "name": "Private Group Session",
        "slug": "private-group-session",
        "pricing_model": "per_person",
        "per_person_price": 250,
        "currency": "DKK",
        "payment_mode": "deposit"
      }
    ]
  },
  "error": null
}
Products8 documented operations
GET/api/v1/admin/productsBearer or API key

List venue products

List products for the authenticated venue. Business-app JWTs require pos.access; API keys require read:products. Archived products are hidden unless include_archived=true.

Parameters, scopes and examples

Required scopes

read:products

Query parameters

include_archivedboolean
Include archived products. Defaults to false.Default: false
POST/api/v1/admin/productsAPI key

Create a venue product

Create through the canonical product mutation contract. Unknown/protected fields are rejected; initial stock creates one movement.

Parameters, scopes and examples

Required scopes

write:products
GET/api/v1/admin/products/{id}Bearer or API key

Get a venue product

Get one product only when it belongs to the authenticated venue. Business-app JWTs require pos.access; API keys require read:products.

Parameters, scopes and examples

Required scopes

read:products

Path parameters

idstring · required
Product UUID
PATCH/api/v1/admin/products/{id}Bearer or API key

Update a venue product

Update mutable catalog fields through the canonical product core. Business-app JWTs require products.manage; API keys require write:products. Products and its tier-gated Point of Sale dependency must be active. stock_quantity and protected fields are rejected.

Parameters, scopes and examples

Required scopes

write:products

Path parameters

idstring · required
Product UUID
DELETE/api/v1/admin/products/{id}API key

Archive a venue product

Archive through canonical catalog semantics (is_active=false plus archived_at). Permanent deletion is separate and guarded.

Parameters, scopes and examples

Required scopes

write:products

Path parameters

idstring · required
Product UUID
GET/api/v1/venues/{slug}/product-packagesPublic

List buyable clip cards

Public catalog of active product passes (clip cards) for a venue. Cached, IP-throttled.

Parameters, scopes and examples

Path parameters

slugstring · required
Venue slug

Response example

{
  "data": [
    {
      "id": "uuid",
      "product_id": "uuid",
      "name": "10-pack mat rental",
      "description": "Save 15%",
      "quantity": 10,
      "price": 450,
      "currency": "DKK",
      "savings_label": "Save 15%",
      "validity_days": 180
    }
  ],
  "error": null
}
POST/api/v1/product-packages/{id}/quoteBearer token

Review a selected-currency clip card

Authenticated read-only exact package review. Body {currency}; returns accepted_quote with tenant/member/item, entitlement, price-book version, currency, gross minor units and included VAT. Only enabled home-country catalog-tax books are supported. Draft books refuse; no payment or provider mutation occurs.

Parameters, scopes and examples

Path parameters

idstring · required
Product package id

Response example

{
  "data": {
    "accepted_quote": {
      "version": 1,
      "currency": "EUR",
      "grossMinor": 4500
    }
  },
  "error": null
}
POST/api/v1/product-packages/{id}/purchaseBearer token

Buy a clip card

Initiate a product-pass (clip card) purchase. A nonempty Idempotency-Key header is required and defines the durable operation. Returns Customer/ephemeral-key credentials in the same frozen Stripe account as the PaymentIntent, plus `merchant_country_code` and `stripe_account_id` (`acct_*` only for direct Connect; otherwise null). A service-only pre-provider claim binds tenant, catalog, customer, regional, routing, fee, amount, and currency; `product_passes` is granted atomically on `payment_intent.succeeded`. Gated on the `products` module + venue legal docs. Member-JWT. Optional selected body {currency, acceptedQuote} must contain the exact /quote review and is frozen into the original operation; stale book/gross/tax/entitlement or unsupported fields refuse before payment. Empty legacy bodies retain catalog checkout. Selected claims require released SQL; settings activation stays closed.

Parameters, scopes and examples

Path parameters

idstring · required
Product package id

Response example

{
  "data": {
    "client_secret": "pi_xxx_secret_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4",
    "payment_intent_id": "pi_xxx",
    "package_id": "uuid",
    "amount": 450,
    "currency": "DKK"
  },
  "error": null
}
Community1 documented operation
POST/api/v1/events/{id}/ticketsBearer token

Buy community-event tickets

Buy N paid tickets for a community event (`{id}` is the event booking id). Price = `member_price` + `guest_price` × (ticket_count − 1). Requires an Idempotency-Key header. Returns `operation_state` (payment_required, processing, completed or canceled), durable `operation_status`, and immutable ticket_count/amount_minor/minor_unit_multiplier/member_price_minor/guest_price_minor/amount/currency. Same-key replay checks the durable owned order before cached responses; succeeded payments reconcile through the existing finalizer. Completed/processing/canceled responses have no client_secret and must not open PaymentSheet. Only payment_required returns Customer/ephemeral-key credentials in the same frozen Stripe account as the PaymentIntent, plus `merchant_country_code` and `stripe_account_id` (`acct_*` only for direct Connect; otherwise null). On `payment_intent.succeeded` the ticket order flips to paid and a `going` RSVP is upserted with `guest_count = ticket_count − 1`. Gated on the `community_events` module. Member-JWT.

Parameters, scopes and examples

Path parameters

idstring · required
Event booking id

Ticket order

Request body

{
  "ticket_count": 2,
  "guest_names": [
    "Sam Doe"
  ],
  "notes": "Front row please"
}

Response example

{
  "data": {
    "client_secret": "pi_xxx_secret_xxx",
    "customer_id": "cus_xxx",
    "ephemeral_key": "ek_test_xxx",
    "stripe_account_id": null,
    "merchant_country_code": "DK",
    "regional_revision": "org:venue-id:r4",
    "payment_intent_id": "pi_xxx",
    "ticket_id": "uuid",
    "operation_state": "payment_required",
    "operation_status": "bound",
    "ticket_count": 2,
    "amount_minor": 30000,
    "minor_unit_multiplier": 100,
    "member_price_minor": 20000,
    "guest_price_minor": 10000,
    "amount": 300,
    "currency": "DKK"
  },
  "error": null
}

Prefer the machine-readable contract?

The OpenAPI 3.1 document is generated from this same registry.

Open OpenAPI JSON

Partner engineering

Bring us the integration you want to build

For partner access, implementation questions or a contract review, contact integration@bookingbible.com.