# 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 IDCertificate nameCertificate descriptionCourse IDCourse nameURL to the certificate imageTimestamp of certificate issueTimestamp when the certificate expires
User who has got the certificate
User IDFull nameInterface language (de, en, es, fr, pl, pt, ru)Job title (optional)User location (optional)User timezone in TZ formatEmail addressUser ban statusUser role informationWhether created via APIUser avatar informationCustom 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 IDUser's full nameUser's emailUser's role information
Groups the course was successfully assigned to
Group IDGroup nameNumber of users in the groupNumber 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 assignedExact 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 IDParent group IDName of the groupNumber of users in the groupNumber 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 assignedTimestamp when user started the courseAssignment deadline timestampTimestamp when user finished the courseCurrent progress (0-100)Result value (0-100)Whether user has started the courseWhether user has finished the courseWhether the assignment has expired
User details
User IDFull nameEmail addressUser 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 IDCertificate nameCertificate descriptionCourse IDCourse nameURL to the certificate imageTimestamp of certificate issueTimestamp when certificate expires
User who received the certificate
User IDFull nameEmail addressUser 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 usersNumber of courses assigned to usersNumber of users who started the courseNumber of users who finished the courseAverage progress across all users
Course test statistics
Test IDTest nameNumber of questionsNumber of users taken the testTotal number of attemptsNumber of successful completionsAverage test result
Users distribution by progress ranges
Users with 0% progressUsers with 1-10% progressUsers with 11-20% progressUsers with 21-30% progressUsers with 31-40% progressUsers with 41-50% progressUsers with 51-60% progressUsers with 61-70% progressUsers with 71-80% progressUsers with 81-90% progressUsers with 91-99% progressUsers with 100% progress
Question tag statistics
Tag IDTag nameAverage 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 IDCourse nameCourse annotationIndicates whether the course is a draftCreation timestampLast update timestampPublication timestamp
Course cover image details
Image IDURL to full-size imageURL to image 36x36 pxURL to image 330x130 pxCourse description
Attached files
File nameFile size in bytesDownload URLMIME 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 IDCourse nameCourse annotationIndicates whether the course is a draftCreation timestampLast update timestampPublication timestamp
Course cover image details
Image IDURL to full-size imageURL to image 36x36 pxURL 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 IDItem positionItem 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 IDItem positionItem 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 positionItem 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 messageField-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 optionDisplay 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 messageValidation 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 IDParent group IDName of the groupGroup descriptionNumber of users in the groupNumber of courses assigned to the group
Group cover image details
Image IDURL to full-size imageURL to image 36x36 pxURL to image 330x130 px
### Error Responses
Returned when the request is invalid
HTTP status code (400)Error messageValidation 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 IDParent group IDName of the groupGroup descriptionNumber of users in the groupNumber of courses assigned to the group
Group cover image details
Image IDURL to full-size imageURL to image 36x36 pxURL 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 IDParent group ID (optional)Name of the groupGroup descriptionNumber of users in the groupNumber of courses assigned to the group
Group cover image details
Image IDURL to full-size imageURL to image 36x36 pxURL 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 IDCourse nameCourse annotationWhether the course is a draftCreation timestampLast update timestampPublication 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
```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 IDGroup nameParent group ID if applicableNumber of users in groupNumber of courses assigned to groupGroup image informationCustom group attributes
List of users with no activity in the group
User identifierFull nameEmail addressUser role
List of users who haven't completed any assessments
User identifierFull nameEmail addressUser role
Top performing users in the group
Number of completed coursesAverage 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
```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 messageValidation 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
```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 messageValidation 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 IDRecipient email addressCreation timestampSignup 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 IDEmail invitation was sent toTimestamp when invitation was sentURL 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 IDEmail invitation was sent toTimestamp when invitation was sentURL 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 coursesNumber of course assignmentsTotal number of groupsTotal number of users
Array of users without any activity
User IDUser's full nameInterface languageJob titleUser locationUser timezoneUser's emailUser ban statusUser roleWhether user was created via APIUser's avatar
Array of users who haven't completed any courses
Same structure as usersWithoutActivity
Platform's top performers
Number of completed coursesAverage result (0-100)User details
Courses not assigned to any users
Course IDCourse nameCourse descriptionDraft statusCreation timestampLast update timestampPublication timestampCourse 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 IDFull nameInterface languageEmail addressWill be `true` after deactivationUser roleAPI 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 bannedAdditional 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 IDFull nameInterface language (de, en, es, fr, pl, pt, ru)Job titleLocationTimezoneEmail addressBan statusUser roleAPI management status
User avatar
Image IDFull-size image URL36x36px image URL58x58px image URL128x128px image URLCustom attributes
### Error Responses
Returned when request validation fails
HTTP status code (400)Error messageField-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 assignedTimestamp when user started the courseTimestamp when assignment expiresTimestamp when user finished the courseCurrent progress (0-100)Course result (0-100)Whether user has started the courseWhether user has finished the courseWhether assignment has expired
Course details
Course IDCourse nameCourse annotationDraft statusCreation timestampLast update timestampPublication timestampCourse 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 IDFull nameInterface language (de, en, es, fr, pl, pt, ru)Job titleLocationTimezoneEmail addressBan statusUser roleAPI management status
User avatar
Image IDFull-size image URL36x36px image URL58x58px image URL128x128px image URLCustom attributes
### Error Responses
Returned for validation errors
HTTP status code (400)Error messageField-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 identifierFull name of the userInterface language (de, en, es, fr, pl, pt, ru)Job titleLocationTimezone in TZ formatEmail addressWhether user is banned from loginUser role alias/identifierWhether user is managed via API only
User avatar
Image IDFull-size image URL36x36px image URL58x58px image URL128x128px image URLCustom 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 IDParent group IDGroup nameNumber of users in the groupNumber of courses assigned to the group
Group cover image
Image IDFull-size image URL36x36px image URL330x130px image URLCustom 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 identifierFull name of the userInterface and notification language (one of: de, en, es, fr, pl, pt, ru)Optional job titleOptional locationTimezone in TZ formatUser email (unique across platform)Whether user is banned from loginUser role alias/identifierWhether user is managed via API only
User avatar details
Image IDURL to full-size imageURL to 36x36px imageURL to 58x58px imageURL to 128x128px imageCustom 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 assignedTimestamp when user started the courseTimestamp when assignment expiresTimestamp when user finished the courseCurrent progress (0-100)Course result (0-100)Whether user has started the courseWhether user has finished the courseWhether assignment has expiredCourse details
### Error Responses
Validation error response
HTTP status code (400)Error messageField-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 messageField-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 IDCertificate nameCertificate descriptionAssociated course IDAssociated course nameURL to certificate imageAchievement timestampExpiration 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 IDParent group IDGroup nameNumber of users in groupNumber of assigned courses
Group image details
Image IDOriginal image URL36x36px thumbnail URL330x130px image URLCustom group attributes
### Error Responses
Validation error response
HTTP status code (400)Error messageValidation 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 userNumber of courses published by userNumber of courses shared with userNumber of courses assigned to userNumber of courses started by userNumber of courses completed by userAverage progress across all courses
Array of not started courses
Course informationAssignment deadline timestamp
Array of started courses
Course informationCurrent progress (0-100)
Current course element
Element IDElement nameElement typeLast progress update timestampAssignment deadline timestampCourse start statusCourse completion status
Distribution of course results
Number of courses with 0% resultCourses with 1-10% resultCourses with 11-20% resultCourses with 21-30% resultCourses with 31-40% resultCourses with 41-50% resultCourses with 51-60% resultCourses with 61-70% resultCourses with 71-80% resultCourses with 81-90% resultCourses with 91-99% resultNumber of courses with 100% result
Performance statistics by tag
Tag identifierTag nameAverage 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 IDCourse nameCourse descriptionAlways false for publishedCreation timestampLast update timestampPublication timestampCourse cover image
## Course/Unpublished
Triggered when a course is unpublished and becomes unavailable to users.
### Payload
Unpublished course information
Course IDCourse nameCourse descriptionDraft statusCreation timestampLast update timestampPublication timestampCourse 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 IDUser's nameUser's emailUser'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 IDElement nameElement 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 IDUser's full nameUser's email addressUser's interface languageUser's timezoneUser's role informationCustom user attributes
Details about the group
Group IDGroup nameGroup descriptionGroup 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 IDFull nameInterface languageJob titleLocationTimezoneEmail addressBan statusUser roleAPI creation flagUser avatarCustom 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 nameUser's email addressUser role (e.g., "learner", "manager")User interface languageUser's timezoneCustom 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 IDUser's email addressUpdated nameUpdated custom attributes
### Create Group
Creates a new group in Konstantly.
Group nameParent group IDGroup descriptionCustom group attributes
### Add User to Group
Adds a user to an existing group.
User IDGroup ID
### Assign Course
Assigns a course to users or groups.
Course IDArray of user IDsArray of group IDsAssignment 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 IDCourse nameCourse descriptionPublication timestampAuthor information
### Course Assigned
Triggers when a course is assigned to a user or group.
Course IDCourse nameUser or group detailsAssignment timestampAssignment deadline
### Course Completed
Triggers when a user completes a course.
Course IDCourse nameUser informationCompletion timestampFinal score (0-100)
### New User Created
Triggers when a new user is created in Konstantly.
User IDUser's full nameUser's emailUser's roleCustom user attributes
### User Added to Group
Triggers when a user is added to a group.
User IDUser's nameGroup IDGroup nameAssignment 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).