{"openapi":"3.1.0","info":{"title":"RangeWorks Partner API","version":"1.0.0","description":"REST API for RangeWorks integration partners. Provides access to account, customer,\nmembership, reservation, and live range data — including write operations for syncing\ncustomer records from external POS systems.\n\n## Authentication\n\nAll endpoints except `/auth/token` require a Bearer JWT token in the `Authorization` header.\n\n**Step 1:** Exchange your partner credentials for a token:\n\n```bash\ncurl -X POST /api/v1/auth/token \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"client_id\": \"your_client_id\", \"client_secret\": \"your_client_secret\"}'\n```\n\n**Step 2:** Use the returned token on all subsequent requests:\n\n```bash\ncurl /api/v1/account \\\n  -H \"Authorization: Bearer <token>\"\n```\n\nTokens expire after a configurable TTL (default 24 hours). Request a new token when you receive a `401` response.\n\n## Multi-Tenant Access\n\nYour token is scoped to a single **account** which may contain multiple **businesses** (locations).\nUse the `business_slug` query parameter on relevant endpoints to target a specific location.\n\n## Response Format\n\nAll responses follow a consistent envelope:\n\n```json\n{\n  \"data\": { ... },\n  \"meta\": {\n    \"page\": 1,\n    \"per_page\": 25,\n    \"total_count\": 100,\n    \"total_pages\": 4\n  }\n}\n```\n\nSingle-resource endpoints omit `meta`. Error responses use:\n\n```json\n{\n  \"error\": {\n    \"status\": 404,\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Customer not found\"\n  }\n}\n```\n\n## Pagination\n\nCollection endpoints accept `page` (default 1) and `per_page` (default 25, max 100) query parameters.\n\n## Rate Limits\n\nThe API enforces per-token rate limits to protect system stability. Limits vary by HTTP method:\n\n| Method | Limit | Window | Cooldown on violation |\n|--------|-------|--------|-----------------------|\n| **POST** | 10 requests | 10 seconds | 1 minute |\n| **GET** | 60 requests | 30 seconds | 1 minute |\n\nWhen a rate limit is exceeded the API returns `429 Too Many Requests` with a JSON error body\nand a `Retry-After` header indicating how many seconds to wait before retrying.\n\n```json\n{\n  \"error\": {\n    \"status\": 429,\n    \"code\": \"RATE_LIMITED\",\n    \"message\": \"Rate limit exceeded. Retry after 60 seconds.\"\n  }\n}\n```\n\n**Rate limit headers** are included on every response:\n- `X-RateLimit-Limit` — Maximum requests allowed in the current window\n- `X-RateLimit-Remaining` — Requests remaining in the current window\n- `X-RateLimit-Reset` — Unix timestamp (seconds) when the current window resets\n- `Retry-After` — *(429 responses only)* Seconds until the cooldown expires\n\n**Best practices:**\n- Monitor `X-RateLimit-Remaining` and back off before hitting zero.\n- On a `429` response, wait for the duration specified in `Retry-After` before retrying.\n- Avoid tight polling loops — use reasonable intervals (e.g. 5–10 seconds for live range status).\n\n## Field Naming\n\nAll response fields use **snake_case** naming (e.g. `first_name`, `date_start`, `is_active`).\n\n## Exports\n\n- **OpenAPI JSON:** `GET /api/v1/docs/openapi`\n- **Postman Collection:** `GET /api/v1/docs/postman`\n","contact":{"name":"RangeWorks Engineering","url":"https://rangeworks.us"},"license":{"name":"Proprietary"}},"servers":[{"url":"http://localhost:4006/api/v1","description":"Local development"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"Auth","description":"Token exchange for partner authentication"},{"name":"Account","description":"Account and business information"},{"name":"Customers","description":"Retail customer records and related sub-resources"},{"name":"Memberships","description":"Membership plans available for purchase"},{"name":"Members","description":"Active and historical membership enrollments tied to a customer"},{"name":"Classes","description":"Class catalog and scheduled class sessions"},{"name":"Reservations","description":"Range lane reservations"},{"name":"Range","description":"Customer-facing range bays, slot availability, and live ops range status"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT token obtained from POST /auth/token"}},"schemas":{"error_response":{"type":"object","properties":{"error":{"type":"object","properties":{"status":{"type":"integer","description":"HTTP status code","example":404},"code":{"type":"string","description":"Machine-readable error code","example":"NOT_FOUND"},"message":{"type":"string","description":"Human-readable error message","example":"Customer not found"}}}}},"pagination_meta":{"type":"object","description":"Pagination metadata included on collection responses","properties":{"page":{"type":"integer","description":"Current page number","example":1},"per_page":{"type":"integer","description":"Items per page","example":25},"total_count":{"type":"integer","description":"Total number of items across all pages","example":142},"total_pages":{"type":"integer","description":"Total number of pages","example":6}}},"auth_request":{"type":"object","required":["client_id","client_secret"],"properties":{"client_id":{"type":"string","description":"Your partner client ID","example":"partner_abc123"},"client_secret":{"type":"string","description":"Your partner client secret","example":"sk_live_xxxxxxxx"}}},"auth_response":{"type":"object","properties":{"data":{"type":"object","properties":{"token":{"type":"string","description":"JWT Bearer token for API access","example":"eyJhbGciOiJIUzI1NiIs..."},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the token expires","example":"2025-06-16T12:00:00.000Z"},"account":{"$ref":"#/components/schemas/account"}}}}},"account":{"type":"object","description":"Partner account with associated businesses (locations)","properties":{"id":{"type":"string","description":"Unique account ID","example":"1"},"account_name":{"type":"string","description":"Display name of the account","example":"Patriot Arms & Range"},"slug":{"type":"string","description":"URL-safe account identifier","example":"patriot-arms"},"businesses":{"type":"array","description":"Business locations under this account","items":{"$ref":"#/components/schemas/business"}}}},"business":{"type":"object","description":"A single business location","properties":{"id":{"type":"string","example":"1"},"name":{"type":"string","example":"Patriot Arms - Nashville"},"slug":{"type":"string","description":"URL-safe business identifier used in `business_slug` filters","example":"patriot-arms-nashville"},"uuid":{"type":"string","description":"Universally unique business identifier","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}},"customer":{"type":"object","description":"A retail customer record","properties":{"id":{"type":"string","example":"42"},"first_name":{"type":"string","nullable":true,"example":"John"},"last_name":{"type":"string","nullable":true,"example":"Doe"},"email":{"type":"string","nullable":true,"example":"john.doe@example.com"},"phone1":{"type":"string","nullable":true,"example":"615-555-1234"},"address1":{"type":"string","nullable":true,"example":"123 Main St"},"address2":{"type":"string","nullable":true,"example":"Suite 200"},"city":{"type":"string","nullable":true,"example":"Nashville"},"state":{"type":"string","nullable":true,"example":"TN"},"zip_code":{"type":"string","nullable":true,"example":"37209"},"date_of_birth":{"type":"string","format":"date","nullable":true,"example":"1990-05-15"},"drivers_license_number":{"type":"string","nullable":true,"example":"DL123456789"},"external_customer_number":{"type":"string","nullable":true,"description":"Customer number from external POS system (e.g. Counterpoint)","example":"CP-10042"},"rangeworks_client_id":{"type":"string","nullable":true,"description":"Internal RangeWorks client identifier","example":"rw_cust_abc123"}}},"customer_with_memberships":{"allOf":[{"$ref":"#/components/schemas/customer"},{"type":"object","properties":{"memberships":{"type":"array","description":"Active and historical membership enrollments for this customer","items":{"$ref":"#/components/schemas/member"}}}}]},"upsert_customer_request":{"type":"object","required":["external_id"],"description":"Customer data keyed by external ID. If a customer with this ID exists, it will be updated; otherwise a new customer is created.","properties":{"external_id":{"type":"string","description":"External system identifier (match key for upsert)","example":"CP-10042"},"first_name":{"type":"string","description":"Customer first name","example":"John"},"last_name":{"type":"string","description":"Customer last name","example":"Doe"},"address1":{"type":"string","example":"123 Main St"},"address2":{"type":"string","example":"Suite 200"},"city":{"type":"string","example":"Nashville"},"state":{"type":"string","example":"TN"},"zip_code":{"type":"string","example":"37209"},"phone":{"type":"string","example":"615-555-1234"},"drivers_license_number":{"type":"string","example":"DL123456789"}}},"upsert_customer_response":{"type":"object","properties":{"data":{"type":"object","properties":{"retail_customer_id":{"type":"string","description":"RangeWorks internal customer ID","example":"42"},"external_id":{"type":"string","description":"The external ID used as match key","example":"CP-10042"},"created":{"type":"boolean","description":"`true` if a new customer was created, `false` if an existing customer was updated","example":true}}}}},"membership":{"type":"object","description":"A membership plan offered by a business — the catalog entry customers can enroll in.","properties":{"id":{"type":"string","example":"15"},"uuid":{"type":"string","example":"b3c4d5e6-7890-abcd-ef12-3456789abcde"},"business_id":{"type":"string","description":"ID of the business that owns this plan","example":"10"},"slug":{"type":"string","nullable":true,"description":"URL-safe plan identifier","example":"gold-annual"},"name":{"type":"string","nullable":true,"example":"Gold Annual"},"cost":{"type":"number","nullable":true,"description":"Up-front purchase cost in dollars","example":199},"recurring_amount":{"type":"number","nullable":true,"description":"Recurring billing amount in dollars","example":49.99},"billing_period":{"type":"string","nullable":true,"description":"Recurring billing period: e.g. month, year","example":"month"},"billing_cycle":{"type":"integer","nullable":true,"description":"Number of billing periods between charges","example":1},"length_days":{"type":"integer","nullable":true,"description":"Duration of one membership term in days","example":365},"class_discount_percentage":{"type":"number","nullable":true,"example":10},"range_discount_percentage":{"type":"number","nullable":true,"example":15},"family_addons_enabled":{"type":"boolean","nullable":true,"example":true},"additional_family_members":{"type":"integer","nullable":true,"example":3},"family_membership_cost":{"type":"number","nullable":true,"example":49},"family_recurring_monthly_amount":{"type":"number","nullable":true,"example":19.99},"preview_image_url":{"type":"string","nullable":true,"example":"https://cdn.rangeworks.com/memberships/gold-preview.jpg"},"full_image_url":{"type":"string","nullable":true,"example":"https://cdn.rangeworks.com/memberships/gold-full.jpg"},"presale_mode":{"type":"boolean","nullable":true,"example":false},"position":{"type":"integer","nullable":true,"description":"Display order within the catalog","example":1},"short_description":{"type":"string","nullable":true,"example":"Unlimited range access and class discounts"},"description":{"type":"string","nullable":true},"details":{"type":"string","nullable":true},"legal":{"type":"string","nullable":true},"post_order_instructions":{"type":"string","nullable":true},"third_party_waiver_url":{"type":"string","nullable":true,"example":"https://waivers.example.com/sign/abc"}}},"membership_summary":{"type":"object","description":"Lean reference to the membership plan a `member` is enrolled in.","properties":{"id":{"type":"string","example":"15"},"slug":{"type":"string","nullable":true,"example":"gold-annual"},"name":{"type":"string","nullable":true,"example":"Gold Annual"}}},"member":{"type":"object","description":"A customer's enrollment in a specific membership plan.","properties":{"id":{"type":"string","example":"812"},"retail_customer_id":{"type":"string","nullable":true,"example":"42"},"membership_id":{"type":"string","nullable":true,"description":"ID of the membership plan this enrollment is tied to","example":"15"},"status":{"type":"string","description":"Lifecycle state: active, expired, suspended, cancelled, pending","example":"active"},"fulfillment_status":{"type":"string","description":"Fulfillment progress: e.g. fulfilled, pending_documents","example":"fulfilled"},"amount_due":{"type":"number","nullable":true,"example":0},"recurring_amount":{"type":"number","nullable":true,"example":49.99},"billing_period":{"type":"string","nullable":true,"example":"month"},"start_date":{"type":"string","format":"date-time","nullable":true,"example":"2025-01-01T00:00:00Z"},"end_date":{"type":"string","format":"date-time","nullable":true,"example":"2026-01-01T00:00:00Z"},"membership":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/membership_summary"}]}}},"instructor":{"type":"object","description":"An instructor that can be associated with a class.","properties":{"id":{"type":"string","example":"7"},"slug":{"type":"string","nullable":true,"example":"jane-smith"},"name":{"type":"string","nullable":true,"example":"Jane Smith"},"bio":{"type":"string","nullable":true,"description":"Long-form instructor biography (omitted from session-level views)"},"profile_image_url":{"type":"string","nullable":true,"example":"https://cdn.rangeworks.com/instructors/jane.jpg"},"specialties":{"type":"array","items":{"type":"string"},"example":["Pistol","Defensive Carry"]}}},"price_range":{"type":"object","description":"Min and max prices observed across a class's upcoming sessions.","properties":{"min":{"type":"number","example":99},"max":{"type":"number","example":199}}},"range_class":{"type":"object","description":"A class catalog entry. Single-class responses include `range_class_definitions` (upcoming sessions).","properties":{"id":{"type":"string","example":"33"},"uuid":{"type":"string","example":"c1d2e3f4-5678-90ab-cdef-1234567890ab"},"slug":{"type":"string","nullable":true,"example":"defensive-pistol-basics"},"name":{"type":"string","nullable":true,"example":"Defensive Pistol Basics"},"position":{"type":"integer","nullable":true,"example":1},"members_only_ind":{"type":"boolean","nullable":true,"description":"True when only active members can purchase a seat","example":false},"duration_minutes":{"type":"integer","nullable":true,"example":240},"enable_waitlist":{"type":"boolean","nullable":true,"example":true},"hide_instructor":{"type":"boolean","nullable":true,"example":false},"firearm_type":{"type":"string","nullable":true,"example":"pistol"},"skill_level":{"type":"string","nullable":true,"example":"beginner"},"certification_type":{"type":"string","nullable":true,"example":"none"},"competition_type":{"type":"string","nullable":true,"example":"none"},"preview_image_url":{"type":"string","nullable":true},"full_image_url":{"type":"string","nullable":true},"upcoming_session_count":{"type":"integer","example":3},"next_class_datetime":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-01T17:00:00Z"},"next_class_price":{"type":"number","nullable":true,"example":149},"next_class_spots_remaining":{"type":"integer","nullable":true,"example":4},"next_class_capacity":{"type":"integer","nullable":true,"example":12},"price_range":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/price_range"}]},"instructors":{"type":"array","items":{"$ref":"#/components/schemas/instructor"}},"short_description":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"details":{"type":"string","nullable":true},"legal":{"type":"string","nullable":true},"post_order_instructions":{"type":"string","nullable":true},"third_party_waiver_url":{"type":"string","nullable":true}}},"range_class_definition":{"type":"object","description":"A scheduled session of a class — a specific date, location, and seat pool.","properties":{"id":{"type":"string","example":"901"},"uuid":{"type":"string","example":"d1e2f3a4-5678-90ab-cdef-1234567890ab"},"range_class_id":{"type":"string","nullable":true,"example":"33"},"class_date_time":{"type":"string","format":"date-time","nullable":true,"example":"2026-06-01T17:00:00Z"},"duration_minutes":{"type":"integer","nullable":true,"example":240},"cost":{"type":"number","nullable":true,"example":149},"title":{"type":"string","nullable":true,"example":"Defensive Pistol Basics — June Evening"},"class_description":{"type":"string","nullable":true},"max_participants":{"type":"integer","nullable":true,"example":12},"slots_available":{"type":"integer","nullable":true,"example":4},"last_reservation":{"type":"string","format":"date-time","nullable":true,"description":"Cutoff after which seats can no longer be reserved","example":"2026-05-30T17:00:00Z"},"location_name":{"type":"string","nullable":true,"example":"Patriot Arms - Nashville"},"location_slug":{"type":"string","nullable":true,"example":"patriot-arms-nashville"},"location_address":{"type":"string","nullable":true},"location_city":{"type":"string","nullable":true},"location_state":{"type":"string","nullable":true},"location_zip":{"type":"string","nullable":true},"hide_instructor":{"type":"boolean","nullable":true},"contact_to_purchase":{"type":"boolean","nullable":true,"description":"When true, partners should route purchase intent to the business directly","example":false},"has_addons":{"type":"boolean","example":true},"instructor":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/instructor"}]}}},"range_class_with_sessions":{"allOf":[{"$ref":"#/components/schemas/range_class"},{"type":"object","properties":{"range_class_definitions":{"type":"array","description":"Upcoming scheduled sessions for this class","items":{"$ref":"#/components/schemas/range_class_definition"}}}}]},"range_bay":{"type":"object","description":"A customer-facing reservable range bay (catalog entry — distinct from live-ops `venue_space`/`venue_resource`).","properties":{"id":{"type":"string","example":"5"},"uuid":{"type":"string","example":"e1f2a3b4-5678-90ab-cdef-1234567890ab"},"slug":{"type":"string","nullable":true,"example":"pistol-bay-a"},"name":{"type":"string","nullable":true,"example":"Pistol Bay A"},"position":{"type":"integer","example":1},"description":{"type":"string","nullable":true},"session_duration":{"type":"integer","description":"Length of one reservable session in minutes","example":60},"requires_membership":{"type":"boolean","example":false},"required_membership_ids":{"type":"array","description":"When `requires_membership` is true, the set of membership plan IDs that grant access","items":{"type":"string"},"example":["15"]},"allow_resource_selection":{"type":"boolean","nullable":true,"description":"When true, a customer can pick a specific lane at checkout","example":true},"has_addons":{"type":"boolean","example":false},"image_url":{"type":"string","nullable":true},"full_image_url":{"type":"string","nullable":true},"business_id":{"type":"string","example":"10"}}},"range_bay_available_session":{"type":"object","description":"A single bookable slot within a range bay's availability search result.","properties":{"start_time":{"type":"string","nullable":true,"description":"Display time (e.g. \"5:00 PM\")","example":"5:00 PM"},"start_date":{"type":"string","nullable":true,"description":"Display date (YYYY-MM-DD)","example":"2026-06-01"},"open_lane_count":{"type":"integer","example":4},"open_lanes":{"type":"array","items":{"type":"integer"},"example":[1,2,3,5]},"bay_name":{"type":"string","nullable":true,"example":"Pistol Bay A"}}},"range_bay_availability":{"type":"object","description":"Per-bay slot search result returned by `/range/availability`.","properties":{"range_bay_id":{"type":"string","example":"5"},"has_required_membership":{"type":"boolean","nullable":true,"description":"Whether the supplied `membership_id` (or the caller's context) meets the bay's membership gate","example":true},"reservation_price":{"type":"number","nullable":true,"example":25},"display_reservation_price":{"type":"string","nullable":true,"example":"$25.00"},"searched":{"type":"boolean","nullable":true,"description":"True when the search succeeded and `sessions` is populated","example":true},"errors":{"type":"array","items":{"type":"string"},"description":"Search-time validation messages, e.g. membership gate failures","example":[]},"allow_resource_selection":{"type":"boolean","nullable":true,"example":true},"location_slug":{"type":"string","example":"patriot-arms-nashville"},"search_date":{"type":"string","description":"Echo of the requested date (YYYY-MM-DD)","example":"2026-06-01"},"search_time":{"type":"string","description":"Echo of the requested time (h:MM AM/PM)","example":"5:00 PM"},"sessions":{"type":"array","items":{"$ref":"#/components/schemas/range_bay_available_session"}}}},"reservation":{"type":"object","description":"A range lane reservation","properties":{"id":{"type":"string","example":"201"},"retail_customer_id":{"type":"string","nullable":true,"example":"42"},"date_start":{"type":"string","format":"date-time","nullable":true,"description":"Reservation start time","example":"2025-06-15T14:00:00Z"},"date_end":{"type":"string","format":"date-time","nullable":true,"description":"Reservation end time","example":"2025-06-15T15:00:00Z"},"range_type":{"type":"string","nullable":true,"description":"Name of the range bay","example":"Pistol Bay A"},"checked_in":{"type":"boolean","description":"Whether the customer has been checked in for this reservation","example":false}}},"range_party":{"type":"object","description":"An active or historical range session (party occupying a lane)","properties":{"id":{"type":"string","example":"88"},"range_lane_reservation_id":{"type":"string","nullable":true,"description":"Associated reservation ID, if booked in advance","example":"201"},"session_start_time":{"type":"string","format":"date-time","nullable":true,"example":"2025-06-15T14:05:00Z"},"status":{"type":"string","description":"Session status: active, completed, cancelled","example":"active"},"is_active":{"type":"boolean","description":"Whether this party is currently on the range","example":true},"elapsed_time":{"type":"integer","nullable":true,"description":"Elapsed session time in seconds","example":1800}}},"venue_space":{"type":"object","description":"A logical grouping of range lanes (e.g. a bay)","properties":{"id":{"type":"string","example":"3"},"name":{"type":"string","nullable":true,"example":"Pistol Bay A"},"space_type":{"type":"string","nullable":true,"description":"Internal type classification","example":"Venue::RangeBay"},"venue_resource_ids":{"type":"array","description":"IDs of lanes/resources in this space","items":{"type":"string"},"example":["10","11","12","13"]}}},"venue_resource":{"type":"object","description":"A single range lane or resource","properties":{"id":{"type":"string","example":"10"},"space_id":{"type":"string","nullable":true,"description":"Parent venue space ID","example":"3"},"name":{"type":"string","nullable":true,"example":"Lane 1"},"resource_type":{"type":"string","nullable":true,"example":"Venue::RangeLane"},"is_open":{"type":"boolean","description":"Whether this lane is currently open for use","example":true},"is_reservable":{"type":"boolean","description":"Whether this lane accepts advance reservations","example":true},"current_range_party":{"nullable":true,"description":"The party currently occupying this lane, or null if vacant","allOf":[{"$ref":"#/components/schemas/range_party"}]}}},"range_status":{"type":"object","description":"A venue space with its resources and their current occupancy","properties":{"id":{"type":"string","example":"3"},"name":{"type":"string","nullable":true,"example":"Pistol Bay A"},"space_type":{"type":"string","nullable":true,"example":"Venue::RangeBay"},"resources":{"type":"array","description":"Lanes in this space with live status","items":{"$ref":"#/components/schemas/venue_resource"}}}}},"responses":{"rate_limited":{"description":"Rate limit exceeded — too many requests in the current window","headers":{"Retry-After":{"description":"Seconds until the cooldown expires and requests are accepted again","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Maximum requests allowed per window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Always 0 when rate limited","schema":{"type":"integer","example":0}},"X-RateLimit-Reset":{"description":"Unix timestamp when the current cooldown expires","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"},"example":{"error":{"status":429,"code":"RATE_LIMITED","message":"Rate limit exceeded. Retry after 60 seconds."}}}}}},"parameters":{"page":{"name":"page","in":"query","description":"Page number (1-indexed)","schema":{"type":"integer","default":1,"minimum":1}},"per_page":{"name":"per_page","in":"query","description":"Number of items per page","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},"business_slug":{"name":"business_slug","in":"query","description":"Filter results to a specific business location","schema":{"type":"string"},"example":"patriot-arms-nashville"}}},"paths":{"/auth/token":{"post":{"summary":"Exchange credentials for JWT","operationId":"createAuthToken","description":"Exchange your partner `client_id` and `client_secret` for a JWT Bearer token.\nThe token is valid for the configured TTL (default 24 hours). Include it in the\n`Authorization: Bearer <token>` header on all subsequent requests.\n","tags":["Auth"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_request"},"example":{"client_id":"partner_abc123","client_secret":"sk_live_xxxxxxxx"}}}},"responses":{"200":{"description":"JWT token issued successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/auth_response"},"example":{"data":{"token":"eyJhbGciOiJIUzI1NiIs...","expires_at":"2025-06-16T12:00:00.000Z","account":{"id":"1","slug":"patriot-arms","name":"Patriot Arms & Range","businesses":[{"id":"1","name":"Patriot Arms - Nashville","slug":"patriot-arms-nashville","uuid":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}]}}}}}},"400":{"description":"Invalid request body — missing or empty `client_id` / `client_secret`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"},"example":{"error":{"status":400,"code":"INVALID_REQUEST","message":"client_id and client_secret are required"}}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"},"example":{"error":{"status":401,"code":"INVALID_CREDENTIALS","message":"Invalid client_id or client_secret"}}}}},"429":{"$ref":"#/components/responses/rate_limited"},"502":{"description":"Upstream authentication service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}}}}},"/account":{"get":{"summary":"Get current account","operationId":"getAccount","description":"Returns the account associated with your JWT token, including all business locations.","tags":["Account"],"responses":{"200":{"description":"Account details with businesses","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/account"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/customers":{"get":{"summary":"List customers","operationId":"listCustomers","description":"Returns a paginated list of retail customers. Optionally filter by `business_slug`.","tags":["Customers"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"$ref":"#/components/parameters/business_slug"}],"responses":{"200":{"description":"Paginated customer list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/customer"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}},"post":{"summary":"Create or update a customer","operationId":"upsertCustomer","description":"Upserts a retail customer using `external_id` as the match key.\nIf a customer with the given ID already exists, their record is updated;\notherwise a new customer is created.\n\n**Requires `business_slug`** — customers are scoped to a specific business location.\n","tags":["Customers"],"parameters":[{"name":"business_slug","in":"query","required":true,"description":"Business location slug (required for tenant scoping)","schema":{"type":"string"},"example":"patriot-arms-nashville"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/upsert_customer_request"},"example":{"external_id":"CP-10042","first_name":"John","last_name":"Doe","address1":"123 Main St","city":"Nashville","state":"TN","zip_code":"37209","phone":"615-555-1234"}}}},"responses":{"200":{"description":"Existing customer updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/upsert_customer_response"},"example":{"data":{"retail_customer_id":"42","external_id":"CP-10042","created":false}}}}},"201":{"description":"Customer created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/upsert_customer_response"},"example":{"data":{"retail_customer_id":"42","external_id":"CP-10042","created":true}}}}},"400":{"description":"Validation error — missing or invalid fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"},"example":{"error":{"status":400,"code":"INVALID_REQUEST","message":"external_id is required"}}}}},"422":{"description":"Upstream rejection — business logic or POS sync error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"},"example":{"error":{"status":422,"code":"UPSTREAM_REJECTION","message":"Customer number already associated with a different account"}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/customers/{id}":{"get":{"summary":"Get customer by ID","operationId":"getCustomer","description":"Returns a single customer with their membership enrollments.","tags":["Customers"],"parameters":[{"name":"id","in":"path","required":true,"description":"Customer ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Customer details with memberships","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/customer_with_memberships"}}}}}},"404":{"description":"Customer not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/customers/{id}/memberships":{"get":{"summary":"List memberships for a customer","operationId":"listCustomerMemberships","description":"Returns all membership enrollments (member records) for a specific customer.","tags":["Customers"],"parameters":[{"name":"id","in":"path","required":true,"description":"Customer ID","schema":{"type":"string"}},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"}],"responses":{"200":{"description":"Customer membership enrollments","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/member"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/customers/{id}/reservations":{"get":{"summary":"List reservations for a customer","operationId":"listCustomerReservations","description":"Returns range lane reservations for a specific customer. Supports date range filtering.","tags":["Customers"],"parameters":[{"name":"id","in":"path","required":true,"description":"Customer ID","schema":{"type":"string"}},{"name":"date_start","in":"query","description":"Filter reservations starting on or after this date (ISO 8601)","schema":{"type":"string","format":"date-time"},"example":"2025-01-01T00:00:00Z"},{"name":"date_end","in":"query","description":"Filter reservations ending on or before this date (ISO 8601)","schema":{"type":"string","format":"date-time"},"example":"2025-12-31T23:59:59Z"},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"}],"responses":{"200":{"description":"Customer reservations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/reservation"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/memberships":{"get":{"summary":"List membership plans","operationId":"listMemberships","description":"Returns the membership plan catalog — the plans customers can purchase.\nFilter by `business_slug` to scope to a location, by `active_for_sale` to\nhide retired plans, by `presale_mode` to surface presale-only plans, or\nby `family_addons_enabled` to filter to family-capable plans.\n","tags":["Memberships"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"$ref":"#/components/parameters/business_slug"},{"name":"active_for_sale","in":"query","description":"When true, hide plans that are no longer offered","schema":{"type":"boolean"}},{"name":"presale_mode","in":"query","description":"When true, return only presale-mode plans","schema":{"type":"boolean"}},{"name":"family_addons_enabled","in":"query","description":"When true, return only plans that allow family add-ons","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated membership plan list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/membership"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/memberships/{id}":{"get":{"summary":"Get membership plan by ID or slug","operationId":"getMembership","description":"Returns a single membership plan. The path parameter accepts either the\nnumeric plan ID or the plan slug.\n","tags":["Memberships"],"parameters":[{"name":"id","in":"path","required":true,"description":"Membership plan ID or slug","schema":{"type":"string"}}],"responses":{"200":{"description":"Membership plan details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/membership"}}}}}},"404":{"description":"Membership plan not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/members":{"get":{"summary":"List membership enrollments","operationId":"listMembers","description":"Returns a paginated list of membership enrollments (members) across an\naccount. Filter by `customer_id` or `membership_id` to narrow to a\nsingle customer or plan, by `status` / `fulfillment_status` to gate on\nlifecycle state, or by `date_start` / `date_end` to filter by enrollment\nwindow.\n","tags":["Members"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"$ref":"#/components/parameters/business_slug"},{"name":"customer_id","in":"query","description":"Filter members by retail customer ID","schema":{"type":"string"}},{"name":"membership_id","in":"query","description":"Filter members by membership plan ID","schema":{"type":"string"}},{"name":"status","in":"query","description":"Lifecycle filter: active, expired, suspended, cancelled, pending","schema":{"type":"string"}},{"name":"fulfillment_status","in":"query","description":"Fulfillment filter: e.g. fulfilled, pending_documents","schema":{"type":"string"}},{"name":"date_start","in":"query","description":"Filter to members whose start date is on or after this ISO 8601 timestamp","schema":{"type":"string","format":"date-time"}},{"name":"date_end","in":"query","description":"Filter to members whose end date is on or before this ISO 8601 timestamp","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Paginated member list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/member"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/members/{id}":{"get":{"summary":"Get member by ID","operationId":"getMember","description":"Returns a single membership enrollment by ID.","tags":["Members"],"parameters":[{"name":"id","in":"path","required":true,"description":"Member ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Member details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/member"}}}}}},"404":{"description":"Member not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/classes":{"get":{"summary":"List classes","operationId":"listClasses","description":"Returns the customer-facing class catalog. Each entry includes summary\nnext-session information (date, price, remaining seats). Use\n`/classes/{id}/sessions` to fetch the full schedule.\n","tags":["Classes"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"$ref":"#/components/parameters/business_slug"},{"name":"active_for_sale","in":"query","description":"When true, hide classes with no purchasable sessions","schema":{"type":"boolean"}},{"name":"firearm_type","in":"query","description":"Firearm-type enum value","schema":{"type":"integer"}},{"name":"skill_level","in":"query","description":"Skill-level enum value","schema":{"type":"integer"}},{"name":"certification_type","in":"query","description":"Certification-type enum value","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated class catalog","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/range_class"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/classes/{id}":{"get":{"summary":"Get class by ID or slug","operationId":"getClass","description":"Returns a single class with its upcoming sessions inlined under\n`range_class_definitions`. The path parameter accepts either the numeric\nclass ID or the class slug.\n","tags":["Classes"],"parameters":[{"name":"id","in":"path","required":true,"description":"Class ID or slug","schema":{"type":"string"}}],"responses":{"200":{"description":"Class details with upcoming sessions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/range_class_with_sessions"}}}}}},"404":{"description":"Class not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/classes/{id}/sessions":{"get":{"summary":"List sessions for a class","operationId":"listClassSessions","description":"Returns the scheduled sessions (class definitions) for a single class.\nFilter by a date range or by `has_open_seats` to hide full sessions.\nThe path parameter accepts either the numeric class ID or the class\nslug.\n","tags":["Classes"],"parameters":[{"name":"id","in":"path","required":true,"description":"Class ID or slug","schema":{"type":"string"}},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"name":"date_start","in":"query","description":"Filter sessions starting on or after this ISO 8601 timestamp","schema":{"type":"string","format":"date-time"}},{"name":"date_end","in":"query","description":"Filter sessions starting on or before this ISO 8601 timestamp","schema":{"type":"string","format":"date-time"}},{"name":"has_open_seats","in":"query","description":"When true, hide sessions with no remaining seats","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated class sessions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/range_class_definition"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"404":{"description":"Class not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/reservations":{"get":{"summary":"List reservations","operationId":"listReservations","description":"Returns a paginated list of range lane reservations. Supports filtering by customer, date range, and business location.","tags":["Reservations"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"name":"customer_id","in":"query","description":"Filter reservations by customer ID","schema":{"type":"string"}},{"name":"date_start","in":"query","description":"Filter reservations starting on or after this date (ISO 8601)","schema":{"type":"string","format":"date-time"},"example":"2025-01-01T00:00:00Z"},{"name":"date_end","in":"query","description":"Filter reservations ending on or before this date (ISO 8601)","schema":{"type":"string","format":"date-time"},"example":"2025-12-31T23:59:59Z"},{"$ref":"#/components/parameters/business_slug"}],"responses":{"200":{"description":"Paginated reservation list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/reservation"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/reservations/{id}":{"get":{"summary":"Get reservation by ID","operationId":"getReservation","description":"Returns a single range lane reservation by ID.","tags":["Reservations"],"parameters":[{"name":"id","in":"path","required":true,"description":"Reservation ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Reservation details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/reservation"}}}}}},"404":{"description":"Reservation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/bays":{"get":{"summary":"List range bays","operationId":"listRangeBays","description":"Returns the customer-facing reservable range bays. These are distinct\nfrom the live-ops `/range/spaces` / `/range/resources` views, which\nexpose internal venue topology and live occupancy.\n","tags":["Range"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"$ref":"#/components/parameters/business_slug"},{"name":"active_for_reservations","in":"query","description":"When true, hide bays that are not currently accepting reservations","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated range bay list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/range_bay"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/bays/{id}":{"get":{"summary":"Get range bay by ID or slug","operationId":"getRangeBay","description":"Returns a single range bay. The path parameter accepts either the\nnumeric bay ID or the bay slug.\n","tags":["Range"],"parameters":[{"name":"id","in":"path","required":true,"description":"Range bay ID or slug","schema":{"type":"string"}}],"responses":{"200":{"description":"Range bay details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/range_bay"}}}}}},"404":{"description":"Range bay not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/availability":{"get":{"summary":"Search range bay availability for a date and time","operationId":"getRangeBayAvailability","description":"Returns the per-bay slot search result for the supplied `business_slug`,\n`search_date` (YYYY-MM-DD), and `search_time` (h:MM AM/PM). Pass an\noptional `membership_id` to evaluate membership-gated bays as if the\ncaller were enrolled in that plan.\n\nThis endpoint is **not paginated** — the full array of bays is returned\nunder `data`. Bays without a successful search return `searched: false`\nand surface any validation issues under `errors`.\n","tags":["Range"],"parameters":[{"name":"business_slug","in":"query","required":true,"description":"Business location slug (required)","schema":{"type":"string"},"example":"patriot-arms-nashville"},{"name":"search_date","in":"query","required":true,"description":"Date to search (YYYY-MM-DD)","schema":{"type":"string","format":"date"},"example":"2026-06-01"},{"name":"search_time","in":"query","required":true,"description":"Time to search (h:MM AM/PM)","schema":{"type":"string"},"example":"5:00 PM"},{"name":"membership_id","in":"query","description":"Evaluate availability as if the caller held this membership plan","schema":{"type":"string"}}],"responses":{"200":{"description":"Per-bay availability for the requested slot","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/range_bay_availability"}}}}}}},"400":{"description":"Missing required `business_slug`, `search_date`, or `search_time`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/status":{"get":{"summary":"Get live range status","operationId":"getRangeStatus","description":"Returns the real-time status of all range bays and their lanes, including which lanes\nare open, reservable, and currently occupied. This is the primary endpoint for digital\nsignage and live lane status displays.\n\n**Requires `business_slug`** — range status is always location-specific.\n","tags":["Range"],"parameters":[{"name":"business_slug","in":"query","required":true,"description":"Business location slug (required)","schema":{"type":"string"},"example":"patriot-arms-nashville"}],"responses":{"200":{"description":"Range status with nested resources and active parties","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/range_status"}}}}}}},"400":{"description":"Missing required `business_slug` parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error_response"}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/spaces":{"get":{"summary":"List venue spaces","operationId":"listVenueSpaces","description":"Returns all venue spaces (range bays) with their associated resource IDs.","tags":["Range"],"parameters":[{"$ref":"#/components/parameters/business_slug"}],"responses":{"200":{"description":"Venue spaces","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/venue_space"}}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/resources":{"get":{"summary":"List venue resources","operationId":"listVenueResources","description":"Returns individual range lanes/resources, optionally filtered by `space_id`. Includes current occupancy.","tags":["Range"],"parameters":[{"$ref":"#/components/parameters/business_slug"},{"name":"space_id","in":"query","description":"Filter resources by venue space ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Venue resources with current party info","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/venue_resource"}}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}},"/range/parties":{"get":{"summary":"List range parties","operationId":"listRangeParties","description":"Returns a paginated list of range parties (sessions). Filter by `active` status or `space_id`.","tags":["Range"],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/per_page"},{"$ref":"#/components/parameters/business_slug"},{"name":"active","in":"query","description":"Filter to only active (`true`) or completed (`false`) sessions","schema":{"type":"boolean"},"example":true},{"name":"space_id","in":"query","description":"Filter parties by venue space ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated range parties","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/range_party"}},"meta":{"$ref":"#/components/schemas/pagination_meta"}}}}}},"429":{"$ref":"#/components/responses/rate_limited"}}}}}}