# Get Certificate Source: https://docs.konstantly.com/api-reference/certificates/get-certificate GET /certificates/{certificateId} Get a certificate by ID Retrieve detailed information about a specific certificate. ## Path Parameters The unique identifier of the certificate ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Certificate ID Certificate name Certificate description Course ID Course name URL to the certificate image Timestamp of certificate issue Timestamp when the certificate expires User who has got the certificate User ID Full name Interface language (de, en, es, fr, pl, pt, ru) Job title (optional) User location (optional) User timezone in TZ format Email address User ban status User role information Whether created via API User avatar information Custom user attributes ### Error Responses Returned when the certificate is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/certificates/123' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Dict def get_certificate(api_key: str, certificate_id: int) -> Dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/certificates/{certificate_id}" headers = { "X-API-KEY": api_key } try: response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"Certificate {certificate_id} not found") response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error getting certificate: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: certificate = get_certificate("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) print(f"Certificate: {certificate['name']}") print(f"Course: {certificate['courseName']}") print(f"Issued to: {certificate['user']['name']}") print(f"Achieved at: {certificate['achievedAt']}") if certificate.get('expiresAt'): print(f"Expires at: {certificate['expiresAt']}") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to get certificate: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class CertificatesAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async getCertificate(certificateId) { try { const response = await this.client.get(`/certificates/${certificateId}`); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`Certificate ${certificateId} not found`); } throw new Error(`Failed to get certificate: ${error.response?.data?.message || error.message}`); } } // Helper method to format certificate details formatCertificateInfo(certificate) { return { certificateName: certificate.name, courseName: certificate.courseName, issuedTo: certificate.user.name, achievedAt: new Date(certificate.achievedAt * 1000).toLocaleString(), expiresAt: certificate.expiresAt ? new Date(certificate.expiresAt * 1000).toLocaleString() : 'Never' }; } } // Example usage async function main() { const certificatesApi = new CertificatesAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const certificate = await certificatesApi.getCertificate(123); const info = certificatesApi.formatCertificateInfo(certificate); console.log('Certificate Details:'); console.log(`Name: ${info.certificateName}`); console.log(`Course: ${info.courseName}`); console.log(`Issued to: ${info.issuedTo}`); console.log(`Achieved at: ${info.achievedAt}`); console.log(`Expires at: ${info.expiresAt}`); } catch (error) { console.error('Failed to get certificate:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function getCertificate($certificateId) { $ch = curl_init(); $url = "{$this->baseUrl}/certificates/{$certificateId}"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Certificate $certificateId not found"); } if ($httpCode !== 200) { throw new Exception("Failed to get certificate. Status code: $httpCode"); } return json_decode($response, true); } // Helper method to format certificate details public function formatCertificateInfo($certificate) { return [ 'certificateName' => $certificate['name'], 'courseName' => $certificate['courseName'], 'issuedTo' => $certificate['user']['name'], 'achievedAt' => date('Y-m-d H:i:s', $certificate['achievedAt']), 'expiresAt' => isset($certificate['expiresAt']) ? date('Y-m-d H:i:s', $certificate['expiresAt']) : 'Never' ]; } } // Example usage try { $certificatesApi = new CertificatesAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); $certificate = $certificatesApi->getCertificate(123); $info = $certificatesApi->formatCertificateInfo($certificate); echo "Certificate Details:\n"; echo "Name: {$info['certificateName']}\n"; echo "Course: {$info['courseName']}\n"; echo "Issued to: {$info['issuedTo']}\n"; echo "Achieved at: {$info['achievedAt']}\n"; echo "Expires at: {$info['expiresAt']}\n"; } catch (Exception $e) { echo "Failed to get certificate: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Map; public class CertificatesAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); public Map getCertificate(int certificateId) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/certificates/" + certificateId)) .header("X-API-KEY", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { throw new Exception("Certificate " + certificateId + " not found"); } if (response.statusCode() != 200) { throw new Exception("Failed to get certificate. Status: " + response.statusCode()); } return mapper.readValue(response.body(), Map.class); } // Helper method to format certificate details public static class CertificateInfo { public final String certificateName; public final String courseName; public final String issuedTo; public final LocalDateTime achievedAt; public final LocalDateTime expiresAt; public CertificateInfo(Map certificate) { this.certificateName = (String) certificate.get("name"); this.courseName = (String) certificate.get("courseName"); Map user = (Map) certificate.get("user"); this.issuedTo = (String) user.get("name"); this.achievedAt = LocalDateTime.ofInstant( Instant.ofEpochSecond(((Number) certificate.get("achievedAt")).longValue()), ZoneId.systemDefault() ); if (certificate.get("expiresAt") != null) { this.expiresAt = LocalDateTime.ofInstant( Instant.ofEpochSecond(((Number) certificate.get("expiresAt")).longValue()), ZoneId.systemDefault() ); } else { this.expiresAt = null; } } } public static void main(String[] args) { CertificatesAPI api = new CertificatesAPI(); try { Map certificate = api.getCertificate(123); CertificateInfo info = new CertificateInfo(certificate); System.out.println("Certificate Details:"); System.out.println("Name: " + info.certificateName); System.out.println("Course: " + info.courseName); System.out.println("Issued to: " + info.issuedTo); System.out.println("Achieved at: " + info.achievedAt); System.out.println("Expires at: " + (info.expiresAt != null ? info.expiresAt : "Never")); } catch (Exception e) { System.err.println("Failed to get certificate: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "id": 123, "name": "Course Completion Certificate", "description": "Certificate of completion for Advanced Marketing Course", "courseId": 2, "courseName": "Advanced Marketing", "imageUrl": "http://s3.amazonaws.com/qwerty/certificate.png", "achievedAt": 1234567890, "expiresAt": 1334567890, "user": { "id": "1234567890", "name": "John Smith", "email": "john@example.com", "language": "en", "timezone": "Europe/London", "occupation": "Marketing Manager", "location": "London", "isBanned": false, "role": { "alias": "learner" }, "fromApi": false, "image": null, "attributes": {} } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Assign Course Source: https://docs.konstantly.com/api-reference/courses/course-assignments POST /courses/{courseId}/assignments Assign the course to users or groups Assign a course to specific users and/or groups. ## Path Parameters The unique identifier of the course ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Array of user IDs to assign the course to Array of group IDs to assign the course to Timestamp of date and time (UTC) when the course should be assigned Timestamp of date and time (UTC) of the deadline for course completion ## Response Users the course was successfully assigned to User ID User's full name User's email User's role information Groups the course was successfully assigned to Group ID Group name Number of users in the group Number of courses assigned to the group ### Error Responses Returned when the course is not found HTTP status code (404) Error message ```bash theme={null} curl --request POST \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/assignments \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "users": [1, 2, 3], "groups": [4, 5], "assignAt": 1676937600, "deadlineAt": 1677024000 }' ``` ```json theme={null} { "users": [ { "id": "1234567890", "name": "John Doe", "email": "john@example.com", "role": { "alias": "learner" } } ], "groups": [ { "id": 4, "name": "Sales Team", "usersCount": 10, "coursesCount": 5 } ] } ``` # List Course Groups Source: https://docs.konstantly.com/api-reference/courses/course-assignments-groups GET /courses/{courseId}/assignments/groups Get the list of groups assigned to the course Retrieve a list of all groups that have been assigned to a specific course. ## Path Parameters The unique identifier of the course ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Array of group assignment objects Timestamp when the course was assigned Exact timestamp when the assignment expires Period within which group members must complete the course Time unit (hours, days, weeks, months) Number of units Group details Group ID Parent group ID Name of the group Number of users in the group Number of courses assigned to the group Total number of group assignments ### Error Responses Returned when the course is not found HTTP status code (404) Error message ```bash theme={null} curl --request GET \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/assignments/groups \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```json theme={null} { "assignments": [ { "assignedAt": 1443183322, "deadlineAt": 0, "deadlinePeriod": { "measure": "months", "value": 3 }, "group": { "id": 1, "parentId": null, "name": "Sales Team", "usersCount": 10, "coursesCount": 5 } } ], "totalCount": 1 } ``` # List Course Users Source: https://docs.konstantly.com/api-reference/courses/course-assignments-users GET /courses/{courseId}/assignments/users Get the list of users assigned to the course Retrieve a list of all users who have been assigned to a specific course. ## Path Parameters The unique identifier of the course ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Array of user assignment objects Timestamp when the course was assigned Timestamp when user started the course Assignment deadline timestamp Timestamp when user finished the course Current progress (0-100) Result value (0-100) Whether user has started the course Whether user has finished the course Whether the assignment has expired User details User ID Full name Email address User role Total number of assignments ### Error Responses Returned when the course is not found HTTP status code (404) Error message ```bash theme={null} curl --request GET \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/assignments/users \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```json theme={null} { "assignments": [ { "assignedAt": 1443183322, "startedAt": 1443188932, "deadlineAt": 1444188932, "finishedAt": 1443188946, "progressValue": 100, "resultValue": 50, "isCourseStarted": true, "isCourseFinished": true, "isExpired": false, "user": { "id": "1234567890", "name": "John Doe", "email": "john@example.com", "role": { "alias": "learner" } } } ], "totalCount": 1 } ``` # List Course Certificates Source: https://docs.konstantly.com/api-reference/courses/course-certificates GET /courses/{courseId}/certificates Get a list of certificates for the course (20 per page) Retrieve a list of all certificates issued for a specific course. ## Path Parameters The unique identifier of the course ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Query Parameters Entry number to start listing from The number of entries to be listed ## Response Array of certificate objects Certificate ID Certificate name Certificate description Course ID Course name URL to the certificate image Timestamp of certificate issue Timestamp when certificate expires User who received the certificate User ID Full name Email address User role information Total number of certificates ### Error Responses Returned when the course is not found HTTP status code (404) Error message ```bash theme={null} curl --request GET \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/certificates \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```json theme={null} { "certificates": [ { "id": 123, "name": "Course Completion Certificate", "description": "Certificate of completion", "courseId": 2, "courseName": "Advanced Sales Techniques", "imageUrl": "http://s3.amazonaws.com/certificates/cert123.png", "achievedAt": 1234567890, "expiresAt": 1334567890, "user": { "id": "1234567890", "name": "John Doe", "email": "john@example.com", "role": { "alias": "learner" } } } ], "totalCount": 1 } ``` # Get Course Statistics Source: https://docs.konstantly.com/api-reference/courses/course-statistics GET /courses/{courseId}/statistics Get detailed statistics for a specific course Retrieve comprehensive statistics for a specific course, including user progress, completion rates, and test results. ## Path Parameters The unique identifier of the course ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response General course statistics Number of courses shared to users Number of courses assigned to users Number of users who started the course Number of users who finished the course Average progress across all users Course test statistics Test ID Test name Number of questions Number of users taken the test Total number of attempts Number of successful completions Average test result Users distribution by progress ranges Users with 0% progress Users with 1-10% progress Users with 11-20% progress Users with 21-30% progress Users with 31-40% progress Users with 41-50% progress Users with 51-60% progress Users with 61-70% progress Users with 71-80% progress Users with 81-90% progress Users with 91-99% progress Users with 100% progress Question tag statistics Tag ID Tag name Average result value for this tag (0-100) ### Error Responses Returned when the course is not found HTTP status code (404) Error message ```bash theme={null} curl --request GET \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/statistics \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```json theme={null} { "statistics": { "sharedCoursesCount": 10, "assignedCoursesCount": 50, "startUsersCount": 45, "finishedUsersCount": 30, "averageResultValue": 85 }, "tests": [ { "id": 1, "name": "Final Test", "questionsCount": 20, "usersCount": 40, "attemptsCount": 45, "successUsersCount": 35, "averageResultValue": 88 } ], "progress": { "0": 2, "1-10": 1, "11-20": 3, "21-30": 9, "31-40": 0, "41-50": 4, "51-60": 3, "61-70": 5, "71-80": 7, "81-90": 4, "91-99": 3, "100": 2 }, "tags": [ { "tagId": 1, "tag": "Sales Basics", "resultValue": 85 }, { "tagId": 2, "tag": "Advanced Techniques", "resultValue": 75 } ] } ``` # Get Course Source: https://docs.konstantly.com/api-reference/courses/get-course GET /courses/{courseId} Get detailed information about a specific course Retrieve detailed information about a specific course by its ID. ## Path Parameters The unique identifier of the course ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Course ID Course name Course annotation Indicates whether the course is a draft Creation timestamp Last update timestamp Publication timestamp Course cover image details Image ID URL to full-size image URL to image 36x36 px URL to image 330x130 px Course description Attached files File name File size in bytes Download URL MIME type ### Error Responses Returned when the course is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/courses/123' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def get_course(course_id): url = f"https://YOURSITE.konstant.ly/openapi/v1/courses/{course_id}" headers = { "X-API-KEY": "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv" } try: response = requests.get(url, headers=headers) response.raise_for_status() # Raises an exception for 4XX/5XX errors return response.json() except requests.exceptions.RequestException as e: if response.status_code == 404: print(f"Course {course_id} not found") else: print(f"Error fetching course: {str(e)}") return None # Example usage course = get_course(123) if course: print(f"Course name: {course['name']}") print(f"Description: {course['description']}") # Print attached files for file in course.get('attachedFiles', []): print(f"Attached file: {file['name']} ({file['size']} bytes)") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getCourse(courseId) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/courses/${courseId}`, { headers: { 'X-API-KEY': '1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' } } ); return response.data; } catch (error) { if (error.response && error.response.status === 404) { console.error(`Course ${courseId} not found`); } else { console.error('Error fetching course:', error.message); } throw error; } } // Example usage getCourse(123) .then(course => { console.log(`Course: ${course.name}`); console.log(`Description: ${course.description}`); // Log attached files course.attachedFiles?.forEach(file => { console.log(`Attached file: ${file.name} (${file.size} bytes)`); }); }) .catch(error => { console.error('Failed to get course:', error); }); ``` ```php PHP theme={null} "https://YOURSITE.konstant.ly/openapi/v1/courses/$courseId", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { echo 'Error:' . curl_error($ch); return null; } curl_close($ch); if ($httpCode === 404) { echo "Course $courseId not found\n"; return null; } return json_decode($response, true); } // Example usage $course = getCourse(123); if ($course) { echo "Course: " . $course['name'] . "\n"; echo "Description: " . $course['description'] . "\n"; // Print attached files foreach ($course['attachedFiles'] as $file) { echo "Attached file: {$file['name']} ({$file['size']} bytes)\n"; } } ?> ``` ```java Java theme={null} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class GetCourse { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; public static void getCourse(int courseId) { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/courses/" + courseId)) .header("X-API-KEY", API_KEY) .GET() .build(); try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { System.out.println("Course " + courseId + " not found"); return; } if (response.statusCode() != 200) { System.out.println("Error: " + response.statusCode()); return; } System.out.println(response.body()); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { getCourse(123); } } ``` ```json Response theme={null} { "id": 123, "name": "Course name", "annotation": "Course annotation", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229, "image": { "id": 1234, "original": "http://example.com/images/original/1234.jpg", "mini": "http://example.com/images/mini/1234.jpg", "small": "http://example.com/images/small/1234.jpg" }, "description": "Detailed course description", "attachedFiles": [ { "name": "myfile.txt", "size": 123, "url": "http://example.com/files/myfile.txt", "mime": "text/plain" } ] } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # List Courses Source: https://docs.konstantly.com/api-reference/courses/get-courses GET /courses Get a list of courses (20 per page) Retrieve a paginated list of all courses on your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Query Parameters Entry number to start listing from. Use this for pagination. Example: `20` to get the second page of results ## Response Array of course objects Course ID Course name Course annotation Indicates whether the course is a draft Creation timestamp Last update timestamp Publication timestamp Course cover image details Image ID URL to full-size image URL to image 36x36 px URL to image 330x130 px The total number of courses ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/courses?offset=0' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests url = "https://YOURSITE.konstant.ly/openapi/v1/courses" headers = { "X-API-KEY": "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv" } params = { "offset": 0 # Optional: for pagination } response = requests.get(url, headers=headers, params=params) courses = response.json() for course in courses['courses']: print(f"Course: {course['name']}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function listCourses(offset = 0) { try { const response = await axios.get('https://YOURSITE.konstant.ly/openapi/v1/courses', { headers: { 'X-API-KEY': '1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' }, params: { offset: offset } }); return response.data; } catch (error) { console.error('Error fetching courses:', error.message); throw error; } } // Example usage listCourses() .then(data => { console.log(`Found ${data.totalCount} courses`); data.courses.forEach(course => { console.log(`- ${course.name}`); }); }) .catch(error => { console.error('Failed to list courses:', error); }); ``` ```php PHP theme={null} 'https://YOURSITE.konstant.ly/openapi/v1/courses', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ] ]); $response = curl_exec($ch); $courses = json_decode($response, true); if (curl_errno($ch)) { echo 'Error:' . curl_error($ch); } curl_close($ch); foreach ($courses['courses'] as $course) { echo "Course: " . $course['name'] . "\n"; } ?> ``` ```java Java theme={null} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class ListCourses { public static void main(String[] args) { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://YOURSITE.konstant.ly/openapi/v1/courses")) .header("X-API-KEY", "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") .GET() .build(); try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); } catch (Exception e) { e.printStackTrace(); } } } ``` ```json Response theme={null} { "courses": [ { "id": 123, "name": "Course name", "annotation": "Course annotation", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229, "image": { "id": 1234, "original": "http://example.com/images/original/1234.jpg", "mini": "http://example.com/images/mini/1234.jpg", "small": "http://example.com/images/small/1234.jpg" } } ], "totalCount": 1 } ``` # Unassign Group from Course Source: https://docs.konstantly.com/api-reference/courses/unassign-course-group DELETE /courses/{courseId}/assignments/groups/{groupId} Unassign a specific group from a course Remove a course assignment from a specific group. ## Path Parameters The unique identifier of the course The unique identifier of the group to unassign ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response On successful unassignment, returns an empty response with HTTP status code 200. ### Error Responses Returned when either the course or group is not found HTTP status code (404) Error message ```bash theme={null} curl --request DELETE \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/assignments/groups/456 \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```json theme={null} { // Empty response on success } ``` ```json 404 Not Found theme={null} { "status": 404, "message": "Not found" } ``` # Unassign User from Course Source: https://docs.konstantly.com/api-reference/courses/unassign-course-user DELETE /courses/{courseId}/assignments/users/{userId} Unassign a specific user from a course Remove a course assignment from a specific user. ## Path Parameters The unique identifier of the course The unique identifier of the user to unassign ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response On successful unassignment, returns an empty response with HTTP status code 200. ### Error Responses Returned when either the course or user is not found HTTP status code (404) Error message ```bash theme={null} curl --request DELETE \ --url https://YOURSITE.konstant.ly/openapi/v1/courses/123/assignments/users/1234567890 \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```json theme={null} { // Empty response on success } ``` ```json 404 Not Found theme={null} { "status": 404, "message": "Not found" } ``` # Get Group Custom Attributes Source: https://docs.konstantly.com/api-reference/custom-attributes/get-group-attributes GET /custom/attributes/groups Get a list of custom group attributes Retrieve all custom attributes configured for groups on your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Array of custom attribute objects with the following properties: Attribute ID Attribute name Attribute type: * 1: text * 2: date * 3: single choice * 4: multiple choice * 5: user choice * 6: yes/no * 7: external link * 8: number Attribute API identifier Display position order Whether the attribute is mandatory Additional data for choice-type attributes Item ID Item position Item value ## Notes on Attribute Types * Text (type 1): Single-line text input * Date (type 2): Date selector * Single Choice (type 3): Dropdown with single selection * Multiple Choice (type 4): Checkbox list with multiple selections * User Choice (type 5): User selector * Yes/No (type 6): Boolean toggle * External Link (type 7): URL input * Number (type 8): Numeric input ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/groups' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import List, Dict, Any def get_group_attributes(api_key: str) -> List[Dict[str, Any]]: url = "https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/groups" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # Example usage try: attributes = get_group_attributes("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") def get_type_name(type_id: int) -> str: types = { 1: "Text", 2: "Date", 3: "Single Choice", 4: "Multiple Choice", 5: "User Choice", 6: "Yes/No", 7: "External Link", 8: "Number" } return types.get(type_id, "Unknown") for attr in attributes: print(f"Attribute: {attr['name']}") print(f"Type: {get_type_name(attr['type'])}") print(f"Required: {attr['isRequired']}") if attr['extraData']: print("Options:") for option in attr['extraData']: print(f" - {option['value']}") print("---") except Exception as e: print(f"Error fetching group attributes: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getGroupAttributes(apiKey) { try { const response = await axios.get( 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/groups', { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { console.error('Error fetching group attributes:', error.message); throw error; } } // Example usage const getTypeName = (typeId) => { const types = { 1: 'Text', 2: 'Date', 3: 'Single Choice', 4: 'Multiple Choice', 5: 'User Choice', 6: 'Yes/No', 7: 'External Link', 8: 'Number' }; return types[typeId] || 'Unknown'; }; getGroupAttributes('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv') .then(attributes => { attributes.forEach(attr => { console.log(`Attribute: ${attr.name}`); console.log(`Type: ${getTypeName(attr.type)}`); console.log(`Required: ${attr.isRequired}`); if (attr.extraData?.length) { console.log('Options:'); attr.extraData.forEach(option => { console.log(` - ${option.value}`); }); } console.log('---'); }); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} [ { "id": 1, "name": "Region", "type": 3, "apiId": "region", "position": 1, "isRequired": true, "extraData": [ { "id": 1, "position": 1, "value": "North America" }, { "id": 2, "position": 2, "value": "Europe" }, { "id": 3, "position": 3, "value": "Asia Pacific" } ] }, { "id": 2, "name": "Budget Center", "type": 1, "apiId": "budget_center", "position": 2, "isRequired": true, "extraData": null }, { "id": 3, "name": "Active", "type": 6, "apiId": "is_active", "position": 3, "isRequired": false, "extraData": null } ] ``` # Get User Custom Attributes Source: https://docs.konstantly.com/api-reference/custom-attributes/get-user-attributes GET /custom/attributes/users Get a list of custom user attributes Retrieve all custom attributes configured for users on your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Array of custom attribute objects with the following properties: Attribute ID Attribute name Attribute type: * 1: text * 2: date * 3: single choice * 4: multiple choice * 5: user choice * 6: yes/no * 7: external link * 8: number Attribute API identifier Display position order Whether the attribute is mandatory Additional data for choice-type attributes Item ID Item position Item value Whether the attribute can be modified ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/users' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import List, Dict, Any def get_user_attributes(api_key: str) -> List[Dict[str, Any]]: url = "https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/users" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # Example usage try: attributes = get_user_attributes("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") for attr in attributes: print(f"Attribute: {attr['name']}") print(f"Type: {attr['type']}") print(f"Required: {attr['isRequired']}") if attr['extraData']: print("Options:") for option in attr['extraData']: print(f" - {option['value']}") print("---") except Exception as e: print(f"Error fetching user attributes: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getUserAttributes(apiKey) { try { const response = await axios.get( 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/users', { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { console.error('Error fetching user attributes:', error.message); throw error; } } // Example usage getUserAttributes('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv') .then(attributes => { attributes.forEach(attr => { console.log(`Attribute: ${attr.name}`); console.log(`Type: ${attr.type}`); console.log(`Required: ${attr.isRequired}`); if (attr.extraData?.length) { console.log('Options:'); attr.extraData.forEach(option => { console.log(` - ${option.value}`); }); } console.log('---'); }); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} [ { "id": 1, "name": "Department", "type": 3, "apiId": "department", "position": 1, "isRequired": true, "extraData": [ { "id": 1, "position": 1, "value": "Sales" }, { "id": 2, "position": 2, "value": "Marketing" }, { "id": 3, "position": 3, "value": "Engineering" } ], "isEditable": true }, { "id": 2, "name": "Employee ID", "type": 1, "apiId": "employee_id", "position": 2, "isRequired": true, "extraData": null, "isEditable": true } ] ``` # Update Group Custom Attributes Source: https://docs.konstantly.com/api-reference/custom-attributes/update-group-attributes POST /custom/attributes/groups Update custom group attributes Update or create custom attributes for groups. This endpoint allows you to modify existing attributes or add new ones. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Array of attribute objects, each containing: Attribute name Attribute type: * 1: text * 2: date * 3: single choice * 4: multiple choice * 5: user choice * 6: yes/no * 7: external link * 8: number Attribute API identifier (generated if not provided) Display position order Whether attribute is mandatory Whether attribute is visible in the interface For choice-type attributes (types 3 and 4): Item position Item value ## Important Notes 1. Attribute Types: * Text fields (type 1) are limited to 255 characters * Date fields (type 2) expect ISO format dates * Choice fields (types 3 and 4) require extraData with options * Yes/No fields (type 6) are stored as booleans 2. Updating Existing Attributes: * Match existing apiId to update rather than create * Cannot change type of existing attributes * Can modify options for choice-type fields ## Response Array of updated attribute objects with the same structure as the GET endpoint response. ### Error Responses Validation error response HTTP status code (400) Error message Field-specific validation errors ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/groups' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '[ { "name": "Region", "type": 3, "apiId": "region", "position": 1, "isRequired": true, "isVisible": true, "extraData": [ { "position": 1, "value": "North America" }, { "position": 2, "value": "Europe" }, { "position": 3, "value": "Asia Pacific" } ] }, { "name": "Active Status", "type": 6, "apiId": "is_active", "position": 2, "isRequired": false, "isVisible": true } ]' ``` ```python Python theme={null} import requests from typing import List, Dict, Any def update_group_attributes(api_key: str, attributes: List[Dict[str, Any]]) -> List[Dict[str, Any]]: url = "https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/groups" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } response = requests.post(url, headers=headers, json=attributes) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) response.raise_for_status() return response.json() # Example usage attributes = [ { "name": "Region", "type": 3, "apiId": "region", "position": 1, "isRequired": True, "isVisible": True, "extraData": [ {"position": 1, "value": "North America"}, {"position": 2, "value": "Europe"}, {"position": 3, "value": "Asia Pacific"} ] }, { "name": "Active Status", "type": 6, "apiId": "is_active", "position": 2, "isRequired": False, "isVisible": True } ] try: updated_attributes = update_group_attributes( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", attributes ) print("Attributes updated successfully") for attr in updated_attributes: print(f"Updated: {attr['name']}") if attr['extraData']: print("Options:") for option in attr['extraData']: print(f" - {option['value']}") print("---") except Exception as e: print(f"Failed to update attributes: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function updateGroupAttributes(apiKey, attributes) { try { const response = await axios.post( 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/groups', attributes, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { if (error.response?.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } throw error; } } // Example usage const attributes = [ { name: "Region", type: 3, apiId: "region", position: 1, isRequired: true, isVisible: true, extraData: [ { position: 1, value: "North America" }, { position: 2, value: "Europe" }, { position: 3, value: "Asia Pacific" } ] }, { name: "Active Status", type: 6, apiId: "is_active", position: 2, isRequired: false, isVisible: true } ]; updateGroupAttributes('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', attributes) .then(updatedAttributes => { console.log('Attributes updated successfully'); updatedAttributes.forEach(attr => { console.log(`Updated: ${attr.name}`); if (attr.extraData?.length) { console.log('Options:'); attr.extraData.forEach(option => { console.log(` - ${option.value}`); }); } console.log('---'); }); }) .catch(error => console.error('Failed to update attributes:', error.message)); ``` ```json Response theme={null} [ { "id": 1, "name": "Region", "type": 3, "apiId": "region", "position": 1, "isRequired": true, "extraData": [ { "id": 1, "position": 1, "value": "North America" }, { "id": 2, "position": 2, "value": "Europe" }, { "id": 3, "position": 3, "value": "Asia Pacific" } ] }, { "id": 2, "name": "Active Status", "type": 6, "apiId": "is_active", "position": 2, "isRequired": false, "extraData": null } ] ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "attributes[0].name": ["Name is required"], "attributes[0].type": ["Invalid attribute type"], "attributes[0].extraData": ["Options required for choice type attributes"] } } ``` # Update User Custom Attributes Source: https://docs.konstantly.com/api-reference/custom-attributes/update-user-attributes POST /custom/attributes/users Update custom user attributes with new configurations ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body The request body should contain an array of attribute configuration objects. Each attribute object has these properties: ID of an existing attribute if updating (omit for new attributes) Display name of the attribute Type of attribute: * 1: text * 2: date * 3: single choice * 4: multiple choice * 5: user choice * 6: yes/no * 7: external link * 8: number API identifier for the attribute. Generated automatically if not provided. Display order position of the attribute Whether this attribute is mandatory Whether this attribute is visible in the interface Whether this attribute can be modified Required for choice-type attributes (types 3 and 4). Array of options, each containing: Display order of the option Display value of the option ## Response Returns an array of the updated attribute configurations with IDs assigned. ### Error Responses Validation error response HTTP status code (400) Error message Validation errors by field ## Usage Notes 1. Type-specific requirements: * Text (type 1): Limited to 255 characters * Date (type 2): Must be valid ISO format dates * Single/Multiple Choice (types 3, 4): Must include extraData options * Number (type 8): Must be valid numeric values 2. Updating existing attributes: * Include the ID to update rather than create new * Cannot change type of existing attributes * Can modify options for choice attributes * Position numbers should be unique 3. Validation: * All required fields must be provided * Type must be valid (1-8) * Position must be a positive integer * Choice types require at least one option * apiId must be unique if provided ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/users' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '[ { "name": "Division", "type": 3, "apiId": "division", "position": 1, "isRequired": true, "isVisible": true, "isEditable": true, "extraData": [ { "position": 1, "value": "Sales" }, { "position": 2, "value": "Marketing" } ] }, { "name": "Start Date", "type": 2, "apiId": "start_date", "position": 2, "isRequired": true, "isVisible": true, "isEditable": true } ]' ``` ```python Python theme={null} import requests from typing import List, Dict, Any def update_user_attributes(api_key: str, attributes: List[Dict[str, Any]]) -> List[Dict[str, Any]]: url = "https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/users" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } response = requests.post(url, headers=headers, json=attributes) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) response.raise_for_status() return response.json() # Example usage attributes = [ { "name": "Division", "type": 3, # single choice "apiId": "division", "position": 1, "isRequired": True, "isVisible": True, "isEditable": True, "extraData": [ {"position": 1, "value": "Sales"}, {"position": 2, "value": "Marketing"} ] }, { "name": "Start Date", "type": 2, # date "apiId": "start_date", "position": 2, "isRequired": True, "isVisible": True, "isEditable": True } ] try: updated_attributes = update_user_attributes( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", attributes ) print("Attributes updated successfully") for attr in updated_attributes: print(f"Updated: {attr['name']} (ID: {attr['id']})") except Exception as e: print(f"Failed to update attributes: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function updateUserAttributes(apiKey, attributes) { try { const response = await axios.post( 'https://YOURSITE.konstant.ly/openapi/v1/custom/attributes/users', attributes, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { if (error.response?.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } throw error; } } // Example usage const attributes = [ { name: "Division", type: 3, // single choice apiId: "division", position: 1, isRequired: true, isVisible: true, isEditable: true, extraData: [ { position: 1, value: "Sales" }, { position: 2, value: "Marketing" } ] }, { name: "Start Date", type: 2, // date apiId: "start_date", position: 2, isRequired: true, isVisible: true, isEditable: true } ]; updateUserAttributes('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', attributes) .then(updatedAttributes => { console.log('Attributes updated successfully'); updatedAttributes.forEach(attr => { console.log(`Updated: ${attr.name} (ID: ${attr.id})`); }); }) .catch(error => console.error('Failed to update attributes:', error.message)); ``` ```json Response theme={null} [ { "id": 1, "name": "Division", "type": 3, "apiId": "division", "position": 1, "isRequired": true, "isEditable": true, "extraData": [ { "id": 1, "position": 1, "value": "Sales" }, { "id": 2, "position": 2, "value": "Marketing" } ] }, { "id": 2, "name": "Start Date", "type": 2, "apiId": "start_date", "position": 2, "isRequired": true, "isEditable": true, "extraData": null } ] ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "attributes[0].name": ["Name is required"], "attributes[0].type": ["Invalid attribute type"], "attributes[0].extraData": ["Options required for choice type attributes"] } } ``` # Create Group Source: https://docs.konstantly.com/api-reference/groups/create-group POST /groups Create a new group Create a new group on your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Group name Parent group ID Group description ## Response Group ID Parent group ID Name of the group Group description Number of users in the group Number of courses assigned to the group Group cover image details Image ID URL to full-size image URL to image 36x36 px URL to image 330x130 px ### Error Responses Returned when the request is invalid HTTP status code (400) Error message Validation errors by field ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "name": "New Sales Team", "parentId": 1, "description": "Sales team for new region" }' ``` ```python Python theme={null} import requests from dataclasses import dataclass from typing import Optional, Dict, Any @dataclass class GroupRequest: name: str parent_id: Optional[int] = None description: Optional[str] = None def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "parentId": self.parent_id, "description": self.description } def create_group(api_key: str, group_request: GroupRequest) -> Dict[str, Any]: url = "https://YOURSITE.konstant.ly/openapi/v1/groups" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } try: response = requests.post(url, headers=headers, json=group_request.to_dict()) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error creating group: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: group_request = GroupRequest( name="New Sales Team", parent_id=1, description="Sales team for new region" ) group = create_group("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", group_request) print(f"Group created successfully!") print(f"ID: {group['id']}") print(f"Name: {group['name']}") print(f"Users Count: {group['usersCount']}") print(f"Courses Count: {group['coursesCount']}") if group.get('parentId'): print(f"Parent Group ID: {group['parentId']}") except Exception as e: print(f"Failed to create group: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } }); } async createGroup({ name, parentId, description }) { try { const response = await this.client.post('/groups', { name, parentId, description }); return response.data; } catch (error) { if (error.response) { if (error.response.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } throw new Error(`Failed to create group: ${error.response.data.message || error.message}`); } throw error; } } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const group = await groupsApi.createGroup({ name: 'New Sales Team', parentId: 1, description: 'Sales team for new region' }); console.log('Group created successfully!'); console.log(`ID: ${group.id}`); console.log(`Name: ${group.name}`); console.log(`Users Count: ${group.usersCount}`); console.log(`Courses Count: ${group.coursesCount}`); if (group.parentId) { console.log(`Parent Group ID: ${group.parentId}`); } } catch (error) { console.error('Failed to create group:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function createGroup($groupData) { $ch = curl_init(); $url = "{$this->baseUrl}/groups"; $jsonData = json_encode($groupData); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $jsonData, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey, 'Content-Type: application/json' ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); $responseData = json_decode($response, true); if ($httpCode === 400) { echo "Validation errors:\n"; if (isset($responseData['errors'])) { foreach ($responseData['errors'] as $field => $errors) { echo "$field: " . implode(', ', $errors) . "\n"; } } throw new Exception($responseData['message'] ?? 'Invalid request'); } if ($httpCode !== 200) { throw new Exception("Failed to create group. Status code: $httpCode"); } return $responseData; } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); $groupData = [ 'name' => 'New Sales Team', 'parentId' => 1, 'description' => 'Sales team for new region' ]; $group = $groupsApi->createGroup($groupData); echo "Group created successfully!\n"; echo "ID: {$group['id']}\n"; echo "Name: {$group['name']}\n"; echo "Users Count: {$group['usersCount']}\n"; echo "Courses Count: {$group['coursesCount']}\n"; if (isset($group['parentId'])) { echo "Parent Group ID: {$group['parentId']}\n"; } } catch (Exception $e) { echo "Failed to create group: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Map; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); // Request class public static class GroupRequest { @JsonProperty("name") public String name; @JsonProperty("parentId") public Integer parentId; @JsonProperty("description") public String description; public GroupRequest(String name, Integer parentId, String description) { this.name = name; this.parentId = parentId; this.description = description; } } // Response class (reusing from list endpoint) public static class Group { @JsonProperty("id") public int id; @JsonProperty("parentId") public Integer parentId; @JsonProperty("name") public String name; @JsonProperty("usersCount") public int usersCount; @JsonProperty("coursesCount") public int coursesCount; @JsonProperty("image") public GroupImage image; } public Group createGroup(GroupRequest request) throws Exception { String jsonBody = mapper.writeValueAsString(request); HttpRequest httpRequest = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups")) .header("X-API-KEY", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) .build(); HttpResponse response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 400) { Map errorResponse = mapper.readValue(response.body(), Map.class); System.out.println("Validation errors:"); if (errorResponse.containsKey("errors")) { Map errors = (Map) errorResponse.get("errors"); errors.forEach((field, messages) -> { System.out.println(field + ": " + messages); }); } throw new Exception(errorResponse.getOrDefault("message", "Invalid request").toString()); } if (response.statusCode() != 200) { throw new Exception("Failed to create group. Status: " + response.statusCode()); } return mapper.readValue(response.body(), Group.class); } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { GroupRequest request = new GroupRequest( "New Sales Team", 1, "Sales team for new region" ); Group group = api.createGroup(request); System.out.println("Group created successfully!"); System.out.println("ID: " + group.id); System.out.println("Name: " + group.name); System.out.println("Users Count: " + group.usersCount); System.out.println("Courses Count: " + group.coursesCount); if (group.parentId != null) { System.out.println("Parent Group ID: " + group.parentId); } } catch (Exception e) { System.err.println("Failed to create group: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "id": 2, "parentId": 1, "name": "New Sales Team", "usersCount": 0, "coursesCount": 0, "image": null } ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "name": ["Name is required"] } } ``` # Delete Group Source: https://docs.konstantly.com/api-reference/groups/delete-group DELETE /groups/{groupId} Delete a group Delete an existing group. Note that this operation cannot be undone. ## Path Parameters The unique identifier of the group to delete ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ### Error Responses Returned when the group is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request DELETE \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups/123' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Optional def delete_group(api_key: str, group_id: int) -> bool: url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}" headers = { "X-API-KEY": api_key } try: response = requests.delete(url, headers=headers) if response.status_code == 404: raise ValueError(f"Group {group_id} not found") response.raise_for_status() return True except requests.exceptions.RequestException as e: print(f"Error deleting group: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: # Optional: Check if group exists first try: # Assuming get_group function from previous example group = get_group("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) print(f"Found group: {group.name}") except ValueError as e: print("Group not found, no need to delete") exit(0) # Delete the group success = delete_group("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) if success: print("Group deleted successfully") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to delete group: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async deleteGroup(groupId) { try { await this.client.delete(`/groups/${groupId}`); return true; } catch (error) { if (error.response) { if (error.response.status === 404) { throw new Error(`Group ${groupId} not found`); } throw new Error(`Failed to delete group: ${error.response.data.message || error.message}`); } throw error; } } // Helper method to safely delete a group (checks existence first) async safeDeleteGroup(groupId) { try { // Check if group exists first await this.getGroup(groupId); // If group exists, delete it await this.deleteGroup(groupId); return true; } catch (error) { if (error.message.includes('not found')) { console.log('Group already does not exist'); return false; } throw error; } } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { // Option 1: Direct deletion await groupsApi.deleteGroup(123); console.log('Group deleted successfully'); // Option 2: Safe deletion (checks existence first) const wasDeleted = await groupsApi.safeDeleteGroup(123); if (wasDeleted) { console.log('Group deleted successfully'); } else { console.log('Group did not exist'); } } catch (error) { console.error('Failed to delete group:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function deleteGroup($groupId) { $ch = curl_init(); $url = "{$this->baseUrl}/groups/{$groupId}"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Group $groupId not found"); } if ($httpCode !== 200) { throw new Exception("Failed to delete group. Status code: $httpCode"); } return true; } // Helper method to safely delete a group (checks existence first) public function safeDeleteGroup($groupId) { try { // Check if group exists first $this->getGroup($groupId); // If group exists, delete it $this->deleteGroup($groupId); return true; } catch (Exception $e) { if (strpos($e->getMessage(), 'not found') !== false) { echo "Group already does not exist\n"; return false; } throw $e; } } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); // Option 1: Direct deletion $groupsApi->deleteGroup(123); echo "Group deleted successfully\n"; // Option 2: Safe deletion (checks existence first) $wasDeleted = $groupsApi->safeDeleteGroup(123); if ($wasDeleted) { echo "Group deleted successfully\n"; } else { echo "Group did not exist\n"; } } catch (Exception $e) { echo "Failed to delete group: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); public boolean deleteGroup(int groupId) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups/" + groupId)) .header("X-API-KEY", API_KEY) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { throw new Exception("Group " + groupId + " not found"); } if (response.statusCode() != 200) { throw new Exception("Failed to delete group. Status: " + response.statusCode()); } return true; } // Helper method to safely delete a group (checks existence first) public boolean safeDeleteGroup(int groupId) { try { // Check if group exists first getGroup(groupId); // If group exists, delete it deleteGroup(groupId); return true; } catch (Exception e) { if (e.getMessage().contains("not found")) { System.out.println("Group already does not exist"); return false; } throw new RuntimeException(e); } } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { // Option 1: Direct deletion api.deleteGroup(123); System.out.println("Group deleted successfully"); // Option 2: Safe deletion (checks existence first) boolean wasDeleted = api.safeDeleteGroup(123); if (wasDeleted) { System.out.println("Group deleted successfully"); } else { System.out.println("Group did not exist"); } } catch (Exception e) { System.err.println("Failed to delete group: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { // Empty response on success } ``` ```json 404 Error theme={null} { "status": 404, "message": "Group not found" } ``` # Get Group Source: https://docs.konstantly.com/api-reference/groups/get-group GET /groups/{groupId} Get detailed information about a specific group Retrieve detailed information about a specific group by its ID. ## Path Parameters The unique identifier of the group to retrieve ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Group ID Parent group ID Name of the group Group description Number of users in the group Number of courses assigned to the group Group cover image details Image ID URL to full-size image URL to image 36x36 px URL to image 330x130 px ### Error Responses Returned when the group is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups/123' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from dataclasses import dataclass from typing import Optional, Dict, Any @dataclass class GroupImage: id: int original: str mini: str small: str @dataclass class Group: id: int name: str parent_id: Optional[int] users_count: int courses_count: int image: Optional[GroupImage] def get_group(api_key: str, group_id: int) -> Group: url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}" headers = { "X-API-KEY": api_key } try: response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"Group {group_id} not found") response.raise_for_status() data = response.json() # Parse image data if present image = None if data.get("image"): image = GroupImage( id=data["image"]["id"], original=data["image"]["original"], mini=data["image"]["mini"], small=data["image"]["small"] ) return Group( id=data["id"], name=data["name"], parent_id=data.get("parentId"), users_count=data["usersCount"], courses_count=data["coursesCount"], image=image ) except requests.exceptions.RequestException as e: print(f"Error fetching group: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: group = get_group("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) print(f"Group Details:") print(f"ID: {group.id}") print(f"Name: {group.name}") print(f"Users: {group.users_count}") print(f"Courses: {group.courses_count}") if group.parent_id: print(f"Parent Group ID: {group.parent_id}") if group.image: print(f"Image URL: {group.image.original}") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to get group: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async getGroup(groupId) { try { const response = await this.client.get(`/groups/${groupId}`); return response.data; } catch (error) { if (error.response) { if (error.response.status === 404) { throw new Error(`Group ${groupId} not found`); } throw new Error(`Failed to fetch group: ${error.response.data.message || error.message}`); } throw error; } } // Helper method to check if a group exists async groupExists(groupId) { try { await this.getGroup(groupId); return true; } catch (error) { if (error.message.includes('not found')) { return false; } throw error; } } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { // Check if group exists first (optional) const exists = await groupsApi.groupExists(123); if (!exists) { console.log('Group does not exist'); return; } // Get group details const group = await groupsApi.getGroup(123); console.log('Group Details:'); console.log(`ID: ${group.id}`); console.log(`Name: ${group.name}`); console.log(`Users: ${group.usersCount}`); console.log(`Courses: ${group.coursesCount}`); if (group.parentId) { console.log(`Parent Group ID: ${group.parentId}`); } if (group.image) { console.log(`Image URL: ${group.image.original}`); } } catch (error) { console.error('Failed to get group:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function getGroup($groupId) { $ch = curl_init(); $url = "{$this->baseUrl}/groups/{$groupId}"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Group $groupId not found"); } if ($httpCode !== 200) { throw new Exception("Failed to fetch group. Status code: $httpCode"); } return json_decode($response, true); } // Helper method to check if a group exists public function groupExists($groupId) { try { $this->getGroup($groupId); return true; } catch (Exception $e) { if (strpos($e->getMessage(), 'not found') !== false) { return false; } throw $e; } } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); // Check if group exists first (optional) if (!$groupsApi->groupExists(123)) { echo "Group does not exist\n"; exit; } // Get group details $group = $groupsApi->getGroup(123); echo "Group Details:\n"; echo "ID: {$group['id']}\n"; echo "Name: {$group['name']}\n"; echo "Users: {$group['usersCount']}\n"; echo "Courses: {$group['coursesCount']}\n"; if (isset($group['parentId'])) { echo "Parent Group ID: {$group['parentId']}\n"; } if (isset($group['image'])) { echo "Image URL: {$group['image']['original']}\n"; } } catch (Exception $e) { echo "Failed to get group: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); // Using previously defined Group and GroupImage classes public Group getGroup(int groupId) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups/" + groupId)) .header("X-API-KEY", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { throw new Exception("Group " + groupId + " not found"); } if (response.statusCode() != 200) { throw new Exception("Failed to fetch group. Status: " + response.statusCode()); } return mapper.readValue(response.body(), Group.class); } // Helper method to check if a group exists public boolean groupExists(int groupId) { try { getGroup(groupId); return true; } catch (Exception e) { if (e.getMessage().contains("not found")) { return false; } // Rethrow other exceptions throw new RuntimeException(e); } } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { // Check if group exists first (optional) if (!api.groupExists(123)) { System.out.println("Group does not exist"); return; } // Get group details Group group = api.getGroup(123); System.out.println("Group Details:"); System.out.println("ID: " + group.id); System.out.println("Name: " + group.name); System.out.println("Users: " + group.usersCount); System.out.println("Courses: " + group.coursesCount); if (group.parentId != null) { System.out.println("Parent Group ID: " + group.parentId); } if (group.image != null) { System.out.println("Image URL: " + group.image.original); } } catch (Exception e) { System.err.println("Failed to get group: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "id": 123, "parentId": 1, "name": "Sales Team", "usersCount": 10, "coursesCount": 5, "image": { "id": 1234, "original": "http://example.com/images/original/1234.jpg", "mini": "http://example.com/images/mini/1234.jpg", "small": "http://example.com/images/small/1234.jpg" } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # List Groups Source: https://docs.konstantly.com/api-reference/groups/get-groups GET /groups Get a list of groups (20 per page) Retrieve a paginated list of all groups on your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Query Parameters Entry number to start listing from. Use this for pagination. Example: `20` to get the second page of results ## Response Array of group objects Group ID Parent group ID (optional) Name of the group Group description Number of users in the group Number of courses assigned to the group Group cover image details Image ID URL to full-size image URL to image 36x36 px URL to image 330x130 px The total number of groups ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups?offset=0' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import List, Optional from dataclasses import dataclass @dataclass class GroupImage: id: int original: str mini: str small: str @dataclass class Group: id: int name: str parent_id: Optional[int] users_count: int courses_count: int image: Optional[GroupImage] def list_groups(api_key: str, offset: int = 0) -> tuple[List[Group], int]: url = "https://YOURSITE.konstant.ly/openapi/v1/groups" headers = { "X-API-KEY": api_key } params = {"offset": offset} try: response = requests.get(url, headers=headers, params=params) response.raise_for_status() data = response.json() groups = [] for group_data in data["groups"]: image = None if group_data.get("image"): image = GroupImage(**group_data["image"]) group = Group( id=group_data["id"], name=group_data["name"], parent_id=group_data.get("parentId"), users_count=group_data["usersCount"], courses_count=group_data["coursesCount"], image=image ) groups.append(group) return groups, data["totalCount"] except requests.exceptions.RequestException as e: print(f"Error fetching groups: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: groups, total = list_groups("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") print(f"Found {total} total groups") for group in groups: print(f"\nGroup: {group.name}") print(f"Users: {group.users_count}") print(f"Courses: {group.courses_count}") if group.parent_id: print(f"Parent Group ID: {group.parent_id}") except Exception as e: print(f"Failed to list groups: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async listGroups(offset = 0) { try { const response = await this.client.get('/groups', { params: { offset } }); return { groups: response.data.groups, totalCount: response.data.totalCount }; } catch (error) { if (error.response) { console.error('Error response:', error.response.data); throw new Error(`Failed to fetch groups: ${error.response.data.message || error.message}`); } throw error; } } // Helper method to fetch all groups (handles pagination) async getAllGroups() { const groups = []; let offset = 0; let totalCount = 0; do { const response = await this.listGroups(offset); groups.push(...response.groups); totalCount = response.totalCount; offset += 20; // Page size } while (groups.length < totalCount); return groups; } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { // Get first page of groups const { groups, totalCount } = await groupsApi.listGroups(); console.log(`Found ${totalCount} total groups`); groups.forEach(group => { console.log('\nGroup Details:'); console.log(`Name: ${group.name}`); console.log(`Users: ${group.usersCount}`); console.log(`Courses: ${group.coursesCount}`); if (group.parentId) { console.log(`Parent Group ID: ${group.parentId}`); } }); // Optional: Get all groups console.log('\nFetching all groups...'); const allGroups = await groupsApi.getAllGroups(); console.log(`Retrieved all ${allGroups.length} groups`); } catch (error) { console.error('Failed to list groups:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function listGroups($offset = 0) { $ch = curl_init(); $url = "{$this->baseUrl}/groups?offset={$offset}"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode !== 200) { throw new Exception("Failed to fetch groups. Status code: $httpCode"); } return json_decode($response, true); } // Helper method to get all groups public function getAllGroups() { $allGroups = []; $offset = 0; do { $response = $this->listGroups($offset); $allGroups = array_merge($allGroups, $response['groups']); $offset += 20; // Page size } while (count($allGroups) < $response['totalCount']); return $allGroups; } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); // Get first page of groups $result = $groupsApi->listGroups(); echo "Found {$result['totalCount']} total groups\n"; foreach ($result['groups'] as $group) { echo "\nGroup Details:\n"; echo "Name: {$group['name']}\n"; echo "Users: {$group['usersCount']}\n"; echo "Courses: {$group['coursesCount']}\n"; if (isset($group['parentId'])) { echo "Parent Group ID: {$group['parentId']}\n"; } } // Optional: Get all groups echo "\nFetching all groups...\n"; $allGroups = $groupsApi->getAllGroups(); echo "Retrieved all " . count($allGroups) . " groups\n"; } catch (Exception $e) { echo "Failed to list groups: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); // Data classes public static class GroupImage { @JsonProperty("id") public int id; @JsonProperty("original") public String original; @JsonProperty("mini") public String mini; @JsonProperty("small") public String small; } public static class Group { @JsonProperty("id") public int id; @JsonProperty("parentId") public Integer parentId; @JsonProperty("name") public String name; @JsonProperty("usersCount") public int usersCount; @JsonProperty("coursesCount") public int coursesCount; @JsonProperty("image") public GroupImage image; } public static class GroupsResponse { @JsonProperty("groups") public List groups; @JsonProperty("totalCount") public int totalCount; } public GroupsResponse listGroups(int offset) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups?offset=" + offset)) .header("X-API-KEY", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new Exception("Failed to fetch groups. Status: " + response.statusCode()); } return mapper.readValue(response.body(), GroupsResponse.class); } // Helper method to get all groups public List getAllGroups() throws Exception { List allGroups = new ArrayList<>(); int offset = 0; GroupsResponse response; do { response = listGroups(offset); allGroups.addAll(response.groups); offset += 20; // Page size } while (allGroups.size() < response.totalCount); return allGroups; } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { // Get first page of groups GroupsResponse response = api.listGroups(0); System.out.println("Found " + response.totalCount + " total groups"); for (Group group : response.groups) { System.out.println("\nGroup Details:"); System.out.println("Name: " + group.name); System.out.println("Users: " + group.usersCount); System.out.println("Courses: " + group.coursesCount); if (group.parentId != null) { System.out.println("Parent Group ID: " + group.parentId); } } // Optional: Get all groups System.out.println("\nFetching all groups..."); List allGroups = api.getAllGroups(); System.out.println("Retrieved all " + allGroups.size() + " groups"); } catch (Exception e) { System.err.println("Failed to list groups: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "groups": [ { "id": 1, "parentId": null, "name": "Sales Team", "usersCount": 10, "coursesCount": 5, "image": { "id": 1234, "original": "http://example.com/images/original/1234.jpg", "mini": "http://example.com/images/mini/1234.jpg", "small": "http://example.com/images/small/1234.jpg" } } ], "totalCount": 1 } ``` # List Group Assignments Source: https://docs.konstantly.com/api-reference/groups/group-assignments GET /groups/{groupId}/assignments Get a list of the courses assigned to the group Retrieve a list of all courses that are assigned to a specific group. ## Path Parameters The unique identifier of the group ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Array of assignment objects Timestamp when the course was assigned to the group Exact timestamp when the assignment expires The group member has to complete the course within this period Name of the time unit (hours, days, weeks, months) Number of time units Course information Course ID Course name Course annotation Whether the course is a draft Creation timestamp Last update timestamp Publication timestamp Total number of assignments ### Error Responses Returned when the group is not found HTTP status code (404) Error message ```python Python theme={null} from typing import List, Optional, Dict from dataclasses import dataclass from datetime import datetime import requests @dataclass class DeadlinePeriod: measure: str # "hours", "days", "weeks", "months" value: int @dataclass class Course: id: int name: str annotation: str is_draft: bool created_at: datetime updated_at: datetime published_at: datetime @dataclass class Assignment: assigned_at: datetime deadline_at: Optional[datetime] deadline_period: Optional[DeadlinePeriod] course: Course def get_group_assignments(api_key: str, group_id: int) -> List[Assignment]: """ Fetch all assignments for a specific group. Args: api_key: Your API key group_id: The ID of the group to fetch assignments for Returns: List of Assignment objects containing course and deadline information Raises: ValueError: If the group is not found requests.exceptions.RequestException: For API communication errors """ url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}/assignments" headers = {"X-API-KEY": api_key} try: response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"Group {group_id} not found") response.raise_for_status() data = response.json() assignments = [] for item in data["assignments"]: # Parse deadline information deadline_at = None if "deadlineAt" in item: deadline_at = datetime.fromtimestamp(item["deadlineAt"]) deadline_period = None if "deadlinePeriod" in item: deadline_period = DeadlinePeriod( measure=item["deadlinePeriod"]["measure"], value=item["deadlinePeriod"]["value"] ) # Parse course information course = Course( id=item["course"]["id"], name=item["course"]["name"], annotation=item["course"]["annotation"], is_draft=item["course"]["isDraft"], created_at=datetime.fromtimestamp(item["course"]["createdAt"]), updated_at=datetime.fromtimestamp(item["course"]["updatedAt"]), published_at=datetime.fromtimestamp(item["course"]["publishedAt"]) ) assignment = Assignment( assigned_at=datetime.fromtimestamp(item["assignedAt"]), deadline_at=deadline_at, deadline_period=deadline_period, course=course ) assignments.append(assignment) return assignments except requests.exceptions.RequestException as e: print(f"Error fetching assignments: {str(e)}") if hasattr(e, "response") and e.response is not None: print(f"Response: {e.response.text}") raise if __name__ == "__main__": try: assignments = get_group_assignments("YOUR_API_KEY", 123) print(f"Found {len(assignments)} assignments\n") for assignment in assignments: print(f"Course: {assignment.course.name}") print(f"Status: {'Draft' if assignment.course.is_draft else 'Published'}") print(f"Assigned: {assignment.assigned_at.strftime('%Y-%m-%d %H:%M:%S')}") if assignment.deadline_at: print(f"Deadline: {assignment.deadline_at.strftime('%Y-%m-%d %H:%M:%S')}") elif assignment.deadline_period: print(f"Deadline: {assignment.deadline_period.value} {assignment.deadline_period.measure}") print(f"Description: {assignment.course.annotation}") print() except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to get assignments: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupAssignments { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } /** * Fetch all assignments for a specific group. * @param {number} groupId - The ID of the group to fetch assignments for * @returns {Promise} The assignments data * @throws {Error} If the API request fails */ async getAssignments(groupId) { try { const response = await this.client.get(`/groups/${groupId}/assignments`); return this.formatAssignments(response.data); } catch (error) { if (error.response?.status === 404) { throw new Error(`Group ${groupId} not found`); } throw error; } } /** * Format timestamps and organize assignment data. * @private */ formatAssignments(data) { return { assignments: data.assignments.map(assignment => ({ course: { id: assignment.course.id, name: assignment.course.name, annotation: assignment.course.annotation, isDraft: assignment.course.isDraft, createdAt: new Date(assignment.course.createdAt * 1000), updatedAt: new Date(assignment.course.updatedAt * 1000), publishedAt: new Date(assignment.course.publishedAt * 1000) }, assignedAt: new Date(assignment.assignedAt * 1000), deadlineAt: assignment.deadlineAt ? new Date(assignment.deadlineAt * 1000) : null, deadlinePeriod: assignment.deadlinePeriod || null })), totalCount: data.totalCount }; } /** * Format deadline information for display. * @private */ formatDeadline(assignment) { if (assignment.deadlineAt) { return assignment.deadlineAt.toLocaleString(); } if (assignment.deadlinePeriod) { return `${assignment.deadlinePeriod.value} ${assignment.deadlinePeriod.measure}`; } return 'No deadline'; } } // Example usage async function main() { const api = new GroupAssignments('YOUR_API_KEY'); try { const { assignments, totalCount } = await api.getAssignments(123); console.log(`Found ${totalCount} total assignments\n`); assignments.forEach(assignment => { console.log(`Course: ${assignment.course.name}`); console.log(`Status: ${assignment.course.isDraft ? 'Draft' : 'Published'}`); console.log(`Assigned: ${assignment.assignedAt.toLocaleString()}`); console.log(`Deadline: ${api.formatDeadline(assignment)}`); console.log(`Description: ${assignment.course.annotation}`); console.log(); }); } catch (error) { console.error('Failed to fetch assignments:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } /** * Fetch all assignments for a specific group. * * @param int $groupId The ID of the group to fetch assignments for * @return array The assignments data * @throws Exception If the API request fails */ public function getAssignments(int $groupId): array { $ch = curl_init(); $url = "{$this->baseUrl}/groups/{$groupId}/assignments"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('API request failed: ' . curl_error($ch)); } curl_close($ch); if ($statusCode === 404) { throw new Exception("Group {$groupId} not found"); } if ($statusCode !== 200) { throw new Exception("API request failed with status: {$statusCode}"); } $data = json_decode($response, true); return $this->formatAssignments($data); } /** * Format timestamps and organize assignment data. * @private */ private function formatAssignments(array $data): array { return array_map(function($assignment) { return [ 'course' => [ 'id' => $assignment['course']['id'], 'name' => $assignment['course']['name'], 'annotation' => $assignment['course']['annotation'], 'isDraft' => $assignment['course']['isDraft'], 'createdAt' => date('Y-m-d H:i:s', $assignment['course']['createdAt']), 'updatedAt' => date('Y-m-d H:i:s', $assignment['course']['updatedAt']), 'publishedAt' => date('Y-m-d H:i:s', $assignment['course']['publishedAt']) ], 'assignedAt' => date('Y-m-d H:i:s', $assignment['assignedAt']), 'deadlineAt' => isset($assignment['deadlineAt']) ? date('Y-m-d H:i:s', $assignment['deadlineAt']) : null, 'deadlinePeriod' => $assignment['deadlinePeriod'] ?? null ]; }, $data['assignments']); } /** * Format deadline information for display. * @private */ private function formatDeadline(array $assignment): string { if (isset($assignment['deadlineAt'])) { return $assignment['deadlineAt']; } if (isset($assignment['deadlinePeriod'])) { return "{$assignment['deadlinePeriod']['value']} " . "{$assignment['deadlinePeriod']['measure']}"; } return 'No deadline'; } } // Example usage try { $api = new GroupAssignments('YOUR_API_KEY'); $assignments = $api->getAssignments(123); echo "Found " . count($assignments) . " assignments\n\n"; foreach ($assignments as $assignment) { echo "Course: {$assignment['course']['name']}\n"; echo "Status: " . ($assignment['course']['isDraft'] ? 'Draft' : 'Published') . "\n"; echo "Assigned: {$assignment['assignedAt']}\n"; echo "Deadline: " . $api->formatDeadline($assignment) . "\n"; echo "Description: {$assignment['course']['annotation']}\n\n"; } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ````java Java theme={null} import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.List; public class GroupAssignments { private final String apiKey; private final String baseUrl; private final HttpClient client; private final ObjectMapper mapper; private final DateTimeFormatter formatter; // Data Classes public static class Course { @JsonProperty("id") public int id; @JsonProperty("name") ```java @JsonProperty("name") public String name; @JsonProperty("annotation") public String annotation; @JsonProperty("isDraft") public boolean isDraft; @JsonProperty("createdAt") public long createdAt; @JsonProperty("updatedAt") public long updatedAt; @JsonProperty("publishedAt") public long publishedAt; public LocalDateTime getCreatedDateTime() { return LocalDateTime.ofInstant( Instant.ofEpochSecond(createdAt), ZoneId.systemDefault() ); } public LocalDateTime getUpdatedDateTime() { return LocalDateTime.ofInstant( Instant.ofEpochSecond(updatedAt), ZoneId.systemDefault() ); } public LocalDateTime getPublishedDateTime() { return LocalDateTime.ofInstant( Instant.ofEpochSecond(publishedAt), ZoneId.systemDefault() ); } } public static class DeadlinePeriod { @JsonProperty("measure") public String measure; // hours, days, weeks, months @JsonProperty("value") public int value; @Override public String toString() { return value + " " + measure; } } public static class Assignment { @JsonProperty("assignedAt") public long assignedAt; @JsonProperty("deadlineAt") public Long deadlineAt; @JsonProperty("deadlinePeriod") public DeadlinePeriod deadlinePeriod; @JsonProperty("course") public Course course; public LocalDateTime getAssignedDateTime() { return LocalDateTime.ofInstant( Instant.ofEpochSecond(assignedAt), ZoneId.systemDefault() ); } public LocalDateTime getDeadlineDateTime() { return deadlineAt != null ? LocalDateTime.ofInstant( Instant.ofEpochSecond(deadlineAt), ZoneId.systemDefault() ) : null; } public String getFormattedDeadline() { if (deadlineAt != null) { return getDeadlineDateTime().format( DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") ); } else if (deadlinePeriod != null) { return deadlinePeriod.toString(); } return "No deadline"; } } public static class AssignmentResponse { @JsonProperty("assignments") public List assignments; @JsonProperty("totalCount") public int totalCount; } public GroupAssignments(String apiKey) { this.apiKey = apiKey; this.baseUrl = "https://YOURSITE.konstant.ly/openapi/v1"; this.client = HttpClient.newHttpClient(); this.mapper = new ObjectMapper(); this.formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); } /** * Fetch all assignments for a specific group. * * @param groupId The ID of the group to fetch assignments for * @return AssignmentResponse containing assignments and total count * @throws Exception if the API request fails */ public AssignmentResponse getAssignments(int groupId) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/groups/" + groupId + "/assignments")) .header("X-API-KEY", apiKey) .GET() .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); if (response.statusCode() == 404) { throw new Exception("Group " + groupId + " not found"); } if (response.statusCode() != 200) { throw new Exception( "API request failed with status: " + response.statusCode() ); } return mapper.readValue(response.body(), AssignmentResponse.class); } public static void main(String[] args) { GroupAssignments api = new GroupAssignments("YOUR_API_KEY"); try { AssignmentResponse response = api.getAssignments(123); System.out.println("Found " + response.totalCount + " total assignments\n"); for (Assignment assignment : response.assignments) { System.out.println("Course: " + assignment.course.name); System.out.println("Status: " + (assignment.course.isDraft ? "Draft" : "Published")); System.out.println("Assigned: " + assignment.getAssignedDateTime().format(api.formatter)); System.out.println("Deadline: " + assignment.getFormattedDeadline()); System.out.println("Description: " + assignment.course.annotation); System.out.println(); } } catch (Exception e) { System.err.println("Error fetching assignments: " + e.getMessage()); e.printStackTrace(); } } } ```` ```json Response theme={null} { "assignments": [ { "assignedAt": 1443183322, "deadlineAt": 1444188932, "course": { "id": 123, "name": "Sales Strategy Fundamentals", "annotation": "Learn essential sales strategies and techniques", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229 } }, { "assignedAt": 1443183400, "deadlinePeriod": { "measure": "months", "value": 2 }, "course": { "id": 124, "name": "Customer Relationship Management", "annotation": "Master modern CRM principles and practices", "isDraft": false, "createdAt": 1507807800, "updatedAt": 1507808400, "publishedAt": 1508225300 } } ], "totalCount": 2 } ``` ```json 404 Error theme={null} { "status": 404, "message": "Group not found" } ``` # Group Statistics Source: https://docs.konstantly.com/api-reference/groups/group-statistics GET /groups/{groupId}/statistics Get statistics for a group Retrieve detailed statistics and analytics information for a specific group. ## Path Parameters The unique identifier of the group to get statistics for ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Basic group information Group ID Group name Parent group ID if applicable Number of users in group Number of courses assigned to group Group image information Custom group attributes List of users with no activity in the group User identifier Full name Email address User role List of users who haven't completed any assessments User identifier Full name Email address User role Top performing users in the group Number of completed courses Average result (0-100) User information ### Error Responses Returned when the group is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups/123/statistics' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Dict def get_group_statistics(api_key: str, group_id: int) -> Dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}/statistics" headers = { "X-API-KEY": api_key } try: response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"Group {group_id} not found") response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error getting group statistics: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: stats = get_group_statistics("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) print(f"Group: {stats['group']['name']}") print(f"Total users: {stats['group']['usersCount']}") print(f"Users without activity: {len(stats['usersWithoutActivity'])}") print(f"Top performers: {len(stats['leaders'])}") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to get group statistics: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async getGroupStatistics(groupId) { try { const response = await this.client.get(`/groups/${groupId}/statistics`); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`Group ${groupId} not found`); } throw error; } } // Helper method to get summary of statistics async getGroupStatisticsSummary(groupId) { const stats = await this.getGroupStatistics(groupId); return { groupName: stats.group.name, totalUsers: stats.group.usersCount, inactiveUsers: stats.usersWithoutActivity.length, topPerformers: stats.leaders.length }; } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { // Get full statistics const stats = await groupsApi.getGroupStatistics(123); console.log('Group:', stats.group.name); console.log('Total users:', stats.group.usersCount); // Or get summary const summary = await groupsApi.getGroupStatisticsSummary(123); console.log('Statistics summary:', summary); } catch (error) { console.error('Failed to get group statistics:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function getGroupStatistics($groupId) { $ch = curl_init(); $url = "{$this->baseUrl}/groups/{$groupId}/statistics"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Group $groupId not found"); } if ($httpCode !== 200) { throw new Exception("Failed to get statistics. Status code: $httpCode"); } return json_decode($response, true); } // Helper method to get summary of statistics public function getGroupStatisticsSummary($groupId) { $stats = $this->getGroupStatistics($groupId); return [ 'groupName' => $stats['group']['name'], 'totalUsers' => $stats['group']['usersCount'], 'inactiveUsers' => count($stats['usersWithoutActivity']), 'topPerformers' => count($stats['leaders']) ]; } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); // Get full statistics $stats = $groupsApi->getGroupStatistics(123); echo "Group: {$stats['group']['name']}\n"; echo "Total users: {$stats['group']['usersCount']}\n"; // Or get summary $summary = $groupsApi->getGroupStatisticsSummary(123); echo "Statistics summary:\n"; print_r($summary); } catch (Exception $e) { echo "Failed to get group statistics: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Map; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); public Map getGroupStatistics(int groupId) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups/" + groupId + "/statistics")) .header("X-API-KEY", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { throw new Exception("Group " + groupId + " not found"); } if (response.statusCode() != 200) { throw new Exception("Failed to get statistics. Status: " + response.statusCode()); } return mapper.readValue(response.body(), Map.class); } // Helper method to get summary of statistics public Map getGroupStatisticsSummary(int groupId) throws Exception { Map stats = getGroupStatistics(groupId); Map group = (Map) stats.get("group"); List inactive = (List) stats.get("usersWithoutActivity"); List leaders = (List) stats.get("leaders"); Map summary = new HashMap<>(); summary.put("groupName", group.get("name")); summary.put("totalUsers", group.get("usersCount")); summary.put("inactiveUsers", inactive.size()); summary.put("topPerformers", leaders.size()); return summary; } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { // Get full statistics Map stats = api.getGroupStatistics(123); Map group = (Map) stats.get("group"); System.out.println("Group: " + group.get("name")); System.out.println("Total users: " + group.get("usersCount")); // Or get summary Map summary = api.getGroupStatisticsSummary(123); System.out.println("Statistics summary: " + summary); } catch (Exception e) { System.err.println("Failed to get group statistics: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "group": { "id": 123, "name": "Marketing Team", "parentId": null, "usersCount": 25, "coursesCount": 10, "image": null, "attributes": {} }, "usersWithoutActivity": [ { "id": "user1", "name": "John Smith", "email": "john@example.com", "role": { "alias": "learner" } } ], "usersWithoutResults": [ { "id": "user2", "name": "Jane Doe", "email": "jane@example.com", "role": { "alias": "learner" } } ], "leaders": [ { "coursesCount": 8, "resultValue": 95, "user": { "id": "user3", "name": "Mike Johnson", "email": "mike@example.com", "role": { "alias": "learner" } } } ] } ``` ```json 404 Error theme={null} { "status": 404, "message": "Group not found" } ``` # Add Users to Group Source: https://docs.konstantly.com/api-reference/groups/group-users POST /groups/{groupId}/users Add users to a group Add one or more users to a specified group. ## Path Parameters The unique identifier of the group ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Array of user IDs to add to the group ### Error Responses Returned when the group is not found HTTP status code (404) Error message Returned when there's a validation error (e.g., users already in group) HTTP status code (400) Error message Validation errors by field ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups/123/users' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "users": ["user1", "user2"] }' ``` ```python Python theme={null} import requests from typing import List def add_users_to_group(api_key: str, group_id: int, user_ids: List[str]): url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}/users" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } data = { "users": user_ids } try: response = requests.post(url, headers=headers, json=data) if response.status_code == 404: raise ValueError(f"Group {group_id} not found") if response.status_code == 400: error_data = response.json() raise ValueError(f"Validation error: {error_data.get('message', '')}") response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error adding users to group: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: # Add multiple users to group result = add_users_to_group( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123, ["user1", "user2"] ) print(f"Added {len(result['users'])} users successfully") # Optional: Verify additions users = get_group_users("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) print(f"Group now has {users['totalCount']} total users") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to add users to group: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } }); } async addUsersToGroup(groupId, userIds) { try { const response = await this.client.post(`/groups/${groupId}/users`, { users: userIds }); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`Group ${groupId} not found`); } if (error.response?.status === 400) { const errorData = error.response.data; throw new Error(`Validation error: ${errorData.message || 'Invalid request'}`); } throw error; } } // Helper method to safely add users and verify async safeAddUsersToGroup(groupId, userIds) { const result = await this.addUsersToGroup(groupId, userIds); // Verify the additions const updatedGroup = await this.getGroupUsers(groupId); const addedUsers = result.users.map(u => u.id); const currentUsers = updatedGroup.users.map(u => u.id); const allAdded = addedUsers.every(id => currentUsers.includes(id)); return { success: allAdded, addedCount: result.users.length, totalUsers: updatedGroup.totalCount }; } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { // Option 1: Simple add const result = await groupsApi.addUsersToGroup(123, ['user1', 'user2']); console.log(`Added ${result.users.length} users successfully`); // Option 2: Safe add with verification const safeResult = await groupsApi.safeAddUsersToGroup(123, ['user3', 'user4']); console.log(`Added ${safeResult.addedCount} users, group now has ${safeResult.totalUsers} users`); } catch (error) { console.error('Failed to add users to group:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function addUsersToGroup($groupId, array $userIds) { $ch = curl_init(); $url = "{$this->baseUrl}/groups/{$groupId}/users"; $data = json_encode([ 'users' => $userIds ]); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey, 'Content-Type: application/json', 'Content-Length: ' . strlen($data) ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Group $groupId not found"); } if ($httpCode === 400) { $errorData = json_decode($response, true); throw new Exception("Validation error: " . ($errorData['message'] ?? 'Invalid request')); } if ($httpCode !== 200) { throw new Exception("Failed to add users. Status code: $httpCode"); } return json_decode($response, true); } // Helper method to safely add users and verify public function safeAddUsersToGroup($groupId, array $userIds) { $result = $this->addUsersToGroup($groupId, $userIds); // Verify the additions $updatedGroup = $this->getGroupUsers($groupId); $addedUsers = array_map(function($user) { return $user['id']; }, $result['users']); $currentUsers = array_map(function($user) { return $user['id']; }, $updatedGroup['users']); $allAdded = empty(array_diff($addedUsers, $currentUsers)); return [ 'success' => $allAdded, 'addedCount' => count($result['users']), 'totalUsers' => $updatedGroup['totalCount'] ]; } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); // Option 1: Simple add $result = $groupsApi->addUsersToGroup(123, ['user1', 'user2']); echo "Added " . count($result['users']) . " users successfully\n"; // Option 2: Safe add with verification $safeResult = $groupsApi->safeAddUsersToGroup(123, ['user3', 'user4']); echo "Added {$safeResult['addedCount']} users, group now has {$safeResult['totalUsers']} users\n"; } catch (Exception $e) { echo "Failed to add users to group: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.*; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); public Map addUsersToGroup(int groupId, List userIds) throws Exception { Map requestBody = new HashMap<>(); requestBody.put("users", userIds); String jsonBody = mapper.writeValueAsString(requestBody); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups/" + groupId + "/users")) .header("X-API-KEY", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { throw new Exception("Group " + groupId + " not found"); } if (response.statusCode() == 400) { Map errorData = mapper.readValue(response.body(), Map.class); throw new Exception("Validation error: " + errorData.getOrDefault("message", "Invalid request")); } if (response.statusCode() != 200) { throw new Exception("Failed to add users. Status: " + response.statusCode()); } return mapper.readValue(response.body(), Map.class); } // Helper method to safely add users and verify public Map safeAddUsersToGroup(int groupId, List userIds) throws Exception { Map result = addUsersToGroup(groupId, userIds); // Verify the additions Map updatedGroup = getGroupUsers(groupId, 0); List> addedUsers = (List>) result.get("users"); List> currentUsers = (List>) updatedGroup.get("users"); Set addedIds = addedUsers.stream() .map(u -> (String)u.get("id")) .collect(Collectors.toSet()); Set currentIds = currentUsers.stream() .map(u -> (String)u.get("id")) .collect(Collectors.toSet()); boolean allAdded = addedIds.stream().allMatch(currentIds::contains); Map response = new HashMap<>(); response.put("success", allAdded); response.put("addedCount", addedUsers.size()); response.put("totalUsers", updatedGroup.get("totalCount")); return response; } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { // Option 1: Simple add List usersToAdd = Arrays.asList("user1", "user2"); Map result = api.addUsersToGroup(123, usersToAdd); List> addedUsers = (List>) result.get("users"); System.out.println("Added " + addedUsers.size() + " users successfully"); // Option 2: Safe add with verification List moreUsers = Arrays.asList("user3", "user4"); Map safeResult = api.safeAddUsersToGroup(123, moreUsers); System.out.println(String.format("Added %d users, group now has %d users", safeResult.get("addedCount"), safeResult.get("totalUsers"))); } catch (Exception e) { System.err.println("Failed to add users to group: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "users": [ { "id": "user1", "name": "John Smith", "email": "john@example.com", "role": { "alias": "learner" } } ] } ``` ```json 404 Error theme={null} { "status": 404, "message": "Group not found" } ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation error", "errors": { "users": [ "You cannot add users already consisting in the group: user1", "Unknown users: user2" ] } } ``` # Remove User from Group Source: https://docs.konstantly.com/api-reference/groups/remove-user-from-group DELETE /groups/{groupId}/users/{userId} Remove a user from a group Remove a specific user from a group. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters The unique identifier of the group User API ID to remove from the group ## Response Returns an empty response on success. ### Error Responses Returned when group or user is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request DELETE \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups/123/users/1234567890' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def remove_user_from_group(api_key: str, group_id: int, user_id: str) -> bool: url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}/users/{user_id}" headers = {"X-API-KEY": api_key} response = requests.delete(url, headers=headers) if response.status_code == 404: raise ValueError("Group or user not found") response.raise_for_status() return True # Example usage try: if remove_user_from_group("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123, "1234567890"): print("User removed from group successfully") except Exception as e: print(f"Failed to remove user from group: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function removeUserFromGroup(apiKey, groupId, userId) { try { await axios.delete( `https://YOURSITE.konstant.ly/openapi/v1/groups/${groupId}/users/${userId}`, { headers: { 'X-API-KEY': apiKey } } ); return true; } catch (error) { if (error.response?.status === 404) { throw new Error('Group or user not found'); } throw error; } } // Example usage removeUserFromGroup('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', 123, '1234567890') .then(() => console.log('User removed from group successfully')) .catch(error => console.error('Failed:', error.message)); ``` ```php PHP theme={null} $url, CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Group or user not found"); } return $httpCode === 200; } // Example usage try { if (removeUserFromGroup('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', 123, '1234567890')) { echo "User removed from group successfully\n"; } } catch (Exception $e) { echo "Failed to remove user from group: " . $e->getMessage() . "\n"; } ?> ``` ```json 200 Response theme={null} {} ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Update Group Source: https://docs.konstantly.com/api-reference/groups/update-group PATCH /groups/{groupId} Update the group information Update information for an existing group. ## Path Parameters The unique identifier of the group to update ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Group name Parent group ID (set to `null` to remove from parent) Group description Custom group attributes mapped by attribute name ### Error Responses Returned when the request is invalid HTTP status code (400) Error message Validation errors by field Returned when the group is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request PATCH \ --url 'https://YOURSITE.konstant.ly/openapi/v1/groups/123' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "name": "Updated Sales Team", "description": "Updated description", "attributes": { "region": "North", "department": "Sales" } }' ``` ```python Python theme={null} import requests from dataclasses import dataclass from typing import Optional, Dict, Any @dataclass class GroupUpdate: name: str parent_id: Optional[int] = None description: Optional[str] = None attributes: Optional[Dict[str, Any]] = None def to_dict(self) -> Dict[str, Any]: data = { "name": self.name } if self.parent_id is not None: data["parentId"] = self.parent_id if self.description is not None: data["description"] = self.description if self.attributes is not None: data["attributes"] = self.attributes return data def update_group(api_key: str, group_id: int, update: GroupUpdate) -> Dict[str, Any]: url = f"https://YOURSITE.konstant.ly/openapi/v1/groups/{group_id}" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } try: response = requests.patch(url, headers=headers, json=update.to_dict()) if response.status_code == 404: raise ValueError(f"Group {group_id} not found") if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error updating group: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: update = GroupUpdate( name="Updated Sales Team", description="Updated description", attributes={ "region": "North", "department": "Sales" } ) group = update_group("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123, update) print("Group updated successfully!") print(f"ID: {group['id']}") print(f"Name: {group['name']}") print(f"Description: {group.get('description', 'N/A')}") print("Attributes:") for key, value in group.get('attributes', {}).items(): print(f" {key}: {value}") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to update group: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class GroupsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } }); } async updateGroup(groupId, { name, parentId, description, attributes }) { try { // Build update payload with only defined values const updateData = { name }; if (parentId !== undefined) updateData.parentId = parentId; if (description !== undefined) updateData.description = description; if (attributes !== undefined) updateData.attributes = attributes; const response = await this.client.patch(`/groups/${groupId}`, updateData); return response.data; } catch (error) { if (error.response) { if (error.response.status === 404) { throw new Error(`Group ${groupId} not found`); } if (error.response.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } throw new Error(`Failed to update group: ${error.response.data.message || error.message}`); } throw error; } } } // Example usage async function main() { const groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const group = await groupsApi.updateGroup(123, { name: 'Updated Sales Team', description: 'Updated description', attributes: { region: 'North', department: 'Sales' } }); console.log('Group updated successfully!'); console.log(`ID: ${group.id}`); console.log(`Name: ${group.name}`); console.log(`Description: ${group.description || 'N/A'}`); console.log('Attributes:'); if (group.attributes) { Object.entries(group.attributes).forEach(([key, value]) => { console.log(` ${key}: ${value}`); }); } } catch (error) { console.error('Failed to update group:', error.message); } } main(); ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function updateGroup($groupId, $updateData) { $ch = curl_init(); $url = "{$this->baseUrl}/groups/{$groupId}"; $jsonData = json_encode($updateData); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_POSTFIELDS => $jsonData, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey, 'Content-Type: application/json' ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); $responseData = json_decode($response, true); if ($httpCode === 404) { throw new Exception("Group $groupId not found"); } if ($httpCode === 400) { echo "Validation errors:\n"; if (isset($responseData['errors'])) { foreach ($responseData['errors'] as $field => $errors) { echo "$field: " . implode(', ', $errors) . "\n"; } } throw new Exception($responseData['message'] ?? 'Invalid request'); } if ($httpCode !== 200) { throw new Exception("Failed to update group. Status code: $httpCode"); } return $responseData; } } // Example usage try { $groupsApi = new GroupsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); $updateData = [ 'name' => 'Updated Sales Team', 'description' => 'Updated description', 'attributes' => [ 'region' => 'North', 'department' => 'Sales' ] ]; $group = $groupsApi->updateGroup(123, $updateData); echo "Group updated successfully!\n"; echo "ID: {$group['id']}\n"; echo "Name: {$group['name']}\n"; echo "Description: " . ($group['description'] ?? 'N/A') . "\n"; echo "Attributes:\n"; if (isset($group['attributes'])) { foreach ($group['attributes'] as $key => $value) { echo " $key: $value\n"; } } } catch (Exception $e) { echo "Failed to update group: " . $e->getMessage() . "\n"; } ?> ``` ```java Java theme={null} import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Map; public class GroupsAPI { private static final String API_KEY = "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"; private static final String BASE_URL = "https://YOURSITE.konstant.ly/openapi/v1"; private final HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); // Update Request class public static class GroupUpdate { @JsonProperty("name") public String name; @JsonProperty("parentId") public Integer parentId; @JsonProperty("description") public String description; @JsonProperty("attributes") public Map attributes; public GroupUpdate(String name) { this.name = name; } public GroupUpdate setParentId(Integer parentId) { this.parentId = parentId; return this; } public GroupUpdate setDescription(String description) { this.description = description; return this; } public GroupUpdate setAttributes(Map attributes) { this.attributes = attributes; return this; } } public Group updateGroup(int groupId, GroupUpdate update) throws Exception { String jsonBody = mapper.writeValueAsString(update); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/groups/" + groupId)) .header("X-API-KEY", API_KEY) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonBody)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 404) { throw new Exception("Group " + groupId + " not found"); } if (response.statusCode() == 400) { Map errorResponse = mapper.readValue(response.body(), Map.class); System.out.println("Validation errors:"); if (errorResponse.containsKey("errors")) { Map errors = (Map) errorResponse.get("errors"); errors.forEach((field, messages) -> { System.out.println(field + ": " + messages); }); } throw new Exception(errorResponse.getOrDefault("message", "Invalid request").toString()); } if (response.statusCode() != 200) { throw new Exception("Failed to update group. Status: " + response.statusCode()); } return mapper.readValue(response.body(), Group.class); } public static void main(String[] args) { GroupsAPI api = new GroupsAPI(); try { Map attributes = new HashMap<>(); attributes.put("region", "North"); attributes.put("department", "Sales"); GroupUpdate update = new GroupUpdate("Updated Sales Team") .setDescription("Updated description") .setAttributes(attributes); Group group = api.updateGroup(123, update); System.out.println("Group updated successfully!"); System.out.println("ID: " + group.id); System.out.println("Name: " + group.name); System.out.println("Description: " + (group.description != null ? group.description : "N/A")); System.out.println("Attributes:"); if (group.attributes != null) { group.attributes.forEach((key, value) -> System.out.println(" " + key + ": " + value)); } } catch (Exception e) { System.err.println("Failed to update group: " + e.getMessage()); e.printStackTrace(); } } } ``` ```json Response theme={null} { "id": 123, "parentId": 1, "name": "Updated Sales Team", "usersCount": 10, "coursesCount": 5, "description": "Updated description", "attributes": { "region": "North", "department": "Sales" }, "image": { "id": 1234, "original": "http://example.com/images/original/1234.jpg", "mini": "http://example.com/images/mini/1234.jpg", "small": "http://example.com/images/small/1234.jpg" } } ``` ```json 400 Error - Validation Failed theme={null} { "status": 400, "message": "Validation failed", "errors": { "name": ["Name cannot be empty"], "attributes.region": ["Invalid region specified"] } } ``` ```json 404 Error - Not Found theme={null} { "status": 404, "message": "Group not found" } ``` # Create Invitations Source: https://docs.konstantly.com/api-reference/invitations/create-invite POST /invites Create invitations for new users Send email invitations to potential users to join your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Array of email addresses to send invitations to Custom invitation message Role to assign to users when they accept the invitation Array of group IDs to add users to upon acceptance Array of course IDs to assign to users upon acceptance Timestamp for course assignment deadlines ## Response Array of created invitation objects. Invitation ID Recipient email address Creation timestamp Signup URL for recipient ## Notes 1. Email requirements: * Must be valid email format * Cannot be already registered users * One invitation per email address 2. Role assignment: * Default role is "learner" * Role must exist in system * Custom roles must be valid 3. URL expiration: * Signup URLs are valid for 7 days * New invitations invalidate old ones ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/invites' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "emails": [ "test@example.com", "another@example.com" ], "roleAlias": "learner", "groups": [12], "courses": [110, 150], "text": "Welcome to our learning platform!" }' ``` ```python Python theme={null} import requests from typing import List, Optional, Dict, Any def create_invitations( api_key: str, emails: List[str], text: Optional[str] = None, role_alias: str = "learner", groups: Optional[List[int]] = None, courses: Optional[List[int]] = None, deadline_at: Optional[int] = None ) -> List[Dict[str, Any]]: url = "https://YOURSITE.konstant.ly/openapi/v1/invites" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } data = { "emails": emails, "roleAlias": role_alias } if text: data["text"] = text if groups: data["groups"] = groups if courses: data["courses"] = courses if deadline_at: data["deadlineAt"] = deadline_at response = requests.post(url, headers=headers, json=data) response.raise_for_status() return response.json() # Example usage try: emails = ["test@example.com", "another@example.com"] invites = create_invitations( api_key="1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", emails=emails, text="Welcome to our learning platform!", groups=[12], courses=[110, 150] ) print(f"Created {len(invites)} invitations:") for invite in invites: print(f"\nInvite for: {invite['email']}") print(f"URL: {invite['inviteUrl']}") except Exception as e: print(f"Error creating invitations: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function createInvitations( apiKey, emails, { text = null, roleAlias = 'learner', groups = null, courses = null, deadlineAt = null } = {} ) { const data = { emails, roleAlias }; if (text) data.text = text; if (groups) data.groups = groups; if (courses) data.courses = courses; if (deadlineAt) data.deadlineAt = deadlineAt; try { const response = await axios.post( 'https://YOURSITE.konstant.ly/openapi/v1/invites', data, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { console.error('Error creating invitations:', error.message); throw error; } } // Example usage const emails = ['test@example.com', 'another@example.com']; createInvitations( '1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', emails, { text: 'Welcome to our learning platform!', groups: [12], courses: [110, 150] } ) .then(invites => { console.log(`Created ${invites.length} invitations:`); invites.forEach(invite => { console.log('\nInvite details:'); console.log(`Email: ${invite.email}`); console.log(`URL: ${invite.inviteUrl}`); }); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} [ { "id": 1, "email": "test@example.com", "createdAt": 1234567890, "inviteUrl": "https://YOURSITE.konstant.ly/signup/1qaz2wsx3edc4rfv" }, { "id": 2, "email": "another@example.com", "createdAt": 1234567890, "inviteUrl": "https://YOURSITE.konstant.ly/signup/2wsx3edc4rfv5tgb" } ] ``` # Cancel Invitation Source: https://docs.konstantly.com/api-reference/invitations/delete-invite DELETE /invites/{inviteId} Cancel invitation Cancel an existing invitation, invalidating its signup URL. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters Invitation ID ## Response On success, returns HTTP 200 status code. ### Error Responses Not Found error response HTTP status code (404) Error message ## Important Notes 1. Effects of Cancellation: * The signup URL becomes immediately invalid * Cannot be undone - must create new invitation if needed * Does not affect users who have already accepted 2. Use Cases: * Correcting mistakenly sent invitations * Revoking access before acceptance * Managing invitation expiry 3. Security Considerations: * Consider canceling invitations for departed employees * Review pending invitations periodically * Track cancellations for audit purposes ```bash cURL theme={null} curl --request DELETE \ --url 'https://YOURSITE.konstant.ly/openapi/v1/invites/1234' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def delete_invitation(api_key: str, invite_id: int) -> None: url = f"https://YOURSITE.konstant.ly/openapi/v1/invites/{invite_id}" headers = {"X-API-KEY": api_key} response = requests.delete(url, headers=headers) if response.status_code == 404: raise ValueError(f"Invitation {invite_id} not found") response.raise_for_status() # Example usage try: delete_invitation("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 1234) print("Invitation cancelled successfully") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Error canceling invitation: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function deleteInvitation(apiKey, inviteId) { try { await axios.delete( `https://YOURSITE.konstant.ly/openapi/v1/invites/${inviteId}`, { headers: { 'X-API-KEY': apiKey } } ); console.log('Invitation cancelled successfully'); } catch (error) { if (error.response?.status === 404) { throw new Error(`Invitation ${inviteId} not found`); } throw error; } } // Example usage deleteInvitation('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', 1234) .then(() => { console.log('Successfully cancelled the invitation'); }) .catch(error => { console.error('Failed to cancel invitation:', error.message); }); ``` ```php PHP theme={null} $url, CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-API-KEY: {$apiKey}" ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($httpCode === 404) { throw new Exception("Invitation {$inviteId} not found"); } if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); if ($httpCode !== 200) { throw new Exception("Failed to cancel invitation. Status code: {$httpCode}"); } } // Example usage try { deleteInvitation("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 1234); echo "Invitation cancelled successfully\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Get Invitation Source: https://docs.konstantly.com/api-reference/invitations/get-invite GET /invites/{inviteId} Get invitation by ID Retrieve information about a specific invitation using its unique identifier. ## Path Parameters The unique identifier of the invitation to retrieve ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Invitation details Invitation ID Email invitation was sent to Timestamp when invitation was sent URL to signup ### Error Responses Returned when the invitation is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/invites/123' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from dataclasses import dataclass from typing import Optional from datetime import datetime @dataclass class Invite: id: int email: str created_at: int invite_url: str @property def created_date(self) -> datetime: return datetime.fromtimestamp(self.created_at) def get_invitation(api_key: str, invite_id: int) -> Invite: """ Fetch a specific invitation by ID Args: api_key: Your API key invite_id: Invitation ID to retrieve Returns: Invite object Raises: requests.exceptions.RequestException: If API request fails ValueError: If invitation not found """ url = f"https://YOURSITE.konstant.ly/openapi/v1/invites/{invite_id}" headers = { "X-API-KEY": api_key } try: response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"Invitation {invite_id} not found") response.raise_for_status() data = response.json()["invite"] return Invite( id=data["id"], email=data["email"], created_at=data["createdAt"], invite_url=data["inviteUrl"] ) except requests.exceptions.RequestException as e: print(f"Error fetching invitation: {str(e)}") raise # Example usage if __name__ == "__main__": try: invite = get_invitation("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", 123) print("Invitation Details:") print(f"ID: {invite.id}") print(f"Email: {invite.email}") print(f"Created: {invite.created_date}") print(f"URL: {invite.invite_url}") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Failed to fetch invitation: {str(e)}") ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } /** * Get invitation by ID * * @param integer $inviteId Invitation ID to retrieve * @return array Invitation details * @throws Exception If the API request fails */ public function getInvitation(int $inviteId): array { $ch = curl_init(); $url = "{$this->baseUrl}/invites/{$inviteId}"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode === 404) { throw new Exception("Invitation {$inviteId} not found"); } if ($httpCode !== 200) { throw new Exception("Request failed with status: {$httpCode}"); } return json_decode($response, true)['invite']; } } // Example usage try { $api = new InvitationsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); $invite = $api->getInvitation(123); echo "Invitation Details:\n"; echo "ID: {$invite['id']}\n"; echo "Email: {$invite['email']}\n"; echo "Created: " . date('Y-m-d H:i:s', $invite['createdAt']) . "\n"; echo "URL: {$invite['inviteUrl']}\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```javascript Node.js theme={null} const axios = require('axios'); class InvitationsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } /** * Get invitation by ID * @param {number} inviteId Invitation ID to retrieve * @returns {Promise} Invitation details */ async getInvitation(inviteId) { try { const response = await this.client.get(`/invites/${inviteId}`); return response.data.invite; } catch (error) { if (error.response?.status === 404) { throw new Error(`Invitation ${inviteId} not found`); } throw new Error(`Failed to fetch invitation: ${error.message}`); } } /** * Format date from timestamp * @param {number} timestamp Unix timestamp * @returns {string} Formatted date */ formatDate(timestamp) { return new Date(timestamp * 1000).toLocaleString(); } } // Example usage async function main() { const api = new InvitationsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const invite = await api.getInvitation(123); console.log('Invitation Details:'); console.log(`ID: ${invite.id}`); console.log(`Email: ${invite.email}`); console.log(`Created: ${api.formatDate(invite.createdAt)}`); console.log(`URL: ${invite.inviteUrl}`); } catch (error) { console.error('Error:', error.message); } } main(); ``` ```json Response theme={null} { "invite": { "id": 123, "email": "john.doe@example.com", "createdAt": 1234567890, "inviteUrl": "http://YOURSITE.konstant.ly/signup/1qaz2wsx3edc4rfv" } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Invitation not found" } ``` # Get Invitations Source: https://docs.konstantly.com/api-reference/invitations/get-invites GET /invites Get a list of processed invitations Retrieve a list of all email invitations that have been processed on your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response List of invitation objects Invitation ID Email invitation was sent to Timestamp when invitation was sent URL to signup ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/invites' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from dataclasses import dataclass from typing import List, Optional from datetime import datetime @dataclass class Invite: id: int email: str created_at: int invite_url: str @property def created_date(self) -> datetime: return datetime.fromtimestamp(self.created_at) def get_invitations(api_key: str) -> List[Invite]: """ Fetch all processed invitations Args: api_key: Your API key Returns: List of Invite objects Raises: requests.exceptions.RequestException: If API request fails """ url = "https://YOURSITE.konstant.ly/openapi/v1/invites" headers = { "X-API-KEY": api_key } try: response = requests.get(url, headers=headers) response.raise_for_status() data = response.json() return [ Invite( id=invite["id"], email=invite["email"], created_at=invite["createdAt"], invite_url=invite["inviteUrl"] ) for invite in data["invites"] ] except requests.exceptions.RequestException as e: print(f"Error fetching invitations: {str(e)}") raise # Example usage if __name__ == "__main__": try: invites = get_invitations("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") print(f"Found {len(invites)} invitations:\n") for invite in invites: print(f"ID: {invite.id}") print(f"Email: {invite.email}") print(f"Created: {invite.created_date}") print(f"URL: {invite.invite_url}\n") except Exception as e: print(f"Failed to fetch invitations: {str(e)}") ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } /** * Get list of processed invitations * * @return array List of invitation objects * @throws Exception If the API request fails */ public function getInvitations(): array { $ch = curl_init(); $url = "{$this->baseUrl}/invites"; curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); if ($httpCode !== 200) { throw new Exception("Request failed with status: {$httpCode}"); } return json_decode($response, true)['invites']; } } // Example usage try { $api = new InvitationsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); $invites = $api->getInvitations(); echo sprintf("Found %d invitations\n\n", count($invites)); foreach ($invites as $invite) { echo "ID: {$invite['id']}\n"; echo "Email: {$invite['email']}\n"; echo "Created: " . date('Y-m-d H:i:s', $invite['createdAt']) . "\n"; echo "URL: {$invite['inviteUrl']}\n\n"; } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```javascript Node.js theme={null} const axios = require('axios'); class InvitationsAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } /** * Get list of processed invitations * @returns {Promise} List of invitation objects */ async getInvitations() { try { const response = await this.client.get('/invites'); return response.data.invites; } catch (error) { console.error('Failed to fetch invitations:', error); throw error; } } /** * Format date from timestamp * @param {number} timestamp Unix timestamp * @returns {string} Formatted date */ formatDate(timestamp) { return new Date(timestamp * 1000).toLocaleString(); } } // Example usage async function main() { const api = new InvitationsAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const invites = await api.getInvitations(); console.log(`Found ${invites.length} invitations\n`); invites.forEach(invite => { console.log(`ID: ${invite.id}`); console.log(`Email: ${invite.email}`); console.log(`Created: ${api.formatDate(invite.createdAt)}`); console.log(`URL: ${invite.inviteUrl}\n`); }); } catch (error) { console.error('Failed to fetch invitations:', error.message); } } main(); ``` ```json Response theme={null} { "invites": [ { "id": 1, "email": "john.doe@example.com", "createdAt": 1234567890, "inviteUrl": "http://YOURSITE.konstant.ly/signup/1qaz2wsx3edc4rfv" }, { "id": 2, "email": "jane.smith@example.com", "createdAt": 1234567891, "inviteUrl": "http://YOURSITE.konstant.ly/signup/2wsx3edc4rfv5tgb" } ] } ``` # Authentication Source: https://docs.konstantly.com/api-reference/mcp/authentication How MCP API keys work, scopes, license tiers, and the security model The MCP server authenticates with your Konstantly tenant using a single bearer token — an **MCP API key** — passed in the `X-MCP-Key` HTTP header on every request. ## Key anatomy Every MCP key looks like this: ``` kmcp_a8f3b2k9X7yZqWpL3mN8RvU2sT5dH6jK4F1gA9oP ``` * **Prefix**: `kmcp_` — identifies the token type. Always present. * **Body**: 43 URL-safe base64 characters (256 bits of randomness). Generated by `crypto.randomBytes(32)` server-side. * **Total length**: 48 characters. The full key is shown **once** at creation time. Konstantly stores only the SHA-256 hash; the plaintext is unrecoverable after the dialog closes. ## Key lifecycle | Event | Where it happens | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Generate** | Settings → MCP Keys → Generate. Full key shown once. | | **Store** | In your AI tool's config file (Claude Desktop's `claude_desktop_config.json`, Cursor's `~/.cursor/mcp.json`, etc.) as an env var | | **Authenticate** | Every MCP request sends `X-MCP-Key` header. Backend verifies via constant-time hash compare. | | **Track usage** | `last_used_at` timestamp updated on every successful authentication | | **Revoke** | Settings → MCP Keys → trash icon. Takes effect within seconds; agent stops working immediately. | **Keys do not expire automatically.** Add a calendar reminder to rotate periodically. Optional key expiry is on the roadmap. ## Who the key authenticates as Each MCP key is **owned by a specific user** — the admin who created it. Every tool call executes with that user's permissions, exactly as if they had logged into the web UI and clicked the same button. **Example**: a learner-issued key cannot read admin-only audit logs, cannot assign training to others, cannot ban users — because the owning learner can't do those things in the UI either. This is the same authorization model as everywhere else in Konstantly. There is no parallel "MCP permission system" to learn or get wrong. See the [Permissions matrix](/api-reference/mcp/permissions) for which role each tool requires. ## Scopes When generating a key, you choose its scopes: | Scope | What it allows | | --------- | ------------------------------------------------------------------------------------------------------------ | | **read** | All read tools (`find_user`, `get_compliance_status`, `list_certificates`, etc.) | | **write** | All write tools (`assign_training`, `ban_user`, `clone_course`, etc.). Requires Business or Enterprise plan. | The MCP server consults the [capabilities endpoint](#capabilities-endpoint) on startup to learn which scopes your key has, then registers only the tools the agent can actually call. **Tools the agent can't invoke aren't shown to the LLM** — the model never picks a tool that's guaranteed to 403. ## License tier interaction Your tenant's license tier sets an **upper bound** on what any MCP key can do, regardless of how the key was issued: | License tier (`restrictions.mcpAccess`) | Effect | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"none"` (default) | MCP server disabled. Requests return `403` with code `MCP_LICENSE_DISABLED` and the message *"MCP is not enabled on this workspace. Talk to your administrator."* | | `"read"` | All keys treated as read-only, even keys originally issued with `write: true` | | `"full"` | Read and write tools available per key scope | If your tenant's `mcpAccess` is changed mid-life-of-a-key (e.g. clamped to `"read"` or `"none"`), write tools fail silently for that key — no re-issue required. The clamp is applied on every request. ## Capabilities endpoint When the MCP server starts up, it calls: ```http theme={null} GET /api/v2/mcp/capabilities HTTP/1.1 Host: your-instance.konstantly.com X-MCP-Key: kmcp_a8f3b2k9... ``` Response: ```json theme={null} { "tier": "full", "scopes": { "read": true, "write": true }, "user": { "id": 42, "name": "Alex Admin", "email": "alex@acme.com" }, "tools": [ "find_user", "get_user", "find_course", "get_course", "list_assignments", "list_my_assignments", "get_compliance_status", "list_certificates", "get_statistics", "get_leaderboard", "get_audit_log", "assign_training", "bulk_assign", "assign_chain", "ban_user", "unban_user", "change_user_email", "clone_course" ], "resources": [] } ``` The `tools` array reflects what the calling key can actually invoke after permission + scope + license filtering. The MCP server uses this to register tools with the LLM. Failed authentication on missing/invalid/revoked-key returns `401 Unauthorized` with no body — no information leaks. The `403 MCP_LICENSE_DISABLED` case is distinct: it's only reachable with a valid key, so it deliberately tells the caller MCP isn't enabled for the workspace instead of falling through to an opaque 401. ## Error semantics | HTTP status | Code | When | | ----------- | ----------------------- | ------------------------------------------------------------------------------------- | | `401` | `UNAUTHORIZED` | Missing, invalid, or revoked key; banned user | | `402` | `LICENSE_RESTRICTED` | Session-authed key-management endpoints when the tenant has no MCP license at all | | `403` | `MCP_LICENSE_DISABLED` | Key is valid but tenant's `mcpAccess` is `"none"` | | `403` | `MCP_AI_DISABLED` | AI tool invoked while tenant's `mcpAiAccess` is `false` | | `403` | (varies per endpoint) | Key is valid, but the owning user lacks permission for the requested action | | `429` | `MCP_RATE_LIMITED` | Per-key rate limit exceeded ([see Security](/api-reference/mcp/security#rate-limits)) | | `429` | `MCP_AUTH_RATE_LIMITED` | Per-IP rate limit on failed auth attempts (anti-enumeration) | `429` responses include a `Retry-After` value in the error message body. The MCP server automatically retries 429s with exponential backoff up to 3 times. ## Programmatic verification To verify a key works without going through an AI tool: ```bash theme={null} curl -i https://your-instance.konstantly.com/api/v2/mcp/capabilities \ -H "X-MCP-Key: kmcp_a8f3b2k9X7yZ..." ``` Expected: `200 OK` with the JSON body shown above. Common failure modes: ```bash theme={null} # Bad key HTTP/2 401 {"error":"Unauthorized","code":"UNAUTHORIZED"} # Tier is "none" HTTP/2 403 {"error":"MCP is not enabled on this workspace. Talk to your administrator.","code":"MCP_LICENSE_DISABLED"} # Too many bad-key attempts from this IP HTTP/2 429 {"error":"Too many failed MCP authentication attempts. Retry after 47s.","code":"MCP_AUTH_RATE_LIMITED"} ``` ## Multiple keys per user A single Konstantly user can have multiple MCP keys — one per device, per agent, per environment (e.g., one for your laptop's Claude Desktop, one for your team's Slack bot). Each has its own `last_used_at` and its own revoke control. There's no built-in limit on key count per user. Revoke unused keys regularly to keep the list manageable. ## Next steps All 25 tools, parameters, examples Which role can invoke which tool Rate limits, audit log, best practices Generate a key and try it # MCP Server Overview Source: https://docs.konstantly.com/api-reference/mcp/overview Expose Konstantly to Claude Desktop, Claude Code, Cursor and other AI agents via the Model Context Protocol The **Konstantly MCP server** lets external AI agents — Claude Desktop, Claude Code, Cursor, ChatGPT (when MCP support lands), and any other agent that speaks the [Model Context Protocol](https://modelcontextprotocol.io) — read your Konstantly data and take actions on your behalf, without anyone logging into the browser UI. > *"Who's overdue on the OSHA refresher?"* > > *"Assign the safety training to the warehouse crew, deadline Friday."* > > *"Show me last week's leaderboard for the Sales team."* Your agent answers from your real data. No tab-switching, no copy-paste, no custom integration code. ## How it works ``` ┌─────────────────────┐ │ Claude Desktop / │ │ Code / Cursor │ (your AI tool) └──────────┬──────────┘ │ MCP protocol over stdio ▼ ┌─────────────────────┐ │ @konstantly/ │ (installed via npx, runs on your machine) │ mcp-server │ └──────────┬──────────┘ │ HTTPS + X-MCP-Key header ▼ ┌─────────────────────┐ │ Konstantly v2 API │ (your tenant — same backend as the web UI) └─────────────────────┘ ``` Three pieces work together: 1. **Your AI tool** (Claude Desktop, etc.) handles the conversation with you 2. **`@konstantly/mcp-server`** — a small Node CLI installed via `npx`, brokers between your AI tool and Konstantly 3. **Your Konstantly instance** — authenticates the request, applies your existing permissions, returns data The MCP server is a thin adapter. **All business logic, permission checks, license gating, and audit logging happen in your Konstantly backend** — exactly like the browser UI. An MCP key acts as you; your permissions apply. ## What you can do ### Read (Business and Enterprise) `get_compliance_status` — who's done what training, who's overdue, ready for OSHA / SOC 2 / HIPAA audits `find_user`, `find_course`, `get_user`, `get_course` — semantic lookup by name/email `list_assignments`, `list_my_assignments` — per-user, per-group, by status `list_certificates` — including expiry dates for recertification planning `get_statistics`, `get_leaderboard` — engagement, completion rates, top learners `get_audit_log` — investigate who did what, when ### Write (Business and Enterprise) `assign_training`, `bulk_assign`, `assign_chain` — single learner, whole group, or full learning path `ban_user`, `unban_user`, `change_user_email` — offboarding and account hygiene `clone_course` — duplicate for a new audience, location, or compliance year ### AI course generation (Business and Enterprise) `upload_source_material` — read a local PDF / DOC / image and index it for course generation `create_course_from_prompt` / `create_course_from_files` — the agent builds the whole course with lessons and a quiz, grounded in your materials `get_course_generation_status` — generation runs in the background; the agent polls for completion and reads `courseId` ### AI course refinement (Business and Enterprise) Once a course exists, the agent can iterate on it without starting fresh. All three tools share the same daily AI quota as generation (5 per key, 50 per tenant) — each call counts as one. `refine_course` — append new lessons / tests / sections to an existing course. Append-only by design: never deletes or modifies existing content. `regenerate_question` — surgical fix for a single quiz question. Stays on the same concept; previous text preserved in the audit log. `regenerate_lesson` — refresh a lesson's AI brief without touching pages the author already created. The complete tool reference lives in the [Tools Reference](/api-reference/mcp/tools-reference) page. ## When to use MCP vs the REST API **Use the REST API** when you're building a custom integration with a known shape — a Zapier flow, a HRIS sync, a dashboard, a one-off ETL job. **Use MCP** when you want an AI agent (yours or your customers') to access Konstantly conversationally — when the prompt is "who's overdue on safety training" not "GET /api/v2/courses/123/statistics/users". The two are complementary. MCP is just a different consumption pattern over the same v2 API surface. ## Licensing | Plan | MCP access | | -------------- | ---------------------------------------------------------------------------------------- | | Free | Not available | | **Business** | All 25 tools (read, write, AI course generation + refinement). 60 calls/min + 1,000/day. | | **Enterprise** | All 25 tools. 600 calls/min + 100,000/day. | AI tools (generation + refinement) share a separate, stricter daily quota — 5 calls per key per day, 50 per tenant per day — to ensure sustainable usage. If your plan doesn't include MCP, the **MCP Keys** tab in your settings won't appear. Upgrade or contact your administrator. ## Next steps Generate a key and connect Claude Desktop in 5 minutes How keys work, scopes, license tiers All 25 tools with parameters and examples Key rotation, rate limits, audit log # Permissions Matrix Source: https://docs.konstantly.com/api-reference/mcp/permissions Which Konstantly role unlocks which MCP tool, and how license tier interacts MCP keys do not have their own permission system. They inherit **the permissions of the user who issued them**, exactly as if that user had clicked the equivalent button in the browser UI. This page maps each MCP tool to the underlying permission alias and shows which built-in roles get it. ## Quick rules 1. **Every tool call re-resolves permissions fresh** against the owning user's current role. Revoke a permission → the next MCP call respects it immediately. No cache to invalidate. 2. **License tier `restrictions.mcpAccess` is the upper bound**: * `"none"` → MCP server disabled entirely (401) * `"read"` → all write scopes clamped to false; only read tools available * `"full"` → both read and write tools available per key scope 3. **`restrictions.mcpAiAccess` is a separate, additional gate** for the AI course-generation tools. Requires `mcpAccess: "full"` AND `mcpAiAccess: true`. Default on for Business and Enterprise; can be flipped off per-tenant for abuse mitigation. 4. **The `mcp_manage` permission gates KEY CREATION/REVOCATION**, not key USAGE. A user without `mcp_manage` cannot create new keys but their existing keys keep working until revoked. ## Tool → permission map | Tool | Required permission alias | R/W | License tier needed | | ------------------------------ | ------------------------------------------------- | --- | ------------------------ | | `find_user` | `user_view` | R | read | | `get_user` | `user_view` | R | read | | `find_course` | `course_view` | R | read | | `get_course` | `course_view` | R | read | | `list_assignments` | `user_assign` | R | read | | `list_my_assignments` | any role (learner+) | R | read | | `get_compliance_status` | `common_stat` | R | read | | `list_certificates` | `user_view` | R | read | | `get_statistics` | `common_stat` | R | read | | `get_leaderboard` | `common_stat` | R | read | | `get_audit_log` | `audit_view` | R | read | | `assign_training` | `user_assign` | W | **full** | | `bulk_assign` | `group_assign` | W | **full** | | `assign_chain` | `user_assign` + chain ownership | W | **full** | | `ban_user` | `user_ban` + role hierarchy | W | **full** | | `unban_user` | `user_ban` + role hierarchy | W | **full** | | `change_user_email` | `user_edit` + role hierarchy | W | **full** | | `clone_course` | `course_edit` | W | **full** | | `upload_source_material` | `course_edit` | W | **full** + `mcpAiAccess` | | `create_course_from_prompt` | `course_edit` | W | **full** + `mcpAiAccess` | | `create_course_from_files` | `course_edit` | W | **full** + `mcpAiAccess` | | `get_course_generation_status` | `course_edit` (write-scope only) | W | **full** + `mcpAiAccess` | | `refine_course` | `course_edit` **on the specific course** | W | **full** + `mcpAiAccess` | | `regenerate_question` | `course_edit` **on the question's parent course** | W | **full** + `mcpAiAccess` | | `regenerate_lesson` | `course_edit` **on the lesson's parent course** | W | **full** + `mcpAiAccess` | ## Role → tool map (built-in roles) These are the default permissions on the stock Konstantly roles. Your tenant may have customized roles; check **Settings → Roles** for what each of yours actually grants. ### `super_user` Implicitly has all permissions. Every tool is available. ### `administrator` | Available tools | Permission | | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | All read tools | (has all read perms) | | `assign_training`, `bulk_assign`, `assign_chain` | `user_assign`, `group_assign` | | `ban_user`, `unban_user` | `user_ban` | | `change_user_email` | `user_edit` | | `clone_course` | `course_edit` | | AI course generation (`upload_source_material`, `create_course_from_prompt`, `create_course_from_files`, `get_course_generation_status`) | `course_edit` + `mcpAiAccess` license flag | | AI course refinement (`refine_course`, `regenerate_question`, `regenerate_lesson`) | `course_edit` on the specific course + `mcpAiAccess` | | Audit access | `audit_view` | | MCP key management | `mcp_manage` (seeded with the MCP feature) | In practice, an admin's MCP key is functionally equivalent to a super-user key for tool access (they have all the same aliases). ### `expert` / `instructor` (course creators) | Available tools | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `find_user`, `find_course`, `get_course`, `list_my_assignments`, `list_certificates`, `get_statistics`, `get_leaderboard` (read access) | | `clone_course` | | AI course generation tools — when the tenant has the `mcpAiAccess` license flag enabled, course-creator roles get these four tools too (`course_edit` is the only required alias) | | AI course refinement tools (`refine_course`, `regenerate_question`, `regenerate_lesson`) — same license flag, but each call additionally requires that the user can edit THIS course (per-course access check, not just the role alias) | Cannot ban users, cannot assign to others. ### `learner` | Available tools | | ----------------------------------------------------------------------------- | | `list_my_assignments`, `find_course`, `get_course`, `list_certificates` (own) | The narrowest tool set. A learner-issued key cannot read other learners' data or take admin actions. ## Permission hierarchy enforcement Three tools enforce **role-hierarchy** on top of the basic permission check: * `ban_user`, `unban_user`: caller must outrank the target user * `change_user_email`: caller must outrank the target user Hierarchy is checked exactly as in the web UI (via the same `canAssignRole` / `hasUserAccessToUser` helpers). A manager-level user cannot ban an admin; an admin cannot change a super-user's email. If the hierarchy check fails, the tool returns: > *"Permission denied — your user does not have rights for this action."* ## License tier behavior in detail ### License-clamp scenario You issued a key with `scopes = { read: true, write: true }`. The tenant's `mcpAccess` is later clamped to `"read"` (e.g. abuse mitigation, or a downgrade to a tier that doesn't include MCP). The next MCP call from that key: 1. Authenticates successfully (key still valid) 2. License tier is read at request time → `"read"` 3. Effective scopes after clamp: `{ read: true, write: false }` 4. Write tool invocations 403; read tool invocations succeed **No re-issue required.** The clamp is dynamic. If `mcpAccess` returns to `"full"`, the same key regains write capability without any action. The capabilities endpoint reflects the effective scopes at the time it's called, so the MCP server will deregister write tools from the LLM on next startup. ### Read-only-key scenario You issued a key with `scopes = { read: true, write: false }`. The key always behaves read-only — even if `mcpAccess` allows write — because **stored scopes are the floor**: 1. Authenticates successfully 2. License tier permits write 3. But the key's STORED scopes are still `{ read: true, write: false }` 4. Effective scopes: `{ read: true, write: false }` To grant write to that key, **revoke + re-issue** with `write: true`. Stored scopes are not upgraded automatically. ## Auditing who did what Every MCP write-tool call writes two entries to the audit log: 1. The **action event** (e.g., `COURSE_ASSIGN`) — same as a UI-driven action would write 2. An **`MCP_TOOL_CALL` event** (event type 88, target type `MCP_API_KEY`) with `data: {tool, tier, ...args}` This lets compliance reports filter "actions taken by an agent vs by a human admin". Query the audit log for `eventType=88` to see the MCP-driven action history. `MCP_TOOL_CALL` is emitted only on **write** tools — read tools are too high-volume to log per-call and are tracked in the AI Usage dashboard alongside other AI-driven activity. ## Next steps All 25 tools with parameters Rate limits, key hygiene, audit # Quickstart Source: https://docs.konstantly.com/api-reference/mcp/quickstart From zero to your first AI-driven Konstantly query in five minutes This guide gets you from no setup to asking Claude Desktop *"who's overdue on safety training?"* in about five minutes. **Prerequisites** * A Konstantly tenant on **Business or Enterprise plan** (the MCP Keys settings tab is hidden on Free) * Admin or super-user role on that tenant (the `mcp_manage` permission) * [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs.claude.com/en/docs/claude-code), or [Cursor](https://cursor.sh/) installed * [Node.js 18+](https://nodejs.org/) on your machine (required by `npx`) ## 1. Generate an MCP key In your Konstantly browser tab, navigate to **Settings → MCP Keys**. If the tab isn't visible, your tenant's license doesn't include MCP access. Talk to your account manager about upgrading. Click **Generate key**, give it a recognizable name (e.g. *"My laptop"*, *"Production agent"*), and choose scopes: * **Read** — always available. Queries, reports, lookups. * **Write** — Business or Enterprise plan. Assignments, user management, course operations. Click **Generate**. The full key is shown **once** — copy it now. You'll see something like: ``` kmcp_a8f3b2k9X7yZqWpL3mN8RvU2sT5dH6jK4F1gA9oP ``` This is the only time the full key appears. If you close the dialog without copying, you'll need to revoke and regenerate. The key acts as you — treat it like a password. ## 2. Install in your AI tool Pick the tool you use: Open your Claude Desktop config file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` Add a `mcpServers` entry: ```json theme={null} { "mcpServers": { "konstantly": { "command": "npx", "args": ["-y", "@konstantly/mcp-server"], "env": { "KONSTANTLY_MCP_KEY": "kmcp_a8f3b2k9X7yZ...", "KONSTANTLY_URL": "https://your-instance.konstantly.com" } } } } ``` Restart Claude Desktop. The 🔌 menu now shows `konstantly` with its tool list. One command: ```bash theme={null} claude mcp add konstantly \ --env KONSTANTLY_MCP_KEY=kmcp_a8f3b2k9X7yZ... \ --env KONSTANTLY_URL=https://your-instance.konstantly.com \ -- npx -y @konstantly/mcp-server ``` That's it — Claude Code picks it up automatically on the next prompt. Open or create `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "konstantly": { "command": "npx", "args": ["-y", "@konstantly/mcp-server"], "env": { "KONSTANTLY_MCP_KEY": "kmcp_a8f3b2k9X7yZ...", "KONSTANTLY_URL": "https://your-instance.konstantly.com" } } } } ``` Restart Cursor. The Konstantly tools appear in the agent panel. Replace both placeholders with your real values: * `KONSTANTLY_MCP_KEY` — the `kmcp_...` you copied in step 1 * `KONSTANTLY_URL` — your Konstantly instance URL, exactly as it appears in your browser (e.g. `https://acme.konstantly.com`) ## 3. Ask your first question Open your AI tool. Try a few prompts that work with the read-only tools: > **"Find a user named Sarah."** > > *(fires `find_user`)* > **"How many learners have completed the OSHA refresher course?"** > > *(fires `find_course` + `get_compliance_status`)* > **"Show me last week's leaderboard."** > > *(fires `get_leaderboard`)* > **"What's my own training status?"** > > *(fires `list_my_assignments`)* If you have write access, try: > **"Assign the new-hire bundle to user 42, deadline in 14 days."** > > *(fires `assign_training` — writes an audit log entry visible in Settings → Action Log)* ## 4. Verify the connection (optional) If something's not working, switch on debug mode by adding to your env config: ```json theme={null} "env": { "KONSTANTLY_MCP_KEY": "kmcp_...", "KONSTANTLY_URL": "https://your-instance.konstantly.com", "KONSTANTLY_MCP_DEBUG": "1" } ``` Restart your AI tool. The MCP server now logs to stderr — you can see every request, retry, and tool call. Common issues: | Symptom | Cause | Fix | | ------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | "Konstantly rejected the API key" | Key revoked or never existed | Generate a new key | | "license does not include MCP access" | Plan doesn't include MCP | Upgrade in **Settings → Billing** | | Tool list missing write tools | Your account doesn't have write permission, OR your key was issued without the `write` scope | Check your role's permissions; if needed, revoke + re-issue the key with write scope | | "KONSTANTLY\_URL must use https\://" | You typed `http://` | Fix the URL (must be `https://`) | | Nothing happens when you ask | Your AI tool didn't restart after config change | Quit and reopen | ## 5. What's next * See the full [Tools Reference](/api-reference/mcp/tools-reference) for everything you can do * Understand the [Authentication](/api-reference/mcp/authentication) model * Review [Security](/api-reference/mcp/security) best practices before sharing keys * Read the [Permissions matrix](/api-reference/mcp/permissions) to know which tools your role can use # Security Source: https://docs.konstantly.com/api-reference/mcp/security Rate limits, key hygiene, audit log, threat model, and operational best practices The MCP server's security posture mirrors the rest of Konstantly's API: bearer-token auth, per-user permissions, audit log, rate limits. This page covers what's specific to MCP and what to do operationally. ## Rate limits Three layers, each a sliding window: ### Per-key limits Set by license tier: | Tier | Per minute | Per day | | ---------- | ---------- | ------- | | Business | 60 | 1,000 | | Enterprise | 600 | 100,000 | When a limit is exceeded, the API returns: ```http theme={null} HTTP/1.1 429 Too Many Requests Content-Type: application/json {"error":"Rate limit exceeded (minute). Retry after 12s.","code":"MCP_RATE_LIMITED"} ``` The MCP server **automatically retries** up to 3 times with backoff, honoring the `Retry-After` indication. A well-behaved agent won't trip these in normal use. ### AI course-generation limits (separate, stricter) Every AI-powered tool shares a single tighter quota on top of the standard per-tier limit: * `create_course_from_prompt` * `create_course_from_files` * `refine_course` * `regenerate_question` * `regenerate_lesson` Each call consumes one unit of the daily AI quota regardless of which of the five tools fired it: | Scope | Limit | | ---------- | ---------------- | | Per key | 5 calls per day | | Per tenant | 50 calls per day | When either limit fires: ```http theme={null} HTTP/1.1 429 Too Many Requests {"error":"AI course generation per-key daily quota exceeded. Retry after 3600s."} ``` The error names which scope failed — `per-key` (your own quota) versus `per-tenant` (the workspace-wide quota). The MCP server retries with backoff like any other 429. ### Per-IP limits on failed auth 10 failed authentication attempts per minute per source IP. Designed to make brute-force enumeration noisy (you can detect it in logs) and to throttle bad actors without affecting legitimate use. ```http theme={null} HTTP/1.1 429 Too Many Requests {"error":"Too many failed MCP authentication attempts. Retry after 47s.","code":"MCP_AUTH_RATE_LIMITED"} ``` If you're seeing this in legitimate flows, your tool is misconfigured — most likely sending an expired or wrong key. Fix the env var. ## Key hygiene ### Treat keys like passwords MCP keys are bearer tokens. **Anyone holding the key can act as the issuing user** within the bounds of that user's permissions and license tier. Same exposure model as a GitHub PAT, AWS access key, or Stripe API key. Specifically: * Don't commit keys to git * Don't paste keys into Slack / Discord / email * Don't share keys between team members (each person should issue their own) * Don't reuse a key across multiple machines if you can avoid it (one per device makes revocation more surgical) ### Storage on customer devices The key lives in plaintext in your AI tool's config file: * **Claude Desktop (macOS)**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Claude Desktop (Windows)**: `%APPDATA%\Claude\claude_desktop_config.json` * **Cursor**: `~/.cursor/mcp.json` If your laptop is compromised, the key is too. Treat it accordingly. OS-keychain integration is on the roadmap — for now, the file-on-disk model is the same as how AWS CLI and many other developer tools handle credentials. ### Revoke on device loss If a device with a stored key is lost / stolen / sold / handed to another person: 1. Go to **Settings → MCP Keys** in Konstantly 2. Find the key for that device (use the name you gave it — *"My laptop"*, *"Production agent"*) 3. Click the trash icon → confirm The key stops working within seconds. No re-deploy needed; the next MCP call from that device 401s. ### Rotation Keys don't expire automatically. Best practice: * Calendar reminder to rotate every 90 days * After each org change (someone leaves, role changes) * Whenever you suspect compromise Optional expiry / auto-rotation is on the roadmap. Until then, rotation is manual. ## Audit log Every MCP **write-tool call** writes two events to your tenant's audit log: 1. **The action event** — same as a UI-driven action (e.g., `COURSE_ASSIGN`, `USER_BAN`) 2. **`MCP_TOOL_CALL` event** — explicitly tags the call as MCP-driven, with the tool name and redacted args in the `data` column This gives you two views: * **"What happened?"** — filter audit log normally; results include both UI and MCP-driven actions, indistinguishable from each other (which is correct — they're the same logical action). * **"What did MCP do?"** — filter on `eventType=88` (`MCP_TOOL_CALL`) for a fleet-wide view of agent-driven activity per quarter, per user, per tool. **Read tools are not audited per-call** — too high-volume. Instead they're tracked in the AI Usage dashboard alongside other AI-driven activity. ## Telemetry you get for free Each MCP call generates: * **An AI Usage dashboard entry** — tool name, key, duration, success/failure. Visible in the existing AI Usage view alongside other AI-driven activity. * **An internal analytics event** — fleet-wide tool-selection rates, success/failure ratios, per-tier usage. For ops teams: a spike in failures for a given tool is a strong signal of either a misconfigured key, a tool description the LLM is misunderstanding, or a backend regression. ## Threat model What the security posture defends against: ### Defended | Threat | Mitigation | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Key brute-force** | 256 bits of entropy = \~10⁷⁷ guesses required. Mathematically infeasible. Per-IP rate limit on auth failures adds telemetry signal. | | **Cross-tenant data access** | Per-request tenant resolution by subdomain. Keys are stored per-tenant; a key from tenant A used against tenant B's URL fails at lookup. | | **Privilege escalation** | Capabilities endpoint filters tools by permission BEFORE registering with the LLM. Each endpoint independently re-checks server-side. Layered defense. | | **License downgrade bypass** | License tier read fresh per request. Pre-issued write keys silently clamped. | | **Permission revocation lag** | No permission cache. Revoking a permission immediately blocks the next MCP call requiring it. | | **Timing attack on key validation** | Constant-time SHA-256 hash comparison (`crypto.timingSafeEqual`). | | **Information leak via error responses** | Bad-key / revoked-key / banned-user failures return the same opaque 401 with no body — no distinction between them. The license-disabled case is a deliberate exception: it returns 403 + `MCP_LICENSE_DISABLED`, which is only reachable with a valid key, so the leak is intentional (admin can act on the message instead of filing a support ticket). | | **CSRF** | Not applicable — MCP server is server-to-server, browser never sees `X-MCP-Key`. | | **Session fixation via MCP** | MCP auth synthesizes a SessionData but never sets a cookie. No persistent session created. | ### Acknowledged risks (mitigation = ops + customer hygiene) | Risk | Why we accept it | What you can do | | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Key stored plaintext on customer device | Industry-standard for developer tools today (AWS CLI, gh, npm). Keychain integration on roadmap. | Treat the file like a password store. Encrypt the disk. Revoke on device loss. | | Compromised npm publisher account → poisoned package | Real but not preventable from your side. We publish from a locked-down account and plan to ship sigstore provenance signatures so customers can verify via `npm audit signatures`. | Pin a specific version in enterprise installs (`@konstantly/mcp-server@0.2.0` instead of just the latest tag) to limit auto-flow of a compromised release. | | Customer types wrong `KONSTANTLY_URL` and ships key to wrong host | The MCP server refuses non-HTTPS URLs, but it still trusts whatever host the customer typed. | Use the install snippets from `/settings/api-keys` — copy-paste with the correct URL already filled in. Don't hand-edit URLs. | ## Operational checklist before public-facing use If you're rolling MCP out to a team or customer base: * [ ] **Audit your existing MCP keys** — are any held by users who've left? Revoke them. * [ ] **Document key-naming convention** — *"alex-laptop"*, *"prod-slack-bot"* makes revoke decisions obvious. *"key1"* doesn't. * [ ] **Set a rotation calendar** — 90 days is reasonable for human-held keys; CI bots can rotate weekly via automated re-issue. * [ ] **Surface MCP activity to your security team** — the AI Usage dashboard shows all agent activity in near-real-time. * [ ] **Wire audit log to your SIEM** — events 86 (`MCP_KEY_CREATE`), 87 (`MCP_KEY_REVOKE`), 88 (`MCP_TOOL_CALL`) are the MCP-relevant ones. * [ ] **Train your admins** to revoke keys before offboarding (same workflow as disabling SSO accounts). ## AI Policy How Konstantly handles AI processing — including which third-party providers receive customer data, how the MCP integration creates a sub-processing relationship with whichever AI agent you install (Claude, Cursor, etc.), and what we contractually disallow (e.g. training providers' models on customer data) — is documented in our published [AI Policy](https://konstantly.com/legal/ai-policy). Required reading for enterprise procurement reviews and EU AI Act compliance. ## Next steps Key format, scopes, license-tier interactions Role-by-role tool matrix All 25 tools Try it in 5 minutes # Tools Reference Source: https://docs.konstantly.com/api-reference/mcp/tools-reference All 25 MCP tools — parameters, permissions, example prompts, and what they return This page documents every tool the MCP server exposes. Each tool is filtered at registration time against your key's scopes and your user's permissions — so the AI sees only what it can actually invoke. If your tool list looks shorter than what's documented here, that's intentional: you don't have the permission or license tier required. See the [Permissions matrix](/api-reference/mcp/permissions) for which role unlocks which tool. *** ## Read tools Available to any key with `read` scope (which is all keys). ### find\_user Search for users by name or email substring. Case-insensitive. Name or email substring to search for. Max results (default 20, max 50). **Permission**: `user_view` **Example prompt**: *"Find a user named Sarah."* **Backend**: `GET /api/v2/users?query=...&limit=...` *** ### get\_user Get full details for one user by ID — name, email, role, group memberships, status. Internal user ID (from `find_user` or other tool). **Permission**: `user_view` **Example prompt**: *"Show me everything about user 42."* **Backend**: `GET /api/v2/users/:userId` *** ### find\_course Search for courses by name. Returns matching courses (id, name, status, annotation). Course name substring. Max results (default 20, max 50). **Permission**: `course_view` **Example prompt**: *"Find the OSHA refresher course."* **Backend**: `GET /api/v2/courses?query=...&limit=...` *** ### get\_course Full details for one course — name, structure, settings, publish state. Internal course ID (from `find_course`). **Permission**: `course_view` **Example prompt**: *"Show me the structure of course 17."* **Backend**: `GET /api/v2/courses/:courseId` *** ### list\_assignments List course assignments for **one user or one group** (exactly one required). No top-level "all assignments" view — too much data; the v2 API scopes by user or group. Filter by one user. Mutually exclusive with `groupId`. Filter by one group. Mutually exclusive with `userId`. **Permission**: `user_assign` **Example prompts**: * *"What is Jamie working on?"* → uses `userId` * *"What courses is the Engineering group assigned?"* → uses `groupId` **Backend**: `GET /api/v2/users/:userId/courses/assignments` or `GET /api/v2/groups/:groupId/courses/assignments` *** ### list\_my\_assignments Courses assigned to the current user (the key's owner), with deadlines and completion status. Useful for "what training do I owe?" questions. No parameters. **Permission**: any role (learner+) **Example prompt**: *"What training do I have due this week?"* **Backend**: `GET /api/v2/courses/assigned/me` *** ### get\_compliance\_status The flagship audit tool. Returns compliance breakdown for one course: who's assigned, who's done, who's overdue, completion percentages. Optionally scoped to one group. Course to check compliance against (e.g. OSHA refresher). Optional: scope to one group of users. **Permission**: `common_stat` **Example prompts**: * *"Pull a compliance report for OSHA confined-spaces training across all warehouse staff."* * *"Are we ready for the SOC 2 audit on security training?"* **Backend**: `GET /api/v2/courses/:courseId/statistics/users?groupId=...` *** ### list\_certificates All certificates earned by one user, with issue dates and (when set) expiry dates. Useful for recertification planning. User whose certificates to list. **Permission**: `user_view` **Example prompt**: *"Show Sarah's certifications and what's expiring in the next 90 days."* **Backend**: `GET /api/v2/users/:userId/certificates` *** ### get\_statistics High-level analytics — overall engagement, publishing rate, course-creation activity, insights, tag breakdowns. Which surface to query. One of: `overview`, `publishing`, `courseCreation`, `activity`, `insights`, `tags`. **Permission**: `common_stat` **Example prompt**: *"How are we doing on training engagement this quarter?"* **Backend**: `GET /api/v2/statistics/{scope}` For per-course or per-user numbers prefer `get_compliance_status` or `list_assignments`. *** ### get\_leaderboard Top learners by points/completion this period. Top N learners (default 10, max 50). **Permission**: `common_stat` **Example prompt**: *"Who are our top 5 learners this week? I want to send them a thank-you."* **Backend**: `GET /api/v2/statistics/leaders?limit=...` *** ### get\_audit\_log Read the platform audit log with optional filters. Use for security investigations, compliance questions, and inspecting actions taken by other MCP agents. Filter by acting user. ISO date or YYYY-MM-DD — start of window. ISO date or YYYY-MM-DD — end of window. Max results (default 50, max 250). Pagination offset. **Permission**: `audit_view` **Example prompt**: *"Pull all admin actions taken last Tuesday."* **Backend**: `GET /api/v2/audit?fields[author]=...&fields[createdAt][from]=...&fields[createdAt][to]=...` *** ## Write tools Available when the key has `write` scope AND your license tier is `full` (Business or Enterprise). ### assign\_training Assign one course to one learner. Idempotent — re-assigning the same user is a no-op. For bulk operations use `bulk_assign` instead. Course to assign. Learner to assign to. Unix timestamp (seconds) when the assignment is due. Omit for no deadline. Unix timestamp for scheduled future assignment. Omit to assign immediately. **Permission**: `user_assign` **Example prompts**: * *"Assign the new-hire onboarding to user 42, deadline two weeks from now."* * *"Schedule the Q1 compliance refresher for the new sales rep, starting next Monday."* **Backend**: `POST /api/v2/courses/:courseId/assignments/users` *** ### bulk\_assign Assign multiple courses to a whole group at once. Use for onboarding bundles or compliance pushes. Group whose members get the assignments. One or more courses to assign. Unix-timestamp deadline applied to ALL assignments. **Permission**: `group_assign` **Example prompts**: * *"Assign all new-hire training to the Engineering group, due in 30 days."* * *"Roll out the safety refresher to all warehouse staff."* **Backend**: per-course `POST /api/v2/courses/:courseId/assignments` with `{groups: [groupId]}` *** ### assign\_chain Assign an entire course chain (an ordered learning path) to users or groups. The caller must own the chain (per-chain ownership rule). Course-chain ID. Users to assign the chain to. Groups to assign the chain to. Unix-timestamp deadline for the whole chain. **Permission**: `user_assign` + chain ownership **Example prompt**: *"Put the new-hire learning path on Marco's plate, due 45 days from start date."* **Backend**: `POST /api/v2/courseChains/:chainId/assign` *** ### ban\_user Ban a user (revokes their login + course access, preserves history). Use for offboarding. Target must be below the caller's role in the hierarchy. User to ban. **Permission**: `user_ban` + role hierarchy **Example prompt**: *"Sarah left the company last Friday — disable her account."* **Backend**: `POST /api/v2/users/banned` *** ### unban\_user Restore a previously banned user's access. Used for reinstating offboarded learners (rehires, contractors who returned, etc.). User to unban. **Permission**: `user_ban` + role hierarchy **Example prompt**: *"Marco's back from sabbatical — re-enable his account."* **Backend**: `DELETE /api/v2/users/banned/:userId` *** ### change\_user\_email Update a user's email address (e.g. after a name change or domain migration). Triggers the standard email-change notification. User whose email is changing. New email address. **Permission**: `user_edit` + role hierarchy **Example prompt**: *"Maria changed her name — update her email to [maria.gomez@acme.com](mailto:maria.gomez@acme.com)."* **Backend**: `POST /api/v2/users/:userId/email/change` *** ### clone\_course Duplicate an existing course (creates an unpublished copy that you can then edit). Useful as the first step when forking a course for a different audience or location. Course to clone. **Permission**: `course_edit` **Example prompt**: *"Clone the Forklift Safety course for the new Phoenix warehouse — I'll customize it for their layout."* **Backend**: `POST /api/v2/courses/clone` with `{courseId}` in body *** ## AI course generation Four tools that let an agent build a complete course from a prompt or from uploaded source materials. Available on **Business and Enterprise** (`mcpAccess: "full"` + `mcpAiAccess: true`). Subject to a separate, stricter quota — 5 calls per key per day, 50 per tenant per day. The typical workflow: 1. `upload_source_material` — once per file. Returns a `sessionId`. 2. `create_course_from_files` (with the sessionId) or `create_course_from_prompt` (no files). Returns a `jobId`. 3. `get_course_generation_status` — poll every 3–5 seconds until status is `completed`, then read `result.courseId`. ### upload\_source\_material Read a local file off the user's machine and upload it as a course source. Text is extracted and embedded immediately. PDF / DOC / DOCX / PNG / JPG / GIF / WebP, 20 MB max per file. Absolute path to a local file on the user's machine. Optional. Reuse the same sessionId across multiple uploads to bundle files into one course-generation context. Omit and the tool generates one. **Permission**: `course_edit` + license `mcpAiAccess` **Example prompt**: *"Use this OSHA confined-spaces PDF I just shared as the source for a new course."* **Returns**: `{ sessionId, filename, chunkCount, message }` **Backend**: `POST /api/v2/chat/source-materials/upload` (multipart) then `POST /api/v2/chat/source-materials/process` *** ### create\_course\_from\_prompt Generate a complete course (title, modules, lessons, quiz) from a plain-English description. Returns a `jobId` — the actual generation runs in the background and takes 10–30 seconds. Plain-English description of the course. Be specific: include the topic, audience, scope. Optional. Target number of modules. Default 6. Optional. ISO 639-1 code (`en`, `es`, `fr`, …). If omitted, the model uses the language of the prompt. Optional. Override the AI-generated title. **Permission**: `course_edit` + license `mcpAiAccess` **Example prompt**: *"Build a compliance course on confined-space safety for warehouse staff. 6 lessons + a final quiz."* **Returns**: `{ jobId, status, message }` — `status` is always `queued` here; poll `get_course_generation_status`. **Backend**: `POST /api/v2/ai/courses/generate` with `{ prompt, options }` *** ### create\_course\_from\_files Same as `create_course_from_prompt` but grounded in previously-uploaded source materials. The agent must call `upload_source_material` at least once in this conversation before calling this tool. Plain-English direction for the course. The uploaded files are the primary source; the prompt sets tone, audience, scope. The sessionId returned by `upload_source_material`. Optional. Target number of modules. Optional. ISO 639-1 code. Optional. Override the title. **Permission**: `course_edit` + license `mcpAiAccess` **Example prompt**: *"Build a new-hire safety training based on these PDFs. 6 lessons, plain language, 10-question final quiz."* **Backend**: `POST /api/v2/ai/courses/generate` with `{ prompt, sessionId, options }` *** ### get\_course\_generation\_status Poll a queued generation job. Call every 3–5 seconds until `status` is `completed` (success — `result.courseId` is set) or `failed` (read `error.message`). The `jobId` returned by `create_course_from_prompt` or `create_course_from_files`. **Permission**: write scope (read-scope keys can't enqueue a job they're allowed to poll) **Returns**: ```json theme={null} { "jobId": "ai-course-2381", "status": "queued" | "running" | "completed" | "failed" | "unknown", "progress": 0–100, "result": { "courseId": 2419 }, // only when status="completed" "error": { "message": "...", "attemptsMade": 1 } // only when status="failed" } ``` **Backend**: `GET /api/v2/ai/courses/generate/:jobId` *** ## AI course refinement Three additional MCP tools that operate on courses you (or the agent) already generated. Same license tier as the generation tools (**Business and Enterprise**) and they share the same daily quota — 5 calls per key per day, 50 per tenant per day. Each call counts as one. The agent uses these three tools in addition to `create_course_from_prompt` / `create_course_from_files` to iterate on a course rather than starting fresh. Pick the narrowest tool for the user's intent: `regenerate_question` for one bad question, `regenerate_lesson` to rethink a lesson's brief, `refine_course` to add new structure to the whole course. ### refine\_course Append new elements to an existing course. **Append-only by design** — this tool cannot delete or modify anything that already exists, only add. Use when the author says "add a section on X" or "add more practice" or "tack a survey at the end". The course to extend. Get this from `find_course` or `get_course`. Plain-English description of what to add. Examples: *"Add a section on emergency procedures"*, *"Add two more practice quizzes after lesson 3"*, *"Add a final survey for course feedback"*. **Permission**: `course_edit` on the specific course (not just the role alias — the user must actually own / share / administer this course). **Returns**: `{ courseId, elementsAdded, transitionsAdded, message }`. **Example prompt**: *"For course 248, add a section on confined-space rescue procedures after the current safety lesson — 2 lessons + a quiz."* **Backend**: `POST /api/v2/ai/courses/:courseId/refine` with `{ feedback }` *** ### regenerate\_question Replace a single quiz question's text and options. The new question stays on the **same concept** as the old one (we're not drifting to a new subject — just rewriting). Previous content is preserved in your workspace audit log. The question to regenerate. Single-choice questions only. Optional. What's wrong with the current question and how to improve it. Examples: *"Make it harder"*, *"The distractors are too obvious"*, *"Reword for clarity"*. Omit for a generic "clearer version". **Permission**: `course_edit` on the parent course. **Returns**: `{ questionId, text, options, message }` with the new question content. **Example prompt**: *"Question #5821 in course 248 has confusing wording — make it clearer."* **Backend**: `POST /api/v2/ai/questions/:questionId/regenerate` with `{ feedback? }` *** ### regenerate\_lesson Regenerate the AI-authored brief that drives the lesson's content. **Safe by construction** — this does NOT touch any pages the author has already created; it only updates the brief that the editor uses to bootstrap new content. The lesson to regenerate the brief for. Optional. What's wrong with the current brief and how to improve it. Examples: *"Add a section on the regulatory context"*, *"Reduce to 3 key points"*, *"Target supervisors, not learners"*. Omit for a generic clarity pass. **Permission**: `course_edit` on the parent course. **Returns**: `{ lessonId, brief }` with the new brief content (topic, title, description, keyPoints, targetAudience). **Example prompt**: *"Lesson 312's brief is too theoretical — make it practical with concrete examples."* **Backend**: `POST /api/v2/ai/lessons/:lessonId/regenerate` with `{ feedback? }` *** ## Coming soon (not yet shipped) ### extend\_deadline + send\_reminder *(preview)* Push an assignment's deadline back, or nudge overdue learners. Currently not shipped because no corresponding v2 backend endpoint exists. ## Error handling Every tool maps backend errors to LLM-readable strings: | Backend status | Error shown to LLM | | -------------- | ----------------------------------------------------------------------- | | `403` | *"Permission denied — your user does not have rights for this action."* | | `404` | *"Not found — check the ID exists."* | | `422` | *"Invalid arguments: "* | | `429` | *"Rate limit exceeded. Slow down and retry."* | | Other | The original error message | The MCP server automatically retries 429s up to 3 times with backoff per the `Retry-After` header. Network errors, 5xx, and connection resets are propagated as `Error: ...` for the LLM to surface to you. ## Adding more tools The tool catalog is intentionally focused — we ship what's used, not what's listable. If you'd find a specific tool valuable (e.g. "add to group", "set custom attribute", "trigger webhook"), the v2 REST API already exposes the endpoint; we just need to add a wrapper. [Get in touch](https://konstantly.com?contact=true). # Get Roles Source: https://docs.konstantly.com/api-reference/roles/get-roles GET /roles Get a list of roles Retrieve a list of all available roles in the system. Roles define user permissions and access levels within your platform. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response Array of role objects Role identifier used in API calls (e.g., "administrator", "manager", "expert", "learner") Display name of the role shown in web interface ## Role Types 1. Built-in Roles: * `administrator`: Full system access * `manager`: Can manage users and content * `expert`: Can create and assess content * `learner`: Basic access to assigned content 2. Custom Roles: * Identified by numeric alias * Have custom display names * Permissions configured in web interface ## Usage Notes 1. Role Assignment: * Roles are required when creating users * Users can only have one role * Role changes require user re-authentication 2. Best Practices: * Audit role assignments regularly * Use least-privilege principle * Document custom role configurations ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/roles' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import List, Dict def get_roles(api_key: str) -> List[Dict[str, str]]: url = "https://YOURSITE.konstant.ly/openapi/v1/roles" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) response.raise_for_status() return response.json()['roles'] # Example usage try: roles = get_roles("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") print("Available Roles:") for role in roles: print(f"- {role['name']} (alias: {role['alias']})") except Exception as e: print(f"Error fetching roles: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getRoles(apiKey) { try { const response = await axios.get( 'https://YOURSITE.konstant.ly/openapi/v1/roles', { headers: { 'X-API-KEY': apiKey } } ); return response.data.roles; } catch (error) { console.error('Error fetching roles:', error.message); throw error; } } // Example usage getRoles('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv') .then(roles => { console.log('Available Roles:'); roles.forEach(role => { console.log(`- ${role.name} (alias: ${role.alias})`); }); }) .catch(error => console.error('Error:', error.message)); ``` ```php PHP theme={null} $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "X-API-KEY: {$apiKey}" ] ]); $response = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); $data = json_decode($response, true); return $data['roles']; } // Example usage try { $roles = getRoles("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv"); echo "Available Roles:\n"; foreach ($roles as $role) { echo "- {$role['name']} (alias: {$role['alias']})\n"; } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```json Response theme={null} { "roles": [ { "alias": "administrator", "name": "Administrator" }, { "alias": "manager", "name": "Manager" }, { "alias": "expert", "name": "Expert" }, { "alias": "learner", "name": "Learner" }, { "alias": "1", "name": "Custom Role Title" } ] } ``` # Get Global Statistics Source: https://docs.konstantly.com/api-reference/statistics/get-global-statistics GET /statistics Get the global statistics of Konstantly site Retrieve comprehensive statistics about your platform's usage, including course metrics, user activity, and performance data. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Response General platform statistics Total number of courses Number of course assignments Total number of groups Total number of users Array of users without any activity User ID User's full name Interface language Job title User location User timezone User's email User ban status User role Whether user was created via API User's avatar Array of users who haven't completed any courses Same structure as usersWithoutActivity Platform's top performers Number of completed courses Average result (0-100) User details Courses not assigned to any users Course ID Course name Course description Draft status Creation timestamp Last update timestamp Publication timestamp Course cover image ## Usage Notes 1. Activity Tracking: * Users are considered "without activity" if they haven't started any courses * Users are considered "without results" if they haven't completed any courses * Leaders are ranked by combination of completion count and average score 2. Timestamps: * All timestamps are in Unix epoch format (seconds since 1970) * Timezone conversions should be handled client-side 3. Performance: * This endpoint aggregates data across the entire platform * Response times may increase with platform size * Consider caching results if frequent polling is needed ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/statistics' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Dict, Any def get_global_statistics(api_key: str) -> Dict[str, Any]: url = "https://YOURSITE.konstant.ly/openapi/v1/statistics" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # Example usage try: stats = get_global_statistics("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") # Print general statistics print("Platform Statistics:") print(f"Total Courses: {stats['statistics']['coursesCount']}") print(f"Total Users: {stats['statistics']['usersCount']}") print(f"Total Groups: {stats['statistics']['groupsCount']}") # Print inactive users print(f"\nUsers without activity: {len(stats['usersWithoutActivity'])}") # Print top performers print("\nTop Performers:") for leader in stats['leaders']: print(f"User: {leader['user']['name']}") print(f"Completed Courses: {leader['coursesCount']}") print(f"Average Score: {leader['resultValue']}%") print("---") # Print unassigned courses print(f"\nUnassigned Courses: {len(stats['unassignedCourses'])}") except Exception as e: print(f"Error fetching statistics: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getGlobalStatistics(apiKey) { try { const response = await axios.get( 'https://YOURSITE.konstant.ly/openapi/v1/statistics', { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { console.error('Error fetching statistics:', error.message); throw error; } } // Example usage getGlobalStatistics('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv') .then(stats => { // Print general statistics console.log('Platform Statistics:'); console.log(`Total Courses: ${stats.statistics.coursesCount}`); console.log(`Total Users: ${stats.statistics.usersCount}`); console.log(`Total Groups: ${stats.statistics.groupsCount}`); // Print inactive users console.log(`\nUsers without activity: ${stats.usersWithoutActivity.length}`); // Print top performers console.log('\nTop Performers:'); stats.leaders.forEach(leader => { console.log(`User: ${leader.user.name}`); console.log(`Completed Courses: ${leader.coursesCount}`); console.log(`Average Score: ${leader.resultValue}%`); console.log('---'); }); // Print unassigned courses console.log(`\nUnassigned Courses: ${stats.unassignedCourses.length}`); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} { "statistics": { "coursesCount": 25, "assignedCoursesCount": 150, "groupsCount": 10, "usersCount": 100 }, "usersWithoutActivity": [ { "id": "user123", "name": "John Smith", "email": "john@example.com", "language": "en", "timezone": "Europe/London", "role": { "alias": "learner" }, "isBanned": false, "fromApi": false, "image": null } ], "usersWithoutResults": [ { "id": "user456", "name": "Jane Doe", "email": "jane@example.com", "language": "en", "timezone": "America/New_York", "role": { "alias": "learner" }, "isBanned": false, "fromApi": false, "image": null } ], "leaders": [ { "coursesCount": 15, "resultValue": 95, "user": { "id": "user789", "name": "Alice Johnson", "email": "alice@example.com", "role": { "alias": "learner" } } } ], "unassignedCourses": [ { "id": 123, "name": "Introduction Course", "annotation": "Basic introduction", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229, "image": null } ] } ``` # Deactivate User Source: https://docs.konstantly.com/api-reference/users/block-user POST /users/blocked Deactivate (ban) a user account Deactivate a user account, preventing them from logging in. Deactivated users are not counted towards your license. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body User API ID to deactivate ## Response Returns the user object with `isBanned: true`. User ID Full name Interface language Email address Will be `true` after deactivation User role API management status ### Error Responses Returned when user is not found HTTP status code (404) Error message Returned when user cannot be banned (e.g., assigned as expert on courses) HTTP status code (422) Error message explaining why user cannot be banned Additional data about blocking courses ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/blocked' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "id": "1234567890" }' ``` ```python Python theme={null} import requests def deactivate_user(api_key: str, user_id: str) -> dict: url = "https://YOURSITE.konstant.ly/openapi/v1/users/blocked" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } data = {"id": user_id} response = requests.post(url, headers=headers, json=data) if response.status_code == 404: raise ValueError(f"User {user_id} not found") if response.status_code == 422: error_data = response.json() raise ValueError(f"Cannot deactivate user: {error_data['message']}") response.raise_for_status() return response.json() # Example usage try: user = deactivate_user("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") print(f"User {user['name']} has been deactivated") print(f"isBanned: {user['isBanned']}") except Exception as e: print(f"Failed to deactivate user: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function deactivateUser(apiKey, userId) { try { const response = await axios.post( 'https://YOURSITE.konstant.ly/openapi/v1/users/blocked', { id: userId }, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } if (error.response?.status === 422) { throw new Error(`Cannot deactivate: ${error.response.data.message}`); } throw error; } } // Example usage deactivateUser('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(user => { console.log(`User ${user.name} has been deactivated`); console.log(`isBanned: ${user.isBanned}`); }) .catch(error => console.error('Failed:', error.message)); ``` ```json Response theme={null} { "id": "1234567890", "name": "John Doe", "language": "en", "occupation": "Salesman", "location": "London", "timezone": "Europe/London", "email": "john.doe@example.com", "isBanned": true, "role": { "alias": "learner" }, "fromApi": false, "image": null } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` ```json 422 Error theme={null} { "status": 422, "message": "User cannot be banned because he/she is assigned as expert at courses", "data": { "courses": [ { "id": 123, "name": "Course name", "expertHomeworks": [...], "expertClassworks": [...] } ] } } ``` # Create User Source: https://docs.konstantly.com/api-reference/users/create-user POST /users Create a new user Create a new user on your platform with specified details and optional group/course assignments. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Request Body Full name of the user Email of the user (must be unique across the site) User password Alias or identifier of role to assign to user User ID (alphanumeric unique identifier). Generated automatically if not provided. Optional bio description for About me section Optional job title Optional location Timezone name in TZ format (e.g. "Europe/London") When true, user data can only be edited via API. Set false to allow web interface editing. List of course IDs to assign to the user Timestamp for course assignment deadline List of group IDs to add the user to Custom user attributes specific to your site ## Response User ID Full name Interface language (de, en, es, fr, pl, pt, ru) Job title Location Timezone Email address Ban status User role API management status User avatar Image ID Full-size image URL 36x36px image URL 58x58px image URL 128x128px image URL Custom attributes ### Error Responses Returned when request validation fails HTTP status code (400) Error message Field-specific validation errors ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "name": "Jason Wunderliebb", "email": "j.wunderliebb@mycompany.tld", "password": "VerySecurePas$w0rd!", "roleAlias": "learner", "bio": "Hi folks! I love to sell and teach about sales.", "occupation": "Associate", "location": "London", "timezone": "Europe/London", "fromApi": false, "courses": [110, 150], "deadlineAt": 1234567890, "groups": [2], "attributes": { "designation": 4, "department": 15, "mobile_number": "+44 0 144 44 444" } }' ``` ```python Python theme={null} import requests from typing import List, Dict, Optional class UserAPI: def __init__(self, api_key: str): self.api_key = api_key self.base_url = "https://YOURSITE.konstant.ly/openapi/v1" def create_user( self, name: str, email: str, password: str, role_alias: str, occupation: Optional[str] = None, location: Optional[str] = None, timezone: Optional[str] = None, bio: Optional[str] = None, from_api: bool = True, courses: Optional[List[int]] = None, deadline_at: Optional[int] = None, groups: Optional[List[int]] = None, attributes: Optional[Dict] = None ) -> dict: url = f"{self.base_url}/users" data = { "name": name, "email": email, "password": password, "roleAlias": role_alias, "fromApi": from_api } # Add optional fields if provided if occupation: data["occupation"] = occupation if location: data["location"] = location if timezone: data["timezone"] = timezone if bio: data["bio"] = bio if courses: data["courses"] = courses if deadline_at: data["deadlineAt"] = deadline_at if groups: data["groups"] = groups if attributes: data["attributes"] = attributes headers = { "X-API-KEY": self.api_key, "Content-Type": "application/json" } try: response = requests.post(url, headers=headers, json=data) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error creating user: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": api = UserAPI("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv") try: user = api.create_user( name="Jason Wunderliebb", email="j.wunderliebb@mycompany.tld", password="VerySecurePas$w0rd!", role_alias="learner", occupation="Associate", location="London", timezone="Europe/London", bio="Hi folks! I love to sell and teach about sales.", from_api=False, courses=[110, 150], deadline_at=1234567890, groups=[2], attributes={ "designation": 4, "department": 15, "mobile_number": "+44 0 144 44 444" } ) print("User created successfully!") print(f"ID: {user['id']}") print(f"Name: {user['name']}") print(f"Email: {user['email']}") except Exception as e: print(f"Failed to create user: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class UsersAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } }); } async createUser({ name, email, password, roleAlias, bio, occupation, location, timezone, fromApi = true, courses, deadlineAt, groups, attributes }) { try { const response = await this.client.post('/users', { name, email, password, roleAlias, bio, occupation, location, timezone, fromApi, courses, deadlineAt, groups, attributes }); return response.data; } catch (error) { if (error.response) { if (error.response.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } throw new Error(`Failed to create user: ${error.response.data.message || error.message}`); } throw error; } } } // Example usage async function main() { const usersApi = new UsersAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const user = await usersApi.createUser({ name: "Jason Wunderliebb", email: "j.wunderliebb@mycompany.tld", password: "VerySecurePas$w0rd!", roleAlias: "learner", bio: "Hi folks! I love to sell and teach about sales.", occupation: "Associate", location: "London", timezone: "Europe/London", fromApi: false, courses: [110, 150], deadlineAt: 1234567890, groups: [2], attributes: { designation: 4, department: 15, mobile_number: "+44 0 144 44 444" } }); console.log('User created successfully!'); console.log(`ID: ${user.id}`); console.log(`Name: ${user.name}`); console.log(`Email: ${user.email}`); } catch (error) { console.error('Failed to create user:', error.message); } } main(); ``` ```json Response theme={null} { "id": "1234567890", "name": "Jason Wunderliebb", "language": "en", "occupation": "Associate", "location": "London", "timezone": "Europe/London", "email": "j.wunderliebb@mycompany.tld", "isBanned": false, "role": { "alias": "learner" }, "fromApi": false, "image": null, "attributes": { "designation": 4, "department": 15, "mobile_number": "+44 0 144 44 444" } } ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "email": ["Email is already taken"], "password": ["Password must be at least 8 characters"] } } ``` # Delete User Source: https://docs.konstantly.com/api-reference/users/delete-user DELETE /users/{userId} Delete user account Permanently delete a user account. Users must be deactivated (blocked) before they can be deleted. Use the [Deactivate User](/api-reference/users/block-user) endpoint first. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Response On success, returns HTTP 200 status code. ### Error Responses Not Found error response HTTP status code (404) Error message Unprocessable Entity error response HTTP status code (422) Error message ```bash cURL theme={null} curl --request DELETE \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def delete_user(api_key: str, user_id: str) -> None: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}" headers = {"X-API-KEY": api_key} response = requests.delete(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") elif response.status_code == 422: error_data = response.json() raise ValueError(error_data.get('message', 'Cannot process request')) response.raise_for_status() # Example usage try: delete_user("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") print("User deleted successfully") except Exception as e: print(f"Failed to delete user: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function deleteUser(apiKey, userId) { try { await axios.delete( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}`, { headers: { 'X-API-KEY': apiKey } } ); } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } if (error.response?.status === 422) { throw new Error(error.response.data.message || 'Cannot process request'); } throw error; } } // Example usage deleteUser('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(() => console.log('User deleted successfully')) .catch(error => console.error('Failed to delete user:', error.message)); ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` ```json 422 Error theme={null} { "status": 422, "message": "You should block users before deleting them" } ``` # Get User Assignments Source: https://docs.konstantly.com/api-reference/users/get-user-assignments GET /users/{userId}/assignments Get a list of courses assigned to the user Retrieve a list of all courses assigned to a specific user. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Response Array of course assignments Timestamp when course was assigned Timestamp when user started the course Timestamp when assignment expires Timestamp when user finished the course Current progress (0-100) Course result (0-100) Whether user has started the course Whether user has finished the course Whether assignment has expired Course details Course ID Course name Course annotation Draft status Creation timestamp Last update timestamp Publication timestamp Course cover image ### Error Responses Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/assignments' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def get_user_assignments(api_key: str, user_id: str) -> dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/assignments" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json() # Example usage try: assignments = get_user_assignments("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") for assignment in assignments['assignments']: course = assignment['course'] print(f"Course: {course['name']}") print(f"Progress: {assignment['progressValue']}%") print(f"Status: {'Completed' if assignment['isCourseFinished'] else 'In Progress'}") print("---") except Exception as e: print(f"Error: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getUserAssignments(apiKey, userId) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/assignments`, { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage getUserAssignments('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(data => { data.assignments.forEach(assignment => { console.log(`Course: ${assignment.course.name}`); console.log(`Progress: ${assignment.progressValue}%`); console.log(`Status: ${assignment.isCourseFinished ? 'Completed' : 'In Progress'}`); console.log('---'); }); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} { "assignments": [ { "assignedAt": 1443183322, "startedAt": 1443188932, "deadlineAt": 1444188932, "finishedAt": 1443188946, "progressValue": 100, "resultValue": 50, "isCourseStarted": true, "isCourseFinished": true, "isExpired": false, "course": { "id": 123, "name": "Introduction to Sales", "annotation": "Learn the basics of sales", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229, "image": null } } ] } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Get User by Email Source: https://docs.konstantly.com/api-reference/users/get-user-by-email GET /users/byEmail/{userEmail} Get user information by email address Retrieve detailed information about a specific user using their email address. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters Email address of the user to retrieve ## Response User ID Full name Interface language (de, en, es, fr, pl, pt, ru) Job title Location Timezone Email address Ban status User role API management status User avatar Image ID Full-size image URL 36x36px image URL 58x58px image URL 128x128px image URL Custom attributes ### Error Responses Returned for validation errors HTTP status code (400) Error message Field-specific validation errors Returned when user is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/byEmail/user@example.com' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Dict def get_user_by_email(api_key: str, email: str) -> Dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/byEmail/{email}" headers = { "X-API-KEY": api_key } try: response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"User with email {email} not found") response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching user: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: user = get_user_by_email( api_key="1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", email="user@example.com" ) print(f"Found user: {user['name']}") print(f"ID: {user['id']}") print(f"Role: {user['role']['alias']}") except Exception as e: print(f"Failed to fetch user: {str(e)}") ``` ```php PHP theme={null} apiKey = $apiKey; $this->baseUrl = 'https://YOURSITE.konstant.ly/openapi/v1'; } public function getUserByEmail($email) { $ch = curl_init(); $url = "{$this->baseUrl}/users/byEmail/" . urlencode($email); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-KEY: ' . $this->apiKey ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { throw new Exception('Error: ' . curl_error($ch)); } curl_close($ch); $responseData = json_decode($response, true); if ($httpCode === 404) { throw new Exception("User with email {$email} not found"); } if ($httpCode !== 200) { throw new Exception("Failed to fetch user. Status code: {$httpCode}"); } return $responseData; } } // Example usage try { $usersApi = new UsersAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); $user = $usersApi->getUserByEmail('user@example.com'); echo "Found user: {$user['name']}\n"; echo "ID: {$user['id']}\n"; echo "Role: {$user['role']['alias']}\n"; } catch (Exception $e) { echo "Failed to fetch user: " . $e->getMessage() . "\n"; } ?> ``` ```javascript Node.js theme={null} const axios = require('axios'); class UsersAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async getUserByEmail(email) { try { const response = await this.client.get(`/users/byEmail/${encodeURIComponent(email)}`); return response.data; } catch (error) { if (error.response) { if (error.response.status === 404) { throw new Error(`User with email ${email} not found`); } throw new Error(`Failed to fetch user: ${error.response.data.message || error.message}`); } throw error; } } } // Example usage async function main() { const usersApi = new UsersAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const user = await usersApi.getUserByEmail('user@example.com'); console.log(`Found user: ${user.name}`); console.log(`ID: ${user.id}`); console.log(`Role: ${user.role.alias}`); } catch (error) { console.error('Failed to fetch user:', error.message); } } main(); ``` ```json Response theme={null} { "id": "1234567890", "name": "John Doe", "language": "en", "occupation": "Salesman", "location": "London", "timezone": "Europe/London", "email": "user@example.com", "isBanned": false, "role": { "alias": "learner" }, "fromApi": false, "image": null, "attributes": {} } ``` ```json 404 Error theme={null} { "status": 404, "message": "User not found" } ``` ```json 400 Error theme={null} { "status": 400, "message": "Invalid email format" } ``` # Get User by ID Source: https://docs.konstantly.com/api-reference/users/get-user-by-id GET /users/{userId} Get user personal information by user ID Retrieve detailed information about a specific user using their unique ID. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID (alphanumeric unique identifier) ## Response User ID, alphanumeric unique identifier Full name of the user Interface language (de, en, es, fr, pl, pt, ru) Job title Location Timezone in TZ format Email address Whether user is banned from login User role alias/identifier Whether user is managed via API only User avatar Image ID Full-size image URL 36x36px image URL 58x58px image URL 128x128px image URL Custom user attributes ### Error Responses Returned when user is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def get_user_by_id(api_key: str, user_id: str) -> dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json() # Example usage try: user = get_user_by_id("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") print(f"User: {user['name']}") print(f"Email: {user['email']}") except Exception as e: print(f"Failed to fetch user: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getUserById(apiKey, userId) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}`, { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage getUserById('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(user => { console.log(`User: ${user.name}`); console.log(`Email: ${user.email}`); }) .catch(error => console.error('Failed:', error.message)); ``` ```json Response theme={null} { "id": "1234567890", "name": "John Doe", "language": "en", "occupation": "Salesman", "location": "London", "timezone": "Europe/London", "email": "john.doe@example.com", "isBanned": false, "role": { "alias": "learner" }, "fromApi": false, "image": null, "attributes": {} } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # List User Groups Source: https://docs.konstantly.com/api-reference/users/get-user-groups GET /users/{userId}/groups List the groups the user belongs to Retrieve a list of all groups that a specific user is a member of. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Response Array of groups the user belongs to Group ID Parent group ID Group name Number of users in the group Number of courses assigned to the group Group cover image Image ID Full-size image URL 36x36px image URL 330x130px image URL Custom group attributes ### Error Responses Returned when user is not found HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/groups' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import List, Dict def get_user_groups(api_key: str, user_id: str) -> List[Dict]: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/groups" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json()['groups'] # Example usage try: groups = get_user_groups("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") print(f"User belongs to {len(groups)} groups:") for group in groups: print(f" - {group['name']} (ID: {group['id']})") except Exception as e: print(f"Failed to fetch user groups: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getUserGroups(apiKey, userId) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/groups`, { headers: { 'X-API-KEY': apiKey } } ); return response.data.groups; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage getUserGroups('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(groups => { console.log(`User belongs to ${groups.length} groups:`); groups.forEach(group => { console.log(` - ${group.name} (ID: ${group.id})`); }); }) .catch(error => console.error('Failed:', error.message)); ``` ```json Response theme={null} { "groups": [ { "id": 1, "parentId": null, "name": "Sales Team", "usersCount": 15, "coursesCount": 5, "image": null, "attributes": {} }, { "id": 2, "parentId": 1, "name": "Regional Sales - London", "usersCount": 8, "coursesCount": 3, "image": null, "attributes": {} } ] } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Get Users Source: https://docs.konstantly.com/api-reference/users/get-users GET /users Get a list of users (sorted by name, 20 entries per page) Retrieve a paginated list of users from your platform. The list is sorted by name and includes 20 entries per page. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Query Parameters Get user info by email Example: [user@example.com](mailto:user@example.com) Entry number to start listing from Example: 20 ## Response Array of user objects User ID, alphanumeric unique identifier Full name of the user Interface and notification language (one of: de, en, es, fr, pl, pt, ru) Optional job title Optional location Timezone in TZ format User email (unique across platform) Whether user is banned from login User role alias/identifier Whether user is managed via API only User avatar details Image ID URL to full-size image URL to 36x36px image URL to 58x58px image URL to 128x128px image Custom user attributes The total number of users ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users?offset=20' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def get_users(api_key: str, offset: int = None, query: str = None) -> dict: url = "https://YOURSITE.konstant.ly/openapi/v1/users" headers = { "X-API-KEY": api_key } params = {} if offset is not None: params["offset"] = offset if query is not None: params["query"] = query try: response = requests.get(url, headers=headers, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching users: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f"Response: {e.response.text}") raise # Example usage if __name__ == "__main__": try: users_data = get_users( api_key="1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", offset=20 ) print(f"Total users: {users_data['totalCount']}") print("\nUsers:") for user in users_data['users']: print(f"- {user['name']} ({user['email']})") except Exception as e: print(f"Failed to fetch users: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); class UsersAPI { constructor(apiKey) { this.client = axios.create({ baseURL: 'https://YOURSITE.konstant.ly/openapi/v1', headers: { 'X-API-KEY': apiKey } }); } async getUsers(params = {}) { try { const response = await this.client.get('/users', { params }); return response.data; } catch (error) { if (error.response) { throw new Error(`Failed to fetch users: ${error.response.data.message || error.message}`); } throw error; } } } // Example usage async function main() { const usersApi = new UsersAPI('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv'); try { const usersData = await usersApi.getUsers({ offset: 20 }); console.log(`Total users: ${usersData.totalCount}`); console.log('\nUsers:'); usersData.users.forEach(user => { console.log(`- ${user.name} (${user.email})`); }); } catch (error) { console.error('Failed to fetch users:', error.message); } } main(); ``` ```json Response theme={null} { "users": [ { "id": "1234567890", "name": "John Doe", "language": "en", "occupation": "Salesman", "location": "London", "timezone": "Europe/London", "email": "john.doe@example.com", "isBanned": false, "role": { "alias": "learner" }, "fromApi": false, "image": null, "attributes": {} } ], "totalCount": 1 } ``` # Assign Courses to User Source: https://docs.konstantly.com/api-reference/users/post-user-assignments POST /users/{userId}/assignments Assign courses to the user Assign one or more courses to a specific user. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Request Body Array of course IDs to assign to the user Timestamp when the courses should be assigned (UTC) Timestamp for assignment completion deadline (UTC) ## Response Array of created course assignments Timestamp when course was assigned Timestamp when user started the course Timestamp when assignment expires Timestamp when user finished the course Current progress (0-100) Course result (0-100) Whether user has started the course Whether user has finished the course Whether assignment has expired Course details ### Error Responses Validation error response HTTP status code (400) Error message Field-specific validation errors Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/assignments' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "courses": [110, 150], "deadlineAt": 1704067200 }' ``` ```python Python theme={null} import requests from typing import List, Optional def assign_courses(api_key: str, user_id: str, courses: List[int], assign_at: Optional[int] = None, deadline_at: Optional[int] = None) -> dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/assignments" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } data = {"courses": courses} if assign_at: data["assignAt"] = assign_at if deadline_at: data["deadlineAt"] = deadline_at response = requests.post(url, headers=headers, json=data) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json() # Example usage try: result = assign_courses( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890", courses=[110, 150], deadline_at=1704067200 ) print(f"Assigned {len(result['assignments'])} courses") for assignment in result['assignments']: print(f"Course: {assignment['course']['name']}") except Exception as e: print(f"Failed to assign courses: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function assignCourses(apiKey, userId, courses, assignAt = null, deadlineAt = null) { const data = { courses }; if (assignAt) data.assignAt = assignAt; if (deadlineAt) data.deadlineAt = deadlineAt; try { const response = await axios.post( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/assignments`, data, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { if (error.response?.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage assignCourses( '1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890', [110, 150], null, 1704067200 ) .then(result => { console.log(`Assigned ${result.assignments.length} courses`); result.assignments.forEach(assignment => { console.log(`Course: ${assignment.course.name}`); }); }) .catch(error => console.error('Failed to assign courses:', error.message)); ``` ```json Response theme={null} { "assignments": [ { "assignedAt": 1443183322, "startedAt": 0, "deadlineAt": 1704067200, "finishedAt": 0, "progressValue": 0, "resultValue": 0, "isCourseStarted": false, "isCourseFinished": false, "isExpired": false, "course": { "id": 110, "name": "Sales Fundamentals", "annotation": "Core sales concepts and techniques", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229, "image": null } } ] } ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "courses": ["Courses array cannot be empty"] } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Activate User Source: https://docs.konstantly.com/api-reference/users/unblock-user DELETE /users/blocked/{userId} Activate (unban) a deactivated user account Reactivate a previously deactivated user account, allowing them to log in again. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID to activate ## Response Returns an empty response on success. ### Error Responses Returned when user is not found HTTP status code (404) Error message Returned when license limit is exceeded and user cannot be activated HTTP status code (402) Payment required message ```bash cURL theme={null} curl --request DELETE \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/blocked/1234567890' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def activate_user(api_key: str, user_id: str) -> bool: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/blocked/{user_id}" headers = {"X-API-KEY": api_key} response = requests.delete(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") if response.status_code == 402: raise ValueError("License limit exceeded. Upgrade your plan to activate more users.") response.raise_for_status() return True # Example usage try: if activate_user("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890"): print("User has been activated successfully") except Exception as e: print(f"Failed to activate user: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function activateUser(apiKey, userId) { try { await axios.delete( `https://YOURSITE.konstant.ly/openapi/v1/users/blocked/${userId}`, { headers: { 'X-API-KEY': apiKey } } ); return true; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } if (error.response?.status === 402) { throw new Error('License limit exceeded. Upgrade your plan.'); } throw error; } } // Example usage activateUser('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(() => console.log('User has been activated successfully')) .catch(error => console.error('Failed:', error.message)); ``` ```json 200 Response theme={null} {} ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` ```json 402 Error theme={null} { "status": 402, "message": "Payment required" } ``` # Update User Source: https://docs.konstantly.com/api-reference/users/update-user PATCH /users/{userId} Edit user's personal information Update an existing user's information. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Request Body User's email address (must be unique) Full name of the user (only if changing) User password (only if changing) Role alias or identifier (only if changing) Optional bio description Optional job title (only if changing) Optional location (only if changing) Timezone in TZ format (only if changing) API management flag (only if changing) Complete set of custom user attributes (overwrites current values) ## Response On success, returns HTTP 200 status code. ### Error Responses Validation error response HTTP status code (400) Error message Field-specific validation errors Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request PATCH \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "email": "j.wunderliebb@mycompany.tld", "bio": "An updated information about me: I'\''ve transferred to London!", "occupation": "Associate", "location": "London", "timezone": "Europe/London" }' ``` ```python Python theme={null} import requests def update_user(api_key: str, user_id: str, update_data: dict) -> None: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } response = requests.patch(url, headers=headers, json=update_data) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() # Example usage update_data = { "email": "j.wunderliebb@mycompany.tld", "bio": "An updated information about me: I've transferred to London!", "occupation": "Associate", "location": "London", "timezone": "Europe/London" } try: update_user("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890", update_data) print("User updated successfully") except Exception as e: print(f"Failed to update user: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function updateUser(apiKey, userId, updateData) { try { await axios.patch( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}`, updateData, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); } catch (error) { if (error.response?.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage const updateData = { email: "j.wunderliebb@mycompany.tld", bio: "An updated information about me: I've transferred to London!", occupation: "Associate", location: "London", timezone: "Europe/London" }; updateUser('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890', updateData) .then(() => console.log('User updated successfully')) .catch(error => console.error('Failed to update user:', error.message)); ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "email": ["Invalid email format"] } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Get User Certificates Source: https://docs.konstantly.com/api-reference/users/user-certificates GET /users/{userId}/certificates Get a list of user certificates (20 per page) Retrieve a paginated list of certificates earned by a specific user. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Query Parameters Entry number to start listing from Example: `20` Number of entries to return ## Response Array of certificates Certificate ID Certificate name Certificate description Associated course ID Associated course name URL to certificate image Achievement timestamp Expiration timestamp (if applicable) Total number of certificates ### Error Responses Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/certificates?offset=0&limit=20' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Optional def get_user_certificates(api_key: str, user_id: str, offset: Optional[int] = None, limit: Optional[int] = None) -> dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/certificates" headers = {"X-API-KEY": api_key} params = {} if offset is not None: params["offset"] = offset if limit is not None: params["limit"] = limit response = requests.get(url, headers=headers, params=params) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json() # Example usage try: result = get_user_certificates( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890", offset=0, limit=20 ) print(f"Total certificates: {result['totalCount']}") for cert in result['certificates']: print(f"Certificate: {cert['name']}") print(f"Course: {cert['courseName']}") print(f"Achieved: {cert['achievedAt']}") print("---") except Exception as e: print(f"Error: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getUserCertificates(apiKey, userId, offset = null, limit = null) { const params = {}; if (offset !== null) params.offset = offset; if (limit !== null) params.limit = limit; try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/certificates`, { headers: { 'X-API-KEY': apiKey }, params } ); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage getUserCertificates('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890', 0, 20) .then(result => { console.log(`Total certificates: ${result.totalCount}`); result.certificates.forEach(cert => { console.log(`Certificate: ${cert.name}`); console.log(`Course: ${cert.courseName}`); console.log(`Achieved: ${cert.achievedAt}`); console.log('---'); }); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} { "certificates": [ { "id": 123, "name": "Sales Professional Certificate", "description": "Advanced Sales Training Completion", "courseId": 110, "courseName": "Advanced Sales Techniques", "imageUrl": "http://s3.amazonaws.com/certificates/cert123.png", "achievedAt": 1443188946, "expiresAt": 1474724946 } ], "totalCount": 1 } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # User Groups Source: https://docs.konstantly.com/api-reference/users/user-groups POST /users/{userId}/groups Add user to groups Add a user to one or more groups. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Request Body Array of group IDs to add the user to ## Response Array of groups the user was added to Group ID Parent group ID Group name Number of users in group Number of assigned courses Group image details Image ID Original image URL 36x36px thumbnail URL 330x130px image URL Custom group attributes ### Error Responses Validation error response HTTP status code (400) Error message Validation errors Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request POST \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/groups' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' \ --header 'Content-Type: application/json' \ --data '{ "groups": [1, 2, 3] }' ``` ```python Python theme={null} import requests def add_user_to_groups(api_key: str, user_id: str, group_ids: list) -> dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/groups" headers = { "X-API-KEY": api_key, "Content-Type": "application/json" } data = {"groups": group_ids} response = requests.post(url, headers=headers, json=data) if response.status_code == 400: error_data = response.json() print("Validation errors:") for field, errors in error_data.get('errors', {}).items(): print(f"{field}: {', '.join(errors)}") raise ValueError(error_data.get('message', 'Invalid request')) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json() # Example usage try: result = add_user_to_groups( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890", [1, 2, 3] ) print(f"Added user to {len(result['groups'])} groups") for group in result['groups']: print(f"Group: {group['name']}") except Exception as e: print(f"Failed to add user to groups: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function addUserToGroups(apiKey, userId, groupIds) { try { const response = await axios.post( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/groups`, { groups: groupIds }, { headers: { 'X-API-KEY': apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { if (error.response?.status === 400) { console.error('Validation errors:'); const errors = error.response.data.errors || {}; Object.entries(errors).forEach(([field, messages]) => { console.error(`${field}: ${messages.join(', ')}`); }); throw new Error(error.response.data.message || 'Invalid request'); } if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage addUserToGroups('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890', [1, 2, 3]) .then(result => { console.log(`Added user to ${result.groups.length} groups`); result.groups.forEach(group => { console.log(`Group: ${group.name}`); }); }) .catch(error => console.error('Failed to add user to groups:', error.message)); ``` ```json Response theme={null} { "groups": [ { "id": 1, "parentId": null, "name": "Sales Team", "usersCount": 15, "coursesCount": 5, "image": null, "attributes": {} }, { "id": 2, "parentId": 1, "name": "Regional Sales", "usersCount": 8, "coursesCount": 3, "image": null, "attributes": {} } ] } ``` ```json 400 Error theme={null} { "status": 400, "message": "Validation failed", "errors": { "groups": ["Groups array cannot be empty"] } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Get SSO URL Source: https://docs.konstantly.com/api-reference/users/user-sso GET /users/{userId}/sso Get SSO (Single Sign-On) URL for a user. Link expires in 30 seconds. Generate a temporary SSO URL that allows direct login for a specific user. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Response SSO URL that can be used for direct login. The URL expires in 30 seconds. ### Error Responses Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/sso' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def get_sso_url(api_key: str, user_id: str) -> str: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/sso" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json()['url'] # Example usage try: sso_url = get_sso_url("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") print(f"SSO URL (valid for 30 seconds): {sso_url}") except Exception as e: print(f"Failed to get SSO URL: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getSsoUrl(apiKey, userId) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/sso`, { headers: { 'X-API-KEY': apiKey } } ); return response.data.url; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage getSsoUrl('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(url => { console.log('SSO URL (valid for 30 seconds):', url); }) .catch(error => console.error('Failed to get SSO URL:', error.message)); ``` ```json Response theme={null} { "url": "https://YOURSITE.konstant.ly/sso/login/abcdef123456789" } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Get User Statistics Source: https://docs.konstantly.com/api-reference/users/user-statistics GET /users/{userId}/statistics Get detailed statistics for a specific user Retrieve comprehensive statistics about a user's learning activities, including course progress, test results, and performance metrics. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## URL Parameters User API ID ## Response General course statistics Number of courses created by user Number of courses published by user Number of courses shared with user Number of courses assigned to user Number of courses started by user Number of courses completed by user Average progress across all courses Array of not started courses Course information Assignment deadline timestamp Array of started courses Course information Current progress (0-100) Current course element Element ID Element name Element type Last progress update timestamp Assignment deadline timestamp Course start status Course completion status Distribution of course results Number of courses with 0% result Courses with 1-10% result Courses with 11-20% result Courses with 21-30% result Courses with 31-40% result Courses with 41-50% result Courses with 51-60% result Courses with 61-70% result Courses with 71-80% result Courses with 81-90% result Courses with 91-99% result Number of courses with 100% result Performance statistics by tag Tag identifier Tag name Average result for this tag (0-100) ### Error Responses Not Found error response HTTP status code (404) Error message ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/users/1234567890/statistics' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests def get_user_statistics(api_key: str, user_id: str) -> dict: url = f"https://YOURSITE.konstant.ly/openapi/v1/users/{user_id}/statistics" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"User {user_id} not found") response.raise_for_status() return response.json() # Example usage try: stats = get_user_statistics("1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "1234567890") # Print general statistics print("Course Statistics:") print(f"Started: {stats['courses']['startedCoursesCount']}") print(f"Finished: {stats['courses']['finishedCoursesCount']}") print(f"Average Result: {stats['courses']['averageResultValue']}%") # Print in-progress courses print("\nIn Progress Courses:") for course in stats['started']: print(f"Course: {course['course']['name']}") print(f"Progress: {course['progressValue']}%") print(f"Current Element: {course['courseElement']['name']}") print("---") # Print tag performance print("\nPerformance by Tag:") for tag in stats['tags']: print(f"{tag['tag']}: {tag['resultValue']}%") except Exception as e: print(f"Error: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getUserStatistics(apiKey, userId) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/users/${userId}/statistics`, { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`User ${userId} not found`); } throw error; } } // Example usage getUserStatistics('1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', '1234567890') .then(stats => { // Print general statistics console.log('Course Statistics:'); console.log(`Started: ${stats.courses.startedCoursesCount}`); console.log(`Finished: ${stats.courses.finishedCoursesCount}`); console.log(`Average Result: ${stats.courses.averageResultValue}%`); // Print in-progress courses console.log('\nIn Progress Courses:'); stats.started.forEach(course => { console.log(`Course: ${course.course.name}`); console.log(`Progress: ${course.progressValue}%`); console.log(`Current Element: ${course.courseElement.name}`); console.log('---'); }); // Print tag performance console.log('\nPerformance by Tag:'); stats.tags.forEach(tag => { console.log(`${tag.tag}: ${tag.resultValue}%`); }); }) .catch(error => console.error('Error:', error.message)); ``` ```json Response theme={null} { "courses": { "sharedCoursesCount": 5, "assignedCoursesCount": 10, "startedCoursesCount": 8, "finishedCoursesCount": 6, "averageResultValue": 85 }, "virgin": [ { "course": { "id": 123, "name": "Introduction to Sales", "annotation": "Basic sales concepts", "isDraft": false }, "deadlineAt": 1704067200 } ], "started": [ { "course": { "id": 124, "name": "Advanced Sales Techniques", "annotation": "Advanced sales strategies", "isDraft": false }, "progressValue": 75, "courseElement": { "id": 456, "name": "Customer Psychology", "type": "page" }, "updatedAt": 1673531200, "deadlineAt": 1704067200, "isCourseStarted": true, "isCourseFinished": false } ], "results": { "0": 2, "1-10": 0, "11-20": 1, "21-30": 0, "31-40": 1, "41-50": 0, "51-60": 1, "61-70": 2, "71-80": 1, "81-90": 1, "91-99": 0, "100": 1 }, "tags": [ { "tagId": 1, "tag": "Sales", "resultValue": 85 }, { "tagId": 2, "tag": "Communication", "resultValue": 90 } ] } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Course Webhook Events Source: https://docs.konstantly.com/api-reference/webhooks/course-webhooks Course-related webhook events and payloads Course webhooks provide real-time notifications about course-related activities and state changes. ## Course/Published Triggered when a course is published and becomes available to users. ### Payload Published course information Course ID Course name Course description Always false for published Creation timestamp Last update timestamp Publication timestamp Course cover image ## Course/Unpublished Triggered when a course is unpublished and becomes unavailable to users. ### Payload Unpublished course information Course ID Course name Course description Draft status Creation timestamp Last update timestamp Publication timestamp Course cover image ## Course/Assigned Triggered when a course is assigned to a user. ### Payload Course information (same structure as Course/Published) User the course was assigned to User ID User's name User's email User's role Assignment timestamp Deadline timestamp ## Course/Started Triggered when a user starts a course. ### Payload Course information User information Assignment timestamp Deadline timestamp Timestamp when user started the course Timestamp when user finished (0 if not finished) ## Course/Progress Triggered when a user makes progress in a course. ### Payload Course information User information Current progress (0-100) Current course element Element ID Element name Element type ## Course/Finished Triggered when a user completes a course. ### Payload Course information User information Final result value (0-100) ```json Course/Published theme={null} { "occuredAt": 1673531200, "type": "Course/Published", "body": { "course": { "id": 123, "name": "Sales Training", "annotation": "Comprehensive sales course", "isDraft": false, "createdAt": 1673444800, "updatedAt": 1673531100, "publishedAt": 1673531200 } } } ``` ```json Course/Unpublished theme={null} { "occuredAt": 1673531250, "type": "Course/Unpublished", "body": { "course": { "id": 123, "name": "Sales Training", "annotation": "Comprehensive sales course", "isDraft": true, "createdAt": 1673444800, "updatedAt": 1673531250, "publishedAt": 1673531200 } } } ``` ```json Course/Assigned theme={null} { "occuredAt": 1673531300, "type": "Course/Assigned", "body": { "course": { "id": 123, "name": "Sales Training" }, "user": { "id": "user123", "name": "John Smith", "email": "john@example.com", "role": { "alias": "learner" } }, "assignedAt": 1673531300, "deadlineAt": 1674736000 } } ``` ```json Course/Started theme={null} { "occuredAt": 1673532000, "type": "Course/Started", "body": { "course": { "id": 123, "name": "Sales Training" }, "user": { "id": "user123", "name": "John Smith", "email": "john@example.com" }, "assignedAt": 1673531300, "deadlineAt": 1674736000, "startedAt": 1673532000, "finishedAt": 0 } } ``` ```json Course/Progress theme={null} { "occuredAt": 1673535000, "type": "Course/Progress", "body": { "course": { "id": 123, "name": "Sales Training" }, "user": { "id": "user123", "name": "John Smith" }, "progressValue": 45, "courseElement": { "id": 456, "name": "Module 3: Advanced Techniques", "type": "page" } } } ``` ```json Course/Finished theme={null} { "occuredAt": 1673628000, "type": "Course/Finished", "body": { "course": { "id": 123, "name": "Sales Training" }, "user": { "id": "user123", "name": "John Smith" }, "resultValue": 95 } } ``` # Get Last Event Source: https://docs.konstantly.com/api-reference/webhooks/get-last-event GET /webhook/:event/last Get data of last webhook event of a specific type Retrieve the most recent occurrence of a specified webhook event type. This endpoint is useful for testing webhook integrations and verifying event data structures. ## Request Headers API Key. Go to your Konstantly site > Settings > API and copy the value from there. ## Query Parameters Type of webhook event to retrieve Example: `user.created` ## Response Returns the full webhook event data structure for the most recent event of the specified type. ### Error Responses Not Found error response HTTP status code (404) Error message ## Usage Notes 1. Event Types: * Use dot notation for event types (e.g., `user.created`, `course.published`) * Case sensitive matching * Only returns events from the last 30 days 2. Common Uses: * Testing webhook handlers * Debugging integration issues * Verifying event structures * Recreating missed events 3. Best Practices: * Cache responses when appropriate * Handle "no events" cases gracefully * Use for testing, not production monitoring * Verify event data matches expectations ```bash cURL theme={null} curl --request GET \ --url 'https://YOURSITE.konstant.ly/openapi/v1/webhook/user.created/last' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} import requests from typing import Dict, Any def get_last_event(api_key: str, event_type: str) -> Dict[str, Any]: url = f"https://YOURSITE.konstant.ly/openapi/v1/webhook/{event_type}/last" headers = {"X-API-KEY": api_key} response = requests.get(url, headers=headers) if response.status_code == 404: raise ValueError(f"No recent events found for type: {event_type}") response.raise_for_status() return response.json() # Example usage try: # Get last user creation event last_user_event = get_last_event( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "user.created" ) # Extract user information user = last_user_event['body']['user'] print(f"Last created user: {user['name']} ({user['email']})") print(f"Created at: {last_user_event['occuredAt']}") # Get last course completion event last_completion = get_last_event( "1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv", "course.finished" ) # Extract completion details completion = last_completion['body'] print(f"\nLast completion:") print(f"User: {completion['user']['name']}") print(f"Course: {completion['course']['name']}") print(f"Result: {completion['resultValue']}%") except ValueError as e: print(f"Error: {str(e)}") except Exception as e: print(f"Error fetching event: {str(e)}") ``` ```javascript Node.js theme={null} const axios = require('axios'); async function getLastEvent(apiKey, eventType) { try { const response = await axios.get( `https://YOURSITE.konstant.ly/openapi/v1/webhook/${eventType}/last`, { headers: { 'X-API-KEY': apiKey } } ); return response.data; } catch (error) { if (error.response?.status === 404) { throw new Error(`No recent events found for type: ${eventType}`); } throw error; } } // Example usage async function checkRecentEvents() { try { // Get last user creation event const lastUserEvent = await getLastEvent( '1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', 'user.created' ); const user = lastUserEvent.body.user; console.log(`Last created user: ${user.name} (${user.email})`); console.log(`Created at: ${lastUserEvent.occuredAt}`); // Get last course completion event const lastCompletion = await getLastEvent( '1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv', 'course.finished' ); const completion = lastCompletion.body; console.log('\nLast completion:'); console.log(`User: ${completion.user.name}`); console.log(`Course: ${completion.course.name}`); console.log(`Result: ${completion.resultValue}%`); } catch (error) { console.error('Error:', error.message); } } checkRecentEvents(); ``` ```json User Created Event theme={null} { "occuredAt": 1673531200, "type": "User/Created", "body": { "user": { "id": "user123", "name": "John Smith", "email": "john@example.com", "language": "en", "timezone": "Europe/London", "role": { "alias": "learner" }, "fromApi": true, "userAttributes": [ { "apiId": "department", "name": "Department", "type": 1, "value": "Sales" } ] } } } ``` ```json 404 Error theme={null} { "status": 404, "message": "Not found" } ``` # Group Webhooks Source: https://docs.konstantly.com/api-reference/webhooks/group-webhooks Understanding Group-related webhook events Group webhooks notify you about changes related to group membership and updates. These events help you track when users are added to groups and when group information changes. ## Event Types Triggered when a user is added to a group ## Common Properties All group webhook events share these common properties: Timestamp when the event occurred Event type identifier Event-specific payload data ## Group/User/Created Event This event is triggered whenever a user is added to a group. ### Payload Structure Details about the user being added User ID User's full name User's email address User's interface language User's timezone User's role information Custom user attributes Details about the group Group ID Group name Group description Group cover image details ## Implementation Examples ```bash cURL theme={null} # Test receiving a Group/User/Created webhook curl --location 'https://your-server.com/webhook' \ --header 'Content-Type: application/json' \ --data '{ "type": "Group/User/Created", "occuredAt": 1508225229, "body": { "user": { "id": "1234567890", "name": "John Doe", "email": "john@example.com", "language": "en", "timezone": "Europe/London", "role": { "alias": "learner" }, "userAttributes": { "department": "Sales", "employeeId": "EMP123" } }, "group": { "id": 123, "name": "Sales Team", "description": "Sales department team members" } } }' # Retrieve last group webhook event curl --location 'https://YOURSITE.konstant.ly/openapi/v1/webhook/group.user.created/last' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/webhook', methods=['POST']) def handle_webhook(): # Get webhook data event_data = request.json # Extract common properties event_type = event_data['type'] occurred_at = event_data['occuredAt'] payload = event_data['body'] # Handle group webhook events if event_type == 'Group/User/Created': handle_group_user_created(payload) # Return success quickly return jsonify({'status': 'success'}), 200 def handle_group_user_created(payload): user = payload['user'] group = payload['group'] print(f"User added to group: {user['name']} (ID: {user['id']}) -> {group['name']} (ID: {group['id']})") # Add your group membership handling logic here # For example: # - Sync with other systems # - Update internal user records # - Send notifications # - Trigger automated workflows if __name__ == '__main__': app.run(port=5000) ``` ```javascript Node.js theme={null} const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhook', (req, res) => { // Get webhook data const { type, occuredAt, body } = req.body; // Handle group webhook events try { if (type === 'Group/User/Created') { handleGroupUserCreated(body); } // Return success quickly res.status(200).json({ status: 'success' }); } catch (error) { console.error('Error processing webhook:', error); res.status(500).json({ error: 'Internal server error' }); } }); function handleGroupUserCreated(payload) { const { user, group } = payload; console.log(`User added to group: ${user.name} (ID: ${user.id}) -> ${group.name} (ID: ${group.id})`); // Add your group membership handling logic here // For example: // - Sync with other systems // - Update internal user records // - Send notifications // - Trigger automated workflows } const PORT = process.env.PORT || 5000; app.listen(PORT, () => { console.log(`Webhook server running on port ${PORT}`); }); ``` ```php PHP theme={null} 'success']); } catch (Exception $e) { error_log("Error processing webhook: " . $e->getMessage()); http_response_code(500); echo json_encode(['error' => 'Internal server error']); } } function handleGroupUserCreated($payload) { $user = $payload['user']; $group = $payload['group']; error_log("User added to group: {$user['name']} (ID: {$user['id']}) -> {$group['name']} (ID: {$group['id']})"); // Add your group membership handling logic here // For example: // - Sync with other systems // - Update internal user records // - Send notifications // - Trigger automated workflows } // Handle the webhook handleWebhook(); ?> ``` ## Best Practices 1. **Event Processing** * Process events asynchronously to ensure quick response * Implement idempotency to handle potential duplicate events * Store raw event data for debugging and auditing 2. **Error Handling** * Implement proper error handling and logging * Set up monitoring for failed webhook processing * Handle network timeouts and retries appropriately 3. **Security** * Validate webhook source using API keys or signatures * Use HTTPS endpoints for receiving webhooks * Implement rate limiting and request validation 4. **Integration Tips** * Update related systems when group membership changes * Maintain audit logs of group membership changes * Consider implementing event queuing for reliable processing # Webhooks Overview Source: https://docs.konstantly.com/api-reference/webhooks/overview Understanding and using Konstantly webhooks Webhooks allow you to receive real-time notifications about events happening in your Konstantly platform. Each webhook event contains detailed information about what happened and the relevant resources. ## Event Types ### Course Events Triggered when a course is published Triggered when a course is unpublished Triggered when a course is assigned to a user Triggered when a user starts a course Triggered when a user makes progress in a course Triggered when a user completes a course ### User Events Triggered when a new user is created Triggered when user information is updated ### Group Events Triggered when a user is added to a group ## Common Properties All webhook events share these common properties: Timestamp when the event occurred Event type identifier Event-specific payload data ## Best Practices 1. Webhook Handling: * Implement proper error handling * Use retry logic for failed deliveries * Process webhooks asynchronously * Validate webhook signatures * Respond quickly (2xx status) 2. Security: * Use HTTPS endpoints * Validate event data * Keep endpoint URLs secret * Monitor for unusual patterns 3. Implementation Tips: * Store raw webhook data * Process events idempotently * Set reasonable timeouts * Log webhook failures ## Testing Webhooks 1. Event History: * Use GET /webhook/:event/last to fetch recent events * Test handling with real event data * Verify payload processing 2. Development: * Set up test environment endpoints * Use event simulator when available * Test all event types * Verify error handling ```bash cURL theme={null} # Test receiving a webhook event curl --location 'https://your-server.com/webhook' \ --header 'Content-Type: application/json' \ --data '{ "type": "Course/Published", "occuredAt": 1508225229, "body": { "course": { "id": 123, "name": "Advanced JavaScript Course", "annotation": "Learn advanced JavaScript concepts", "isDraft": false, "createdAt": 1507807711, "updatedAt": 1507808372, "publishedAt": 1508225229 } } }' # Retrieve last webhook event curl --location 'https://YOURSITE.konstant.ly/openapi/v1/webhook/course.published/last' \ --header 'X-API-KEY: 1qaz2wsx3edc4rfv1qaz2wsx3edc4rfv' ``` ```python Python theme={null} from flask import Flask, request, jsonify import hmac import hashlib app = Flask(__name__) @app.route('/webhook', methods=['POST']) def handle_webhook(): # Get webhook data event_data = request.json # Extract common properties event_type = event_data['type'] occurred_at = event_data['occuredAt'] payload = event_data['body'] # Handle different event types if event_type == 'Course/Published': handle_course_published(payload) elif event_type == 'User/Created': handle_user_created(payload) elif event_type == 'Course/Progress': handle_course_progress(payload) # Return success quickly return jsonify({'status': 'success'}), 200 def handle_course_published(payload): course = payload['course'] print(f"Course published: {course['name']} (ID: {course['id']})") # Add your course publication handling logic here def handle_user_created(payload): user = payload['user'] print(f"New user: {user['name']} (Email: {user['email']})") # Add your user creation handling logic here def handle_course_progress(payload): user = payload['user'] course = payload['course'] progress = payload['progressValue'] print(f"Progress update: User {user['name']} - {progress}% in {course['name']}") # Add your progress handling logic here if __name__ == '__main__': app.run(port=5000) ``` ```javascript Node.js theme={null} const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhook', (req, res) => { // Get webhook data const { type, occuredAt, body } = req.body; // Handle different event types try { switch (type) { case 'Course/Published': handleCoursePublished(body); break; case 'User/Created': handleUserCreated(body); break; case 'Course/Progress': handleCourseProgress(body); break; default: console.log(`Unhandled event type: ${type}`); } // Return success quickly res.status(200).json({ status: 'success' }); } catch (error) { console.error('Error processing webhook:', error); res.status(500).json({ error: 'Internal server error' }); } }); function handleCoursePublished(payload) { const { course } = payload; console.log(`Course published: ${course.name} (ID: ${course.id})`); // Add your course publication handling logic here } function handleUserCreated(payload) { const { user } = payload; console.log(`New user: ${user.name} (Email: ${user.email})`); // Add your user creation handling logic here } function handleCourseProgress(payload) { const { user, course, progressValue } = payload; console.log(`Progress update: User ${user.name} - ${progressValue}% in ${course.name}`); // Add your progress handling logic here } const PORT = process.env.PORT || 5000; app.listen(PORT, () => { console.log(`Webhook server running on port ${PORT}`); }); ``` ```php PHP theme={null} 'success']); } catch (Exception $e) { error_log("Error processing webhook: " . $e->getMessage()); http_response_code(500); echo json_encode(['error' => 'Internal server error']); } } function handleCoursePublished($payload) { $course = $payload['course']; error_log("Course published: {$course['name']} (ID: {$course['id']})"); // Add your course publication handling logic here } function handleUserCreated($payload) { $user = $payload['user']; error_log("New user: {$user['name']} (Email: {$user['email']})"); // Add your user creation handling logic here } function handleCourseProgress($payload) { $user = $payload['user']; $course = $payload['course']; $progress = $payload['progressValue']; error_log("Progress update: User {$user['name']} - {$progress}% in {$course['name']}"); // Add your progress handling logic here } // Handle the webhook handleWebhook(); ?> ``` # User Webhook Events Source: https://docs.konstantly.com/api-reference/webhooks/user-webhooks User-related webhook events and payloads User webhooks provide real-time notifications about user-related activities and changes. ## User/Created Triggered when a new user is created in the system. ### Payload New user information User ID Full name Interface language Job title Location Timezone Email address Ban status User role API creation flag User avatar Custom attributes ## User/Edited Triggered when user information is updated. ### Payload Updated user information (same structure as User/Created) ```json User/Created theme={null} { "occuredAt": 1673531200, "type": "User/Created", "body": { "user": { "id": "user123", "name": "John Smith", "language": "en", "occupation": "Sales Manager", "location": "London", "timezone": "Europe/London", "email": "john@example.com", "isBanned": false, "role": { "alias": "learner" }, "fromApi": true, "image": null, "userAttributes": [ { "apiId": "department", "name": "Department", "type": 1, "value": "Sales" } ] } } } ``` ```json User/Edited theme={null} { "occuredAt": 1673531300, "type": "User/Edited", "body": { "user": { "id": "user123", "name": "John Smith", "language": "en", "occupation": "Senior Sales Manager", "location": "Manchester", "timezone": "Europe/London", "email": "john@example.com", "isBanned": false, "role": { "alias": "learner" }, "fromApi": true, "image": null, "userAttributes": [ { "apiId": "department", "name": "Department", "type": 1, "value": "Sales" } ] } } } ``` # Zapier Actions Source: https://docs.konstantly.com/api-reference/zapier/actions Available Konstantly actions in Zapier Actions are operations that your Zap can perform in Konstantly. These actions allow you to create and modify data in your Konstantly account based on triggers from other apps. ## Available Actions ### Create User Creates a new user in Konstantly. User's full name User's email address User role (e.g., "learner", "manager") User interface language User's timezone Custom user attributes ```json theme={null} { "name": "Jane Smith", "email": "jane@example.com", "roleAlias": "learner", "language": "en", "timezone": "Europe/London", "customAttributes": { "department": "Sales", "employee_id": "EMP123" } } ``` ### Update User Updates an existing user's information. User ID User's email address Updated name Updated custom attributes ### Create Group Creates a new group in Konstantly. Group name Parent group ID Group description Custom group attributes ### Add User to Group Adds a user to an existing group. User ID Group ID ### Assign Course Assigns a course to users or groups. Course ID Array of user IDs Array of group IDs Assignment deadline timestamp ```json theme={null} { "courseId": 123, "users": ["user_123", "user_124"], "groups": [1, 2], "deadlineAt": 1643673600 } ``` ## Using Actions ### Best Practices 1. **Data Mapping** * Map fields from previous steps to action inputs * Use Zapier's formatting tools to transform data if needed 2. **Error Handling** * Add error handling steps after actions * Use Zapier's retry options for failed actions 3. **Testing** * Test actions with sample data before activating the Zap * Verify the results in your Konstantly account ### Example Workflows 1. **New Employee Onboarding** ```plaintext theme={null} New employee in HR system → Create User in Konstantly → Add User to Department Group → Assign Onboarding Courses ``` 2. **Course Assignment Automation** ```plaintext theme={null} New course published → Get users from group → Assign course to users → Send notification email ``` 3. **User Group Management** ```plaintext theme={null} Department change in HR system → Update User in Konstantly → Remove from old group → Add to new group ``` # Custom Attributes in Zapier Source: https://docs.konstantly.com/api-reference/zapier/custom-attributes Learn how custom attributes work in Zapier integration Custom attributes in the Konstantly Zapier integration allow you to map and sync custom fields between different systems. The integration supports various attribute types and configurations. ## Supported Attribute Types ### Basic Types Single line text field. Used for short text inputs. Multi-line text field. Used for longer text content. Numeric field that supports decimal values. Date field in ISO format (YYYY-MM-DDThh:mm:ss+00:00). True/false field. ### Advanced Types Single-select dropdown field with predefined options. ```json theme={null} { "type": "unicode", "key": "department", "label": "Department", "choices": ["Sales", "Marketing", "Support"] } ``` Multi-select field that allows multiple values. ```json theme={null} { "type": "unicode", "key": "skills", "label": "Skills", "choices": ["JavaScript", "Python", "Java"], "list": true } ``` ## User Attributes User attributes are available in both triggers and actions related to user operations. ```json User Attribute Example theme={null} { "type": "unicode", "key": "employee_id", "label": "Employee ID", "required": true } ``` ## Group Attributes Group attributes can be used when creating or updating groups. ```json Group Attribute Example theme={null} { "type": "unicode", "key": "department_code", "label": "Department Code", "required": false } ``` ## Course Attributes Course-related custom attributes are available in course triggers and actions. ```json Course Attribute Example theme={null} { "type": "unicode", "key": "course_category", "label": "Course Category", "choices": ["Technical", "Soft Skills", "Compliance"] } ``` ## Working with Custom Attributes When setting up Zaps, custom attributes will appear as dynamic fields based on your Konstantly configuration. You can: 1. Map attributes from other apps to Konstantly fields 2. Use attribute values in filters and conditions 3. Transform data between different formats using Zapier's built-in tools # Zapier Integration Overview Source: https://docs.konstantly.com/api-reference/zapier/overview Get started with Konstantly Zapier integration The Konstantly Zapier integration allows you to connect your Konstantly account with thousands of other apps, enabling powerful automation workflows. With our Zapier integration, you can automate repetitive tasks, sync data between systems, and create custom workflows without any coding. ## Prerequisites * A Konstantly account with API access enabled * A Zapier account * API key from your Konstantly account (found in Settings > API) ## Getting Started 1. Install the Konstantly app from the Zapier marketplace 2. Connect your Konstantly account by providing your API key 3. Start creating Zaps using Konstantly triggers and actions ## Key Features * Real-time synchronization of data between Konstantly and other apps * Support for custom attributes in both triggers and actions * Automatic handling of authentication and data mapping * Built-in error handling and retry mechanisms ## Use Cases Common automation scenarios you can implement: 1. **User Management** * Automatically create users in Konstantly when new employees are added to your HR system * Sync user groups between Konstantly and your organization directory 2. **Course Administration** * Automatically assign courses based on user roles or departments * Track course completion in external systems 3. **Reporting** * Send course completion notifications to Slack or Microsoft Teams * Log learning progress in spreadsheets or databases 4. **Integration** * Connect Konstantly with your CRM system * Sync data with project management tools # Zapier Triggers Source: https://docs.konstantly.com/api-reference/zapier/triggers Available Konstantly triggers in Zapier Triggers are events that occur in Konstantly that can start your Zaps. Each trigger provides specific data that can be used in subsequent steps of your Zap. ## Available Triggers ### New Course Published Triggers when a new course is published in Konstantly. Course ID Course name Course description Publication timestamp Author information ### Course Assigned Triggers when a course is assigned to a user or group. Course ID Course name User or group details Assignment timestamp Assignment deadline ### Course Completed Triggers when a user completes a course. Course ID Course name User information Completion timestamp Final score (0-100) ### New User Created Triggers when a new user is created in Konstantly. User ID User's full name User's email User's role Custom user attributes ### User Added to Group Triggers when a user is added to a group. User ID User's name Group ID Group name Assignment timestamp ## Using Triggers Each trigger can be used as the first step in your Zap. You can: 1. Filter trigger events based on specific conditions 2. Use trigger data in subsequent steps 3. Test triggers with sample data while setting up your Zap ### Example Usage ```json Sample Course Completion Trigger theme={null} { "courseId": 123, "courseName": "Advanced JavaScript", "user": { "id": "user_123", "name": "John Doe", "email": "john@example.com" }, "completedAt": 1643673600, "score": 95 } ``` # Introduction Source: https://docs.konstantly.com/introduction Welcome to the Konstantly API documentation # Konstantly API Welcome to the Konstantly API documentation. This API allows you to programmatically manage users, courses, groups, certificates, and more in your Konstantly learning management platform. This is a beta version of the API documentation. Report any errors to [support@konstantly.com](mailto:support@konstantly.com) ## Base URL All API requests should be made to: ```bash theme={null} https://YOURSITE.konstant.ly/openapi/v1 ``` Replace `YOURSITE` with your Konstantly subdomain. ## Authentication The Konstantly API uses API keys to authenticate requests. Include your API key in the `X-API-KEY` header: ```bash theme={null} X-API-KEY: your-api-key-here ``` To get your API key: 1. Go to your Konstantly site 2. Navigate to **Settings > API** 3. Copy your API key Keep your API keys secure. Never share them in publicly accessible areas such as GitHub or client-side code. ## Request Format The API accepts JSON-encoded request bodies and returns JSON-encoded responses. Include these headers: ```bash theme={null} Content-Type: application/json Accept: application/json ``` ## Pagination Many endpoints return paginated results. By default, endpoints return 20 items per page. Use the `offset` parameter to paginate: | Parameter | Type | Description | | --------- | ------- | ----------------------- | | `offset` | integer | Number of items to skip | ## Support If you have questions or need help with API integration, contact us at [support@konstantly.com](mailto:support@konstantly.com).