openapi: 3.2.0 info: title: Equipment Vendor API (for standalone clients) Cardio Test API description: "At EGYM, we believe that fitness is the soul of healthy living. Work together with partners to help us bringing new ways of working out to\nmillions of gym members, and new ways of prescribing exercises to hundred of thousands of gym trainers, all around the world.\n\nEGYM provides a Fitness Platform for the Connected Gym so you can help us with the mission to ***make the gym work for everyone***.\nEGYM provides a global account that is not scoped for a specific tenant, brand or gym. The account is called EGYM ID. The EGYM ID allows\nusers to use different services of the gym. The EGYM ID is used for the login to EGYM hardware and software products and conneceted equipment.\n\nEGYM works with the best of breed of the fitness industry. In this documentation, we provide tools and info needed to build a variety of workout\nexperiences that integrate directly with the EGYM Ecosystem. We are happy to welcome you as a partner of EGYM.\n\n---\n\n# \U0001F4E6 Changelog of Documentation\n\n| Version | Changes |\n|---------|---------|\n| **1.2.0** | ✅ Added support for **Genius Training Plans** — see [`GET /api/v1/cardio/training-plans`] and [`GET /api/v1/strength/training-plans`]|\n| **1.1.0** | \U0001F510 Introduced **NFC-based login** via Apple/Google Wallet — see [`POST /api/v1/oauth/token`](#tag/OAuth/operation/token) |\n\n---\n\n# \U0001F510 Authentication\n\nThe Equipment Vendor API authentication is based on the OAuth 2.0 as specified in the [RFC6749](https://tools.ietf.org/html/rfc6749),\nmore concretely on the [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#page-37) due to constraint\nenvironment that can be found in our partner devices, i.e. devices with no screen or using the RFID for user identification.\n\nCurrently, different methods for obtaining the Access Token are made available:\n\n- By specifying **user credentials**\n- By specifying **RFID**\n- **By specifying NFC (Wallet) credentials** *(since v1.1.0)*\n\nThe OAuth `client_id` and `client_secret` are issued by EGYM to each partner (vendor) for each gym. These are used in a `Basic` authentication header as:\n\n```\nAuthorization: Basic Base64(client_id:client_secret)\n```\n\n---\n\n## \U0001F511 User Credentials (EGYM ID)\n\n_This is the classic Resource Owner Password Grant._\n\n\n```http\nPOST /api/v1/oauth/token\nAuthorization: Basic \nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=password&machine_type=bike&serial_number=ser123&username=johndoe&password=•••\n```\n\n---\n\n## \U0001F4F6 User RFID\n\n\n```http\nPOST /api/v1/oauth/token\nAuthorization: Basic \nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=rfid&machine_type=bike&serial_number=ser123&rfid=AB12CD34&rfid_format=MIFARE\n```\n\n---\n\n## \U0001F4F1 NFC Login (Apple / Google Wallet)\n\n_Introduced in v1.1.0_\n\nThis method uses NFC payloads from Apple/Google Wallet. Use `grant_type=nfc` and supply:\n\n- `payload`: the NFC data string\n- `transponder_type`: either `APPLE` or `GOOGLE`\n- One of:\n - `issuer_id`: the Wallet issuer identifier (e.g., `com.egym.apple`)\n - `configuration_index`: the DotOrigin VTAP slot ID\n\nExamples:\n\n```APPLE\nPOST /api/v1/oauth/token\nAuthorization: Basic \nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=nfc&machine_type=bike&serial_number=ser123&payload=abc123&transponder_type=APPLE&issuer_id=com.egym.apple\n```\n\n```GOOGLE\nPOST /api/v1/oauth/token\nAuthorization: Basic \nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=nfc&machine_type=bike&serial_number=ser123&payload=abc123&transponder_type=GOOGLE&issuer_id=123456789\n```\n\n```VTAP\nPOST /api/v1/oauth/token\nAuthorization: Basic \nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=nfc&machine_type=bike&serial_number=ser123&payload=abc123&configuration_index=2\n```\n\n\n## \U0001F4DC Terms and Conditions Handling\n\nSome users may not have accepted the latest EGYM Terms and Conditions (T&C). In that case, the user details will indicate that the user did not accepted them yet and partners must handle T&C interaction:\n\n### 1. Retrieve the T&C and privacy policy\n\n```T&C\nGET /api/v1/ressources/terms-and-conditions\nContent-Type: text/html\n\nlocale=en_US\n```\n\n```PP\nGET /api/v1/ressources/privacy-policy\nContent-Type: text/html\n\nlocale=en_US\n```\nResponse will be terms.\n\n\n### 2. Prompt the user to accept or decline\n\n- If **accepted**, proceed to step 3\n- If **declined**, exit the login flow gracefully\n\n### 3. Submit confirmation of acceptance\n\n```http\nPUT /api/v1/users/terms-and-conditions\nAuthorization: Bearer {access_token}\nContent-Type: application/json\n\nlocale=en_US\n```\n\n---\n\n## ⏱ Rate Limit\n\nAuthentication is rate-limited per `client_id`: **10 requests per second**.\n\n---\n\n# \U0001F504 Integration Flow Overview\n\n```plaintext\n[Authenticate (POST /oauth/token)]\n ↓\n[Get User Details (GET /users)]\n ↓\n ┌────────────────────────────┬────────────────────────────┐\n │ │ │\n │ Fitness Equipment │ Measurement Devices │\n │ │ │\n │ - Get Training Plan │ - Submit Measurements │\n │ → GET /training-plans │ → POST /measurements/* │\n │ - Submit Workout │ │\n │ → POST /workouts │ │\n └────────────────────────────┴────────────────────────────┘\n```\n\n---\n\n# ⚠️ Error Handling\n\nAll endpoints return descriptive errors when something goes wrong.\n\n### Common HTTP Status Codes\n\n| Code | Meaning | Notes |\n|------|------------------|-------|\n| 200 | OK | Success |\n| 204 | No Content | Success, no response body |\n| 400 | Bad Request | Validation or field error |\n| 401 | Unauthorized | Missing or invalid auth - credentials are invalid |\n| 403 | Forbidden | Insufficient permission - endpoint is not available for equipment type|\n| 404 | Not Found | Resource doesn't exist - unknown RFID|\n| 500 | Internal Error | Something unexpected |\n\n### Sample Error Response\n\n```json\n{\n \"message\": \"Invalid input\",\n \"fieldErrors\": [\n {\n \"name\": \"username\",\n \"message\": \"Must not be null\"\n }\n ]\n}\n```\n\n---\n\nIf you have any questions or need help integrating, contact us at [integrations@egym.com](mailto:integrations@egym.com).\n\n---\n\n# \U0001F4DD Integration FAQ – Standalone Devices API\n\nThis section addresses common technical integration questions specific to the **Standalone Devices** model.\n\n## 1. Supported Login Types\n- **EGYM ID Login** (`grant_type=password`): EGYM Username & password\n- **RFID Login** (`grant_type=rfid`): RFID number + format\n- **NFC Login** (`grant_type=nfc`): NFC payload + issuer ID or configuration index\n\n## 2. Handling Terms & Conditions\nIf the user has not accepted the latest Terms & Conditions (T&C):\n1. Retrieve T&C URL: `GET /users/{userId}/terms`\n2. Display link to user for acceptance/rejection\n3. If accepted: `POST /users/{userId}/terms/accept`\n4. If rejected: exit the login flow gracefully\n\n## 3. Key Endpoints for Standalone Integrations\n- Authentication: `POST /oauth/token`\n- User details: `GET /users`\n- Training plan: `GET /training-plans`\n- Workout results: `POST /workouts`\n- Measurements: `POST /measurements/*`\n\n## 4. Error Handling Best Practices\n- **401 Unauthorized**: Invalid/missing credentials or token\n- **403 Forbidden**: Permission issue\n- **429 Too Many Requests**: Rate limit exceeded; retry after delay\n- Important: Provide meaningful error messages to the user (not just error codes, ideally with localized description), or silently retry when user experience requires\n\n## 5. Rate Limits\n- Authentication: 10 requests/second per `client_id`\n- Other endpoints: See API documentation\n\n## 6. Testing\n- Use the **EGYM Sandbox** with provided test credentials.\n- Test credentials are prodvided by EGYM\n- Test login flows, training plan retrieval, workout submission, and T&C handling\n\n## 7. What if I send the same measurement twice?**\nUse a different `uniqueMeasurementId` each time.\n\n## .When should I use `issuerId` vs `configurationIndex` in NFC?**\nThese are alternatives for identifying the wallet pass source. Provide one depending on the integration project with EGYM. Configuration Index is only supported when using the Dot Origin VTAP readers.\n" termsOfService: https://egym.com/us/terms/ contact: name: EGYM GmbH url: https://egym.com/ email: support@egym.com license: name: Pending License url: https://egym.com/ version: 1.2.0 servers: - url: https://partner-api.ext-1.test.co.egym.coffee description: Test (ext-1) - url: https://partner-api.api.egym.com description: Production tags: - name: Cardio Test paths: /api/v1/cardio/tests/{cardioTestId}: put: tags: - Cardio Test summary: Update Test description: This endpoint should be invoked in order to create a new step into an ongoing cardio test operationId: updateTest parameters: - name: cardioTestId in: path required: true schema: type: integer format: int64 requestBody: content: application/json: schema: $ref: '#/components/schemas/UpdateCardioTestDTO' required: true responses: '200': description: Ok content: application/json: schema: $ref: '#/components/schemas/CardioTestResponseDTO' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' '401': description: Unauthorized '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' security: - user: [] /api/v1/cardio/reminders: put: tags: - Cardio Test summary: Postpone Reminder description: This endpoint should be invoked in order to postpone the reminder of a cardio test operationId: postponeReminder responses: '204': description: No Content '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' '401': description: Unauthorized '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' security: - user: [] /api/v1/cardio/tests: post: tags: - Cardio Test summary: Initialise Test description: This endpoint should be invoked in order to create and initialize a cardio test for a user operationId: initialiseTest requestBody: content: application/json: schema: $ref: '#/components/schemas/InitialCardioTestDTO' required: true responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/CardioTestResponseDTO' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' '401': description: Unauthorized '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponseDTO' security: - user: [] components: schemas: CardioTestResultDTO: type: object properties: results: type: array description: The historical results. items: $ref: '#/components/schemas/HistoricalCardioResultDTO' cardioAge: type: integer format: int32 description: The cardio age result. example: 26 vo2max: type: number format: double description: The maximum rate of oxygen consumption measured. example: '45.1' InitialCardioTestDTO: type: object properties: personalFitnessRating: type: string description: The personal fitness rating. enum: - NOT_FIT - AVERAGE - VERY_FIT userInfo: $ref: '#/components/schemas/CardioUserInfoDTO' description: The user information that needs to be updated while initializing the cardio test. required: - personalFitnessRating - userInfo UpdateCardioTestDTO: type: object properties: levelId: type: integer format: int32 description: The level id. example: 1 stepCompletionType: type: string description: The completion type. enum: - COMPLETED - ABORTED ratedPerceivedExertion: type: string description: The rated perceived exertion. enum: - NO_EFFORT - VERY_EASY - EASY - COMFORTABLE - SOMEWHAT_DIFFICULT - DIFFICULT - HARD - VERY_HARD - EXTREMELY_HARD - MAXIMAL_EFFORT duration: type: integer format: int64 description: The duration in milliseconds. example: 60000 required: - levelId - stepCompletionType FieldErrorDTO: type: object properties: name: type: string description: The field name. message: type: string description: The error message. example: Must not be null. rejectedValue: description: The rejected value. example: Must not be null. CardioLevelDTO: type: object properties: levelId: type: integer format: int32 description: Identifier for this cardio level. example: 1 duration: type: integer format: int64 description: The duration in milliseconds. example: 60000 watts: type: number format: double description: Power/Resistance in watts. example: 100 stepsPerMinute: type: number format: double description: The number of steps per minute for the activity. example: 80 resistance: type: number format: double description: The resistance of the exercise. example: 3 rotationsPerMinute: type: number format: double description: The rotations per minute. example: 60 stepHeight: type: number format: double description: The height of each step. example: 10 speed: type: number format: double description: The speed of the exercise in metres per second. example: '0.666' incline: type: number format: double description: The incline of the exercise. example: 3 rampAngle: type: number format: double description: The angle of the ramp. example: 2 strideLengthZone: type: number format: double description: The stride length zone. example: 2 CardioUserInfoDTO: type: object properties: height: type: number format: double description: The body height of the user in centimeters. example: 180 weight: type: number format: double description: The body weight of the user in kilograms. example: 75 trainingGoal: type: string deprecated: true description: The user training goal. enum: - WEIGHT_LOSS - FITNESS - MUSCLE_GAIN - BBP - BBP_AND_WEIGHT_LOSS - REHASPORT trainingProgram: type: string description: The user training program. enum: - WEIGHT_LOSS - FITNESS - MUSCLE_GAIN - BBP - BBP_AND_WEIGHT_LOSS - REHASPORT - IMMUNITY dateOfBirth: type: string format: date description: User date of birth in ISO 8601 format (yyyy-MM-dd). gender: type: string description: User gender. enum: - MALE - FEMALE - NON_BINARY HistoricalCardioResultDTO: type: object properties: timestamp: type: integer format: int64 description: The date of the test. example: 1553074068097 maxWatts: type: number format: double description: The max watts. example: 200 cardioAge: type: integer format: int32 description: The cardio age result. example: 26 vo2max: type: number format: double description: The maximum rate of oxygen consumption measured. example: '45.1' ApiErrorResponseDTO: type: object description: Standard error response returned by the API for all error status codes. properties: timestamp: type: integer format: int64 description: Unix epoch milliseconds when the error occurred. example: 1775122463109 path: type: string description: The request path that triggered the error. example: /api/v1/measurements/body/history status: type: integer format: int32 description: HTTP status code. example: 400 error: type: string description: Short textual description of the HTTP status. example: Bad Request requestId: type: string description: Unique identifier for the request, useful for correlating logs and support tickets. example: d0c0fd9e-236 message: type: string description: Human-readable description of the error. example: 'Invalid request parameters: value must be greater than or equal to 1 and less than or equal to 1000' fieldErrors: type: array description: List of field-level validation errors. Only present when the error was caused by request-body binding failures or custom form validation. items: $ref: '#/components/schemas/FieldErrorDTO' errorCode: type: string description: Machine-readable error code for domain-specific errors. Only present for exceptions that carry a custom error code. example: USER_NOT_FOUND CardioTestResponseDTO: type: object properties: id: type: integer format: int64 description: Identifier for this cardio test. example: 1 testLevel: $ref: '#/components/schemas/CardioLevelDTO' description: The Cardio Test level. cardioTestResult: $ref: '#/components/schemas/CardioTestResultDTO' description: The Cardio Test result. instructions: type: array description: The instructions to the next step. items: type: string enum: - CONTINUE - FINISH - REQUEST_TRAINING_GOAL - REQUEST_TRAINING_PROGRAM - REQUEST_HEIGHT - REQUEST_WEIGHT - REQUEST_GENDER - REQUEST_DATE_OF_BIRTH - TERMS_AND_CONDITIONS - SUGGEST_CARDIO_TEST - FIRST_CARDIO_TEST - TEST_SKIPPED uniqueItems: true securitySchemes: user: type: http scheme: bearer bearerFormat: JWT partner: type: oauth2 flows: clientCredentials: tokenUrl: /api/v1/oauth/token scopes: {} x-tagGroups: - name: Authentication tags: - OAuth - name: Partners Information tags: - Partners - name: Measurements tags: - Body Measurement - Cardio Measurement - Flexibility Measurement - name: User tags: - User - Image - Statistics - name: Machine tags: - Machine - name: Resource tags: - Resource - name: Workouts tags: - Strength Workouts - Cardio Workouts - Open Exercise Workouts - name: Cardio Test tags: - Cardio Test - name: Logging tags: - Logging