# XS4 Face > XS4 Face is Salto's face-as-a-credential access control platform. This site documents the APIs that external partners use to manage users, enrollment and consent for face recognition on GT7 terminals. These interfaces are in public BETA. Integrations come in two levels. A Level 1 integration manages users only, and enrollment is handled by the Salto Enrollment App. A Level 2 integration also embeds enrollment — terms of service, image capture and consent — into the partner's own app. All requests are authenticated with an OAuth 2.0 client credentials token from the identity service. Face images are biometric personal data; read the Legal section before building an integration. Generated 2026-08-13 from XS4 Face documentation. --- # Introduction Source: /introduction/ ## Face as a credential Salto XS4 Face's face recognition technology is engineered to provide convenient, secure access using only a face as a credential. Support information for the XS4 Face platform is available on the [Salto Support Site](https://support.saltosystems.com/xs4face/). ## What is XS4 Face? XS4 Face is a hybrid (cloud and on-premise) solution that enables a face to be used as a credential. ### Device The camera unit consists of two parts: a controller and a camera. It detects the presence of a person, sends an image to the recognition appliance and displays the result to the user. ### Recognition Appliance (Server) The recognition appliance is located on-premise and is responsible for image analysis, detecting fakes (presentation attack detection) and recognising an enrolled user. When a user enrolls their face, the image is passed to the appliance, where a biometric template is created and the image is destroyed. A customer can have an appliance per site, enabling all of their users to be recognised at all of their sites. ### XS4 Face Cloud The cloud manages the XS4 Face infrastructure from a single place. It is hosted in Azure West Europe (Netherlands) and provides the API required to manage users, their consent and their images. ### Cloud Enrollment Service The Cloud Enrollment Service is provided by Salto to enable remote enrollment at scale. It ensures all enrollment images are of sufficient quality and maintains a copy of all user images to enable system upgrades and disaster recovery. ### Salto Enrollment App XS4 Face includes a Salto branded enrollment app that makes user enrollment simple. Users are authenticated using email based codes. The app captures the user's acceptance of the terms of service for the Enrollment Service, their enrollment image and their consent to the use of that image for biometric processing — ensuring GDPR compliance. Use of the app is optional: a [Level 2 integration](#level-2---enrolling-users) gives you direct access to the enrollment service features from your own app, with your own branding. ### Console The XS4 Face console is the management UI for the face recognition system. Its primary use is system setup and configuration, enabling appliances to be built and devices to be commissioned. It also provides diagnostic logging to aid support teams. ## Adding face recognition to your software solution The XS4 Face solution integrates with the GT7 family of terminals to provide a face as a credential. There are two levels of integration with XS4 Face that can be achieved, depending on your requirements. ### Level 1 - User management Your membership management system links to Salto XS4 Face Cloud to add, remove and query users. Users enroll their face for XS4 Face using their own mobile device and Salto's Cloud Enrollment Service. ### Level 2 - Enrolling users Instead of using Salto's Enrollment App, the enrollment process is embedded into your customer's app. Salto's Cloud Enrollment Service still quality checks enrollment images and manages delivering them to the face recognition appliance on site. ### Tenant hierarchy You will be created as a partner within the XS4 Face platform. Each of your customers is represented as an XS4 Face site. A customer with more than one physical location can have all of those locations managed within a single XS4 Face site. --- # Getting Started Source: /introduction/_getting_started/ ## Before you start ### 1. Register as a Salto XS4 Face partner Contact the Gantner Integrations Team with the following: - [ ] A signed NDA with Gantner (an existing GT7 NDA is sufficient) - [ ] Your partner name - [ ] The email address and name of your first developer (you will then be able to manage your own users) ### 2. Request credentials for the acceptance environment - [ ] The Gantner Integrations Team will provide a `clientId` and `clientSecret` for the acceptance environment, for use with the [Authentication API](_auth) ### 3. Access the XS4 Face console The XS4 Face acceptance environment is used for your development and testing. The console can be accessed here: [https://console-accept.eu.xs4face.app/](https://console-accept.eu.xs4face.app/) ## Create your first site ### 4. Open Manage Organisations ![no sites](images/first-access.png) Click **OK** to close the **No Sites Yet** dialog, then click **Manage Organisations**. ### 5. Add a site Select the **Sites** tab in your partner, then **Add New Site**, then **Create Site**. ![create site](images/create-site.png) ### 6. Provide a site name > [!info] > The **Site Name** is shown to the user during the enrollment process to > confirm the location where face recognition will take place. ### 7. Provide a company name and privacy policy URL > [!info] > The **Company Name** is shown to the user during the enrollment process to > identify the legal entity that will be performing biometric processing of > their data. The **Privacy Policy URL** gives the user access to that legal > entity's privacy policy, explaining how their data will be managed and who to > contact. Templates for the privacy policy and further information are > available on the Salto Systems > [XS4 Face Support Site](https://support.saltosystems.com/xs4face/system-documents/software/data-protection/). Close the **Manage Organisations** screen. ### 8. Enable the GT7 feature Enable the **GT7** feature on the **Features** page. > [!tip] > Users and enrollments can now be managed. You do not need an **XS4 Face > Appliance** or a **GT7 device** to do this, but both are needed for full > system testing. Once the **GT7** feature is enabled, click the cog icon to see its settings. `Feature ID` is the value needed for all API calls that manage this site. ![GT7 Feature](images/gt7-feature.png) > [!important] > **Send enrolment emails** is enabled by default, so that users are invited to > use the Salto Enrollment App. [Level 2](_index#level-2---enrolling-users) > integrations, where your own app manages the enrollment, should disable this > setting. ## Make your first API calls ### 9. Obtain an access token Call the [Authentication API](_auth) with the `clientId` and `clientSecret` provided by the Gantner Integrations Team, for example: ```bash curl -X POST "https://identity-acc.eu.my-clay.com/connect/token" \ -u ":" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "scope=xs4face.featureapi.users" ``` A successful request returns a JSON response containing the `access_token`: ```json { "access_token": "eyJhbGciOiJ...", "token_type": "Bearer", "expires_in": 3600, "scope": "xs4face.featureapi.users" } ``` Use the returned `access_token` in the `Authorization` header of subsequent API calls, for example `Authorization: Bearer eyJhbGciOiJ...`. ### 10. Enable a user for face recognition Use the [Users API](MMS/_api) `PUT /features/{featureId}/users/{userId}` endpoint, passing the `Feature ID` from the **GT7** feature settings and a `userId` of your choice, for example: ```bash curl -X PUT "https://api-accept.eu.xs4face.app/api/features//users/" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com" }' ``` A successful request returns the created or updated user: ```json { "userId": "", "email": "user@example.com", "status": "PENDING_TOS", "lastUpdated": "2026-08-04T10:57:43.332Z" } ``` > [!note] > The GT7 device requests access using the provided `userId` when the user is > recognised. This triggers an enrollment invite to the email address provided. Once the user accepts the terms of service, provides an image and confirms their consent, they will be able to use face recognition. --- # Approval Process Source: /introduction/_approval/ ## Level 1 Enable user management through the MMS. Partner credentials are restricted to the `xs4face.featureapi.users` scope. ```mermaid {align="center" zoom="true"} graph LR; Register --> AcceptCreds["Accept Credentials Provided"] AcceptCreds --> DevL1["Develop Level 1"] DevL1 --> RequestApproval["Request Approval"] RequestApproval --> Approved Approved --> ProdCreds["Prod Credentials Provided"] ``` ### Approval requirements You must be able to demonstrate that: - [ ] A user can be added with a user ID (credential) and email address - [ ] A user can be removed using their user ID - [ ] The UI shows the user's enrollment status - [ ] An enrolled user is granted access when the GT7 requests their user ID ## Level 2 Enable a user to enroll their image for face recognition through a customer App. ```mermaid {align="center" zoom="true"} graph LR; Request[Request Enrollment API access] --> ProfileUpdated["xs4face.featureapi.enrollment scope added to profile"] ProfileUpdated --> DevL2["Develop Level 2"] DevL2 --> RequestApproval["Request Approval"] RequestApproval --> Approved Approved --> ProdCreds["Prod Credentials Provided/Upgraded"] ``` ### Approval requirements You must be able to demonstrate that: - [ ] A user receives a notification to enroll for face recognition - [ ] A user can view the terms and the privacy policy - [ ] A user can accept the terms and confirm consent for biometric processing - [ ] A user is shown instructions for capturing a good image - [ ] The user's camera is used to capture a live image - [ ] A user is given feedback on image quality and can retake the image if required - [ ] A user can remove their consent ## Requesting approval Email when you are ready for your integration to be tested. An online meeting will be arranged where the system is demonstrated to meet the requirements defined above. --- # Change Log Source: /introduction/_change_log/ All notable changes to the XS4 Face documentation and API specifications. This file is generated from conventional commit messages — do not edit it by hand. ## [0.2.0](https://github.com/touchbyteltd/api-specs/releases/tag/v0.2.0) — 2026-08-13 ### Features - Remove redundant 'get feature user consent' endpoint (#8) ([a8c0da1](https://github.com/touchbyteltd/api-specs/commit/a8c0da176d00c7183a9918daba5e3bc94b382ca0)) ## [0.1.3](https://github.com/touchbyteltd/api-specs/releases/tag/v0.1.3) — 2026-08-11 ### Documentation - Remove 504 responses from API documentation (#6) ([bbb4755](https://github.com/touchbyteltd/api-specs/commit/bbb4755bc496805c966c3c8b8ee5db7dca104cff)) ## [0.1.2](https://github.com/touchbyteltd/api-specs/releases/tag/v0.1.2) — 2026-08-11 ### Documentation - Mark incomplete pages as draft and adjust nav metadata (#7) ([3d36182](https://github.com/touchbyteltd/api-specs/commit/3d3618284175a49fa72150bde90d3bb1e34f1684)) ## [0.1.1](https://github.com/touchbyteltd/api-specs/releases/tag/v0.1.1) — 2026-08-05 ### Documentation - Add clarity to End User Agreement (#5) ([46b8d7b](https://github.com/touchbyteltd/api-specs/commit/46b8d7b1d5e32f4f09896fc12afa3cc1e53bd89c)) ## [0.1.0](https://github.com/touchbyteltd/api-specs/releases/tag/v0.1.0) — 2026-08-04 ### Documentation - **site-integration-users**: :heavy_minus_sign: remove development env domain ([a6aaa2a](https://github.com/touchbyteltd/api-specs/commit/a6aaa2a1748ef2f2e980180178d940124a86f724)) - Initial release (#4) ([a868504](https://github.com/touchbyteltd/api-specs/commit/a8685043c5083f394840cb773bff5ff9244e2ab9)) ### Features - Add site integration users spec ([e887e46](https://github.com/touchbyteltd/api-specs/commit/e887e46e307c22269eeb87a0d324ed22a398dd1a)) - Update to get method to include users in repsonse ([29ce3b8](https://github.com/touchbyteltd/api-specs/commit/29ce3b8d6245f25effc78a04f390e938067af97f)) ### Performance - Generate html at build rather than runtime ([052ce5e](https://github.com/touchbyteltd/api-specs/commit/052ce5ee49d94d3170593bded461c5f6600c973a)) --- # Management App (Level 1) Source: /mms/ A **Level 1** integration links your membership management system to XS4 Face Cloud so that you can add, update, query and remove the users who are entitled to use face recognition at a site. The [Users API](_api) covers the whole of that lifecycle. Enrollment itself — terms of service, image capture and consent — is handled either by the Salto Enrollment App or, for a [Level 2](../introduction/#level-2---enrolling-users) integration, by the [Enrollment API](../app/_api). --- # User Consent Lifecycle Source: /mms/_user_lifecycle/ ```mermaid {align="center" zoom="true"} stateDiagram-v2 [*] --> PENDING: PUT users/{userId} state PENDING { PENDING_TOS --> PENDING_IMAGE: PUT users/{userId}/termsOfService PENDING_IMAGE --> PENDING_CONSENT: POST users/{userId}/image } PENDING --> ACTIVE: PUT users/{userId}/consent PENDING --> [*]: DELETE users/{userId}
DELETE users/{userId}/consent ACTIVE --> [*]: DELETE users/{userId}
DELETE users/{userId}/consent ``` ## States Each state is left by calling the endpoint below. ### User level These are set when the user first enrolls into the Salto Enrollment Service - **PENDING_TOS** — TOS is contained within the Salto End User Licence Agreement, accept the terms of service with [`PUT /features/{featureId}/users/{userId}/termsOfService`](App/_api) ([Enrollment API](App/_api)) to move to `PENDING_IMAGE`. - **PENDING_IMAGE** — Upload a valid image with [`POST /features/{featureId}/users/{userId}/image`](App/_api) ([Enrollment API](App/_api)) to move to `PENDING_CONSENT`. ### Site / Customer Level These are set on a per-site / customer basis. - **PENDING_CONSENT** — Grant consent with [`PUT /features/{featureId}/users/{userId}/consent`](App/_api) ([Enrollment API](App/_api)) to move to `ACTIVE`. - **ACTIVE** — The user is enrolled. Remove them with [`DELETE /features/{featureId}/users/{userId}`](MMS/_api) ([Users API](MMS/_api)). From **any** state the user leaves the lifecycle when their consent is revoked with [`DELETE /features/{featureId}/users/{userId}/consent`](App/_api) ([Enrollment API](App/_api)) or they are removed with [`DELETE /features/{featureId}/users/{userId}`](MMS/_api) ([Users API](MMS/_api)). > [!important] > Revoking consent removes the user — it does not return them to an earlier > state. Once revoked, `GET users/{userId}` responds `400` with the error code > `E002` (resource does not exist), and the user must be added again with > [`PUT users/{userId}`](MMS/_api) to start a fresh enrollment. There is no > status value representing a revoked user. --- # Users API Source: /mms/_api/ The complete OpenAPI specification: ```yaml openapi: 3.0.0 info: version: 1.2.0 title: Users API description: Endpoints for managing users on a site. servers: - url: https://api-accept.eu.xs4face.app/api description: Acceptance testing server - url: https://api.eu.xs4face.app/api description: Production server tags: - name: Users description: Operations for managing users' enrollment. components: schemas: Error: type: object required: - title - detail properties: title: type: string detail: type: string errors: type: array items: type: object required: [detail, code] properties: code: type: string detail: type: string additionalProperties: false BadRequestCode: type: string enum: [ E000, E001, E002, E003 ] description: | * `E000` — Request properties are invalid. * `E001` — Request is missing required properties. * `E002` — Resource does not exist. * `E003` — Property that must be unique is already associated with a different resource. BadRequest: allOf: - $ref: '#/components/schemas/Error' - type: object properties: code: $ref: '#/components/schemas/BadRequestCode' Forbidden: $ref: '#/components/schemas/Error' Unauthorized: $ref: '#/components/schemas/Error' User: type: object required: - userId - email - status - lastUpdated properties: userId: type: string email: type: string format: email status: type: string enum: [PENDING_TOS, PENDING_IMAGE, PENDING_CONSENT, ACTIVE] lastUpdated: type: string format: date-time description: Last updated timestamp for the user, e.g. 2026-08-04T10:57:43.332Z additionalProperties: false UsersResponse: type: object required: - featureUsers properties: featureUsers: type: array items: $ref: '#/components/schemas/User' limit: type: integer minimum: 0 example: 10 offset: type: integer minimum: 0 example: 0 count: type: integer minimum: 0 example: 50 description: The total number of users for the feature site additionalProperties: false UpsertUserRequest: type: object required: - email properties: email: type: string format: email description: The email address of the user in the feature additionalProperties: false responses: User: description: Success with user details content: application/json: schema: $ref: '#/components/schemas/User' Users: description: Success with users details content: application/json: schema: $ref: '#/components/schemas/UsersResponse' NoContent: description: Success with no content BadRequest: description: Bad request content: application/problem+json: schema: $ref: '#/components/schemas/BadRequest' Unauthorized: description: Unauthorized content: application/problem+json: schema: $ref: '#/components/schemas/Unauthorized' Forbidden: description: Forbidden content: application/problem+json: schema: $ref: '#/components/schemas/Forbidden' InternalServerError: description: Internal server error content: application/problem+json: schema: $ref: '#/components/schemas/Error' GatewayTimeout: description: Timeout content: application/problem+json: schema: $ref: '#/components/schemas/Error' securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT security: - BearerAuth: [] paths: /features/{featureId}/users: get: summary: List users description: Returns users with optional filtering and pagination. operationId: listFeatureUsers tags: - Users parameters: - name: featureId in: path required: true schema: type: string - name: email in: query description: Filter by email. required: false schema: type: string format: email - name: limit in: query description: Maximum number of users to return. required: false schema: type: integer minimum: 0 - name: offset in: query description: Number of users to skip before collecting result rows. required: false schema: type: integer minimum: 0 responses: 200: $ref: '#/components/responses/Users' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' /features/{featureId}/users/{userId}: get: summary: Get user description: Returns a user by ID. operationId: getFeatureUser tags: - Users parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string responses: 200: $ref: '#/components/responses/User' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' put: summary: Add or update user description: Creates a new user or updates an existing user if the ID already exists. operationId: upsertFeatureUser tags: - Users parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpsertUserRequest' responses: 200: $ref: '#/components/responses/User' 201: $ref: '#/components/responses/User' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' delete: summary: Remove user description: Deletes a user. operationId: deleteFeatureUser tags: - Users parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string responses: 204: $ref: '#/components/responses/NoContent' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' ``` --- # User App (Level 2) Source: /app/ A **Level 2** integration embeds the enrollment process into your own app instead of using the Salto Enrollment App. Your app collects the three things a user must provide before face recognition can be used: 1. Acceptance of the terms of service for the Enrollment Service 2. An enrollment image 3. Consent to the use of that image for biometric processing The [Enrollment API](_api) exposes each of these steps, and the Cloud Enrollment Service still quality checks the image and delivers it to the recognition appliance on site. > [!important] > A Level 2 integration takes on the responsibility for capturing terms of > service acceptance and consent in a compliant way. Read > [Enrollment Design](_design) before you build it — it carries the licence > agreement text you must display and the privacy policy you must link to. --- # Enrollment Design Source: /app/_design/ ![App Enrollment Flow](images/app-enrollment-flow.png) ## Example - **[Enrollment walkthrough](https://support.saltosystems.com/xs4face/user-guide/user/enrollment/):** example behaviour for image quality feedback and retakes. - **[User disenrollment](https://support.saltosystems.com/xs4face/user-guide/user/disenrollment/):** example consent deletion. The endpoints referenced below are documented in the [Enrollment API](App/_api). Accessing this API requires an authentication token with the scope `xs4face.featureapi.enrollment`. ## User enrollment profile The user profile, including their current status, can be retrieved with the following endpoint: [`GET /features/{featureId}/users/{userId}`](App/_api) ``` json { "userId": "AB-11-22", "email": "john@example.com", "status": "PENDING_IMAGE", "lastUpdated": "2026-08-04T10:57:43.332Z" } ``` The user status is described in [User Lifecycle](MMS/_user_lifecycle). ## Terms of service Record acceptance of the **Salto End User Licence Agreement** for face recognition. The following text **MUST** be displayed to the user to allow Salto Systems to manage face enrollment. Show this step every time the user accesses the feature while their status is not yet `ACTIVE`. > [!info] > Face recognition services are provided by Salto Systems, S.L. By continuing, you accept that the use of this service is subject to Salto's [End User Licence Agreement](https://saltosystems.com/en/legal-data/software-terms/access-control-cloud-applications/end-user-license-agreement/). > > Salto Systems, S.L. will process and store your personal data in the EU, in order to manage face recognition services, in accordance with Salto Systems' [Privacy Policy](https://saltosystems.com/en/legal-data/privacy/privacy-policies/access-control-cloud-applications/). - **Partner responsibility:** display the End User Licence Agreement for face recognition to the user and record acceptance. - **Display when:** the user accesses the feature and their status is not `ACTIVE`. - **Endpoint:** [`PUT /features/{featureId}/users/{userId}/termsOfService`](App/_api) - **Complete when:** the user status is not `PENDING_TOS`. - **Blocked while:** acceptance is missing — the journey cannot finish even if all other enrollment data is present. > [!note] > A user may have already accepted the **Salto End User Licence Agreement** > through their use of face recognition at another provider. To avoid confusion, > and to make sure the user knows who will be managing their data within the app, > show this step whenever they have not yet provided consent. ## Image submission Capture a user image and handle the API's validation outcome. - **Integrator responsibility:** take the picture and show clear feedback on validation failures so the user knows what to fix before retaking. - **Endpoint:** [`POST /features/{featureId}/users/{userId}/image`](App/_api) - **Complete when:** the image is accepted. - **Blocked by:** image quality or validation errors. A valid image is a core prerequisite for enrollment. > [!tip] > A user may have already enrolled an image through their use of face > recognition at another provider. When this is the case they can be taken > straight to consent capture, so check the status returned for the user rather > than assuming every enrollment starts at `PENDING_TOS`. ### Key response codes | Status | Meaning | | --- | --- | | `204` | Image accepted — the user moves to `PENDING_CONSENT`. | | `400` | Image validation failed — see the image error codes below. | A `400` response returns one or more image error codes in the `errors` array: | Code | Meaning | Example | | --- | --- | --- | | `IE001` | Multiple faces found | [image](/images/multiple_faces.jpg) | | `IE002` | Poor resolution | [image](/images/poor_resolution.jpg) | | `IE003` | Poor exposure | [image](/images/poor_exposure.jpg) | | `IE004` | Poor greyscale profile | None | | `IE005` | Image contains hotspots | [image](/images/no_hot_spots.jpg) | | `IE006` | Face not facing forward | [image](/images/not_frontal.jpg) | | `IE007` | Lighting is not uniform | [image](/images/not_uniform_lighting.jpg) | | `IE008` | Eyes are not open | [image](/images/eyes_not_open.jpg) | | `IE009` | Tint on glasses | [image](/images/tinted_glasses.jpg) | | `IE010` | Image is not sharp | [image](/images/not_sharp.jpg) | | `IE011` | Mouth is not closed | [image](/images/mouth_open.jpg) | > [!note] > Once accepted, the user image cannot be retrieved through the API or replaced. > This prevents a user swapping their image to give someone else — a family > member, for example — access. ## Consent capture Allow the user to view the privacy policy that details the use of face recognition, and record their consent. - **Integrator responsibility:** give the user access to a valid privacy policy. - **Endpoint:** [`PUT /features/{featureId}/users/{userId}/consent`](App/_api) - **Complete when:** the user status is `ACTIVE`. - **Blocked while:** consent is missing. Present this step clearly, as consent is a prerequisite for approval. ## Revocation and removal This is the user-triggered withdrawal of consent through the app. Membership expiry, and other reasons for removing a user, are handled by the MMS. Both are required to fulfil the obligations of GDPR. - **Integrator responsibility:** provide a mechanism in the app for the user to opt out of face recognition. - **Endpoint:** [`DELETE /features/{featureId}/users/{userId}/consent`](App/_api) - **Complete when:** the user no longer exists — see below. > [!important] > Revocation removes the user rather than moving them to an earlier state. Every > endpoint in this API then responds `400` with the error code `E002` (resource > does not exist), so treat revocation as terminal in your app. Re-enrolling > means adding the user again through the [Users API](MMS/_api) and starting > from `PENDING_TOS`. --- # Enrollment API Source: /app/_api/ The complete OpenAPI specification: ```yaml openapi: 3.0.0 info: version: 1.3.0 title: Enrollment API description: Endpoints for managing enrollment on a site. servers: - url: https://api-accept.eu.xs4face.app/api description: Acceptance testing server - url: https://api.eu.xs4face.app/api description: Production server tags: - name: Users description: Operations for reading users on a site. - name: Consent description: Managing users' consent for biometric enrollment. - name: TOS description: Managing users' acceptance of the Terms of Service for biometric enrollment. - name: Image description: Managing users' image for biometric enrollment. components: schemas: Error: type: object required: - title - detail properties: title: type: string detail: type: string errors: type: array items: type: object required: [detail, code] properties: code: type: string detail: type: string additionalProperties: false BadRequestCode: type: string enum: [ E000, E001, E002, E003 ] description: | * `E000` — Request properties are invalid. * `E001` — Request is missing required properties. * `E002` — Resource does not exist. * `E003` — Property that must be unique is already associated with a different resource. BadRequestImageCode: type: string enum: [ IE001, IE002, IE003, IE004, IE005, IE006, IE007, IE008, IE009, IE010, IE011 ] description: | * `IE001` — Multiple faces found * `IE002` — Poor resolution * `IE003` — Poor exposure * `IE004` — Poor greyscale profile * `IE005` — Image contains hotspots * `IE006` — Face not facing forward * `IE007` — Lighting is not uniform * `IE008` — Eyes are not open * `IE009` — Tint on glasses * `IE010` — Image is not sharp * `IE011` — Mouth is not closed BadRequest: allOf: - $ref: '#/components/schemas/Error' - type: object properties: code: $ref: '#/components/schemas/BadRequestCode' BadRequestImage: allOf: - $ref: '#/components/schemas/Error' - type: object properties: code: $ref: '#/components/schemas/BadRequestCode' errors: type: array items: type: object required: [detail, code] properties: detail: type: string code: $ref: '#/components/schemas/BadRequestImageCode' additionalProperties: false Forbidden: $ref: '#/components/schemas/Error' Unauthorized: $ref: '#/components/schemas/Error' User: type: object required: - userId - email - status - lastUpdated properties: userId: type: string email: type: string format: email status: type: string enum: [PENDING_TOS, PENDING_IMAGE, PENDING_CONSENT, ACTIVE] lastUpdated: type: string format: date-time description: Last updated timestamp for the user, e.g. 2026-08-04T10:57:43.332Z additionalProperties: false Consent: type: object required: - status properties: status: type: string enum: [PENDING_TOS, PENDING_IMAGE, PENDING_CONSENT, ACTIVE] example: ACTIVE additionalProperties: false responses: User: description: Success with user details content: application/json: schema: $ref: '#/components/schemas/User' Consent: description: Success with consent details content: application/json: schema: $ref: '#/components/schemas/Consent' NoContent: description: Success with no content BadRequest: description: Bad request content: application/problem+json: schema: $ref: '#/components/schemas/BadRequest' BadRequestImage: description: Bad request content: application/problem+json: schema: $ref: '#/components/schemas/BadRequestImage' Unauthorized: description: Unauthorized content: application/problem+json: schema: $ref: '#/components/schemas/Unauthorized' Forbidden: description: Forbidden content: application/problem+json: schema: $ref: '#/components/schemas/Forbidden' InternalServerError: description: Internal server error content: application/problem+json: schema: $ref: '#/components/schemas/Error' GatewayTimeout: description: Timeout content: application/problem+json: schema: $ref: '#/components/schemas/Error' securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT security: - BearerAuth: [] paths: /features/{featureId}/users/{userId}: get: summary: Get user description: Returns a user by ID. operationId: getFeatureUser tags: - Users parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string responses: 200: $ref: '#/components/responses/User' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' /features/{featureId}/users/{userId}/image: post: summary: Upload user image description: Validates and uploads an image for a user operationId: uploadFeatureUserImage tags: - Image parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string requestBody: required: true content: multipart/form-data: schema: type: object required: - image properties: image: type: string format: binary description: The image file to upload for the user's profile. responses: 204: $ref: '#/components/responses/NoContent' 400: $ref: '#/components/responses/BadRequestImage' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' /features/{featureId}/users/{userId}/consent: put: summary: Update user consent description: Provides consent for the user. operationId: updateFeatureUserConsent tags: - Consent parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string responses: 204: $ref: "#/components/responses/NoContent" 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' delete: summary: Revoke user consent description: Revokes the user's consent. operationId: revokeFeatureUserConsent tags: - Consent parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string responses: 204: $ref: '#/components/responses/NoContent' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' /features/{featureId}/users/{userId}/termsOfService: put: summary: Update terms of service consent description: Accepts the terms of service for the user. operationId: updateFeatureUserTermsOfService tags: - TOS parameters: - name: featureId in: path required: true schema: type: string - name: userId in: path required: true schema: type: string responses: 204: $ref: '#/components/responses/NoContent' 400: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/InternalServerError' ``` --- # Authentication Source: /_auth/ The complete OpenAPI specification: ```yaml openapi: 3.0.3 info: version: 1.0.0 title: Identity Service description: OpenID Connect / OAuth 2.0 token endpoint for the Clay identity service. servers: - url: https://identity-acc.eu.my-clay.com description: Acceptance environment - url: https://identity.eu.my-clay.com description: Production environment tags: - name: Token description: OAuth 2.0 token operations. paths: /connect/token: post: tags: - Token summary: Request an access token description: >- OAuth 2.0 token endpoint. Exchange client credentials for an access token. security: - clientSecretBasic: [] requestBody: required: true content: application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenRequest' responses: '200': description: Token successfully issued. content: application/json: schema: $ref: '#/components/schemas/TokenResponse' '400': description: Invalid request. content: application/json: schema: $ref: '#/components/schemas/TokenError' components: securitySchemes: clientSecretBasic: type: http scheme: basic description: >- Client authentication using HTTP Basic (client_id:client_secret). schemas: TokenRequest: type: object required: - grant_type properties: grant_type: type: string description: The OAuth 2.0 grant type. (client_credentials) example: client_credentials scope: type: string description: Read/Write access to feature API. (xs4face.featureapi.users xs4face.featureapi.enrollment) example: xs4face.featureapi.users xs4face.featureapi.enrollment client_id: type: string description: Client identifier. Required when using client_secret_post authentication. client_secret: type: string description: Client secret. Required when using client_secret_post authentication. TokenResponse: type: object properties: access_token: type: string description: The issued access token. token_type: type: string example: Bearer expires_in: type: integer description: Token lifetime in seconds. example: 3600 scope: type: string description: The scopes granted for the access token. TokenError: type: object properties: error: type: string example: invalid_client error_description: type: string ```