Typowy przepływ
Większość integracji z systemem kadrowym składa się z trzech kroków: synchronizacja osób, przydział szkoleń, odczyt stanu zgodności. Każdy krok działa na wartościach, które system kadrowy już zna — numerze pracownika, nazwie działu, kodzie szkolenia — więc nie musisz przechowywać naszych identyfikatorów.
0. Odczyt słowników (raz albo po zmianie)
curl "https://api.elearning.edu.pl/api/v1/departments" -H "X-Tenant-Code: your-tenant" -H "X-API-Key: $KEY"
curl "https://api.elearning.edu.pl/api/v1/course-catalog?assignable=true" -H "X-Tenant-Code: your-tenant" -H "X-API-Key: $KEY"GET /api/v1/departmentsiGET /api/v1/contractors(zakresusers:read) zwracają działy i firmy współpracowników z nazwami i identyfikatorami.GET /api/v1/course-catalog(zakresenrollments:writealbocompliance:read) zwraca szkolenia z ichcode;assignable: falseprzychodzi razem zunassignable_reason(inactive,path_only,license_expired).
1. Synchronizacja osób
Wyszukaj osobę po numerze kadrowym, utwórz ją, jeśli jej nie ma, zaktualizuj zmienione pola.
BASE_URL="${BASE_URL:-https://api.elearning.edu.pl}"
AUTH=(-H "X-Tenant-Code: $TENANT_CODE" -H "X-API-Key: $API_KEY")
# Values your HR system already knows: employee number, department name, course code.
EMPLOYEE_NO=E1001
DEPARTMENT_NAME=Logistics
COURSE_CODE=SAFETY-101
# 1. Look the person up by employee number; create them if missing.
USER_ID=$(curl --fail-with-body -sS "${AUTH[@]}" "$BASE_URL/api/v1/users?employee_no=$EMPLOYEE_NO" | jq -r '.users[0].id // empty')
if [ -z "$USER_ID" ]; then
curl --fail-with-body -sS "${AUTH[@]}" "$BASE_URL/api/v1/users" \
-H "Content-Type: application/json" \
-d "{\"employee_no\": \"$EMPLOYEE_NO\", \"first_name\": \"Anna\", \"last_name\": \"Example\",
\"email\": \"anna.example@example.com\", \"department_name\": \"$DEPARTMENT_NAME\"}" > /dev/null
fi
# 2. Assign a course for this year's cycle (409 = already assigned).
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "${AUTH[@]}" "$BASE_URL/api/v1/enrollments" \
-H "Content-Type: application/json" \
-d "{\"employee_no\": \"$EMPLOYEE_NO\", \"course_code\": \"$COURSE_CODE\", \"cycle_year\": 2026, \"due_date\": \"2026-12-31\"}")
case "$STATUS" in 201|409) ;; *) echo "enrollment failed: $STATUS" >&2; exit 1 ;; esac
# 3. Read the compliance matrix page by page.
LIMIT=100
OFFSET=0
while :; do
PAGE=$(curl --fail-with-body -sS "${AUTH[@]}" "$BASE_URL/api/v1/compliance/matrix?limit=$LIMIT&offset=$OFFSET")
echo "$PAGE" | jq -r '.rows[] | [.employee_no, .full_name, (.courses | to_entries | map(.key + "=" + .value.status) | join(","))] | @tsv'
[ "$(echo "$PAGE" | jq '.rows | length')" -lt "$LIMIT" ] && break
OFFSET=$((OFFSET + LIMIT))
doneimport os
import requests
BASE_URL = os.environ.get("BASE_URL", "https://api.elearning.edu.pl")
# Values your HR system already knows: employee number, department name, course code.
EMPLOYEE_NO = "E1001"
DEPARTMENT_NAME = "Logistics"
COURSE_CODE = "SAFETY-101"
session = requests.Session()
session.headers.update({"X-Tenant-Code": os.environ["TENANT_CODE"], "X-API-Key": os.environ["API_KEY"]})
def api(method, path, **kwargs):
return session.request(method, BASE_URL + path, timeout=30, **kwargs)
# 1. Look the person up by employee number; create them if missing.
found = api("GET", "/api/v1/users", params={"employee_no": EMPLOYEE_NO})
found.raise_for_status()
if not found.json()["users"]:
created = api("POST", "/api/v1/users", json={
"employee_no": EMPLOYEE_NO,
"first_name": "Anna",
"last_name": "Example",
"email": "anna.example@example.com",
"department_name": DEPARTMENT_NAME,
})
created.raise_for_status()
# 2. Assign a course for this year's cycle (409 = already assigned).
assigned = api("POST", "/api/v1/enrollments", json={
"employee_no": EMPLOYEE_NO,
"course_code": COURSE_CODE,
"cycle_year": 2026,
"due_date": "2026-12-31",
})
if assigned.status_code != 409:
assigned.raise_for_status()
# 3. Read the compliance matrix page by page.
limit, offset = 100, 0
while True:
page = api("GET", "/api/v1/compliance/matrix", params={"limit": limit, "offset": offset})
page.raise_for_status()
rows = page.json()["rows"]
for row in rows:
# courses: course code → status cell
print(row["employee_no"], row["full_name"], {code: cell["status"] for code, cell in row["courses"].items()})
if len(rows) < limit:
break
offset += limit<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Exception\ClientException;
$client = new GuzzleHttp\Client([
'base_uri' => getenv('BASE_URL') ?: 'https://api.elearning.edu.pl',
'timeout' => 30,
'headers' => [
'X-Tenant-Code' => getenv('TENANT_CODE'),
'X-API-Key' => getenv('API_KEY'),
],
]);
// Values your HR system already knows: employee number, department name, course code.
$employeeNo = 'E1001';
$departmentName = 'Logistics';
$courseCode = 'SAFETY-101';
function body(Psr\Http\Message\ResponseInterface $response): array
{
return json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
}
// 1. Look the person up by employee number; create them if missing.
$users = body($client->get('/api/v1/users', ['query' => ['employee_no' => $employeeNo]]))['users'];
if ($users === []) {
$client->post('/api/v1/users', ['json' => [
'employee_no' => $employeeNo,
'first_name' => 'Anna',
'last_name' => 'Example',
'email' => 'anna.example@example.com',
'department_name' => $departmentName,
]]);
}
// 2. Assign a course for this year's cycle (409 = already assigned).
try {
$client->post('/api/v1/enrollments', ['json' => [
'employee_no' => $employeeNo,
'course_code' => $courseCode,
'cycle_year' => 2026,
'due_date' => '2026-12-31',
]]);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() !== 409) {
throw $e;
}
}
// 3. Read the compliance matrix page by page.
$limit = 100;
for ($offset = 0; ; $offset += $limit) {
$rows = body($client->get('/api/v1/compliance/matrix', ['query' => ['limit' => $limit, 'offset' => $offset]]))['rows'];
foreach ($rows as $row) {
// courses: course code → status cell
$statuses = array_map(fn (array $cell): string => $cell['status'], $row['courses']);
echo $row['employee_no'], ' ', $row['full_name'], ' ', json_encode($statuses), PHP_EOL;
}
if (count($rows) < $limit) {
break;
}
}package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
var (
baseURL = env("BASE_URL", "https://api.elearning.edu.pl")
// Values your HR system already knows: employee number, department name, course code.
employeeNo = "E1001"
departmentName = "Logistics"
courseCode = "SAFETY-101"
)
func main() {
// 1. Look the person up by employee number; create them if missing.
var found struct {
Users []struct {
ID string `json:"id"`
} `json:"users"`
}
must(call("GET", "/api/v1/users?employee_no="+url.QueryEscape(employeeNo), nil, &found))
if len(found.Users) == 0 {
must(call("POST", "/api/v1/users", map[string]any{
"employee_no": employeeNo,
"first_name": "Anna",
"last_name": "Example",
"email": "anna.example@example.com",
"department_name": departmentName,
}, nil))
}
// 2. Assign a course for this year's cycle (409 = already assigned).
err := call("POST", "/api/v1/enrollments", map[string]any{
"employee_no": employeeNo,
"course_code": courseCode,
"cycle_year": 2026,
"due_date": "2026-12-31",
}, nil)
if apiErr, ok := err.(*apiError); err != nil && !(ok && apiErr.Status == http.StatusConflict) {
panic(err)
}
// 3. Read the compliance matrix page by page.
const limit = 100
for offset := 0; ; offset += limit {
var page struct {
Rows []struct {
EmployeeNo *string `json:"employee_no"`
FullName string `json:"full_name"`
// Course code → status cell.
Courses map[string]struct {
Status string `json:"status"`
} `json:"courses"`
} `json:"rows"`
}
query := url.Values{"limit": {fmt.Sprint(limit)}, "offset": {fmt.Sprint(offset)}}
must(call("GET", "/api/v1/compliance/matrix?"+query.Encode(), nil, &page))
for _, row := range page.Rows {
fmt.Println(row.FullName, row.Courses)
}
if len(page.Rows) < limit {
break
}
}
}
type apiError struct {
Status int
Body string
}
func (e *apiError) Error() string { return fmt.Sprintf("%d: %s", e.Status, e.Body) }
func call(method, path string, body any, out any) error {
var reader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return err
}
reader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, baseURL+path, reader)
if err != nil {
return err
}
req.Header.Set("X-Tenant-Code", os.Getenv("TENANT_CODE"))
req.Header.Set("X-API-Key", os.Getenv("API_KEY"))
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
data, _ := io.ReadAll(res.Body)
return &apiError{Status: res.StatusCode, Body: string(data)}
}
if out == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(out)
}
func env(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func must(err error) {
if err != nil {
panic(err)
}
}const BASE_URL = process.env.BASE_URL ?? "https://api.elearning.edu.pl";
// Values your HR system already knows: employee number, department name, course code.
const EMPLOYEE_NO = "E1001";
const DEPARTMENT_NAME = "Logistics";
const COURSE_CODE = "SAFETY-101";
async function api(method, path, body) {
const response = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
"X-Tenant-Code": process.env.TENANT_CODE ?? "",
"X-API-Key": process.env.API_KEY ?? "",
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw Object.assign(new Error(`${response.status} ${await response.text()}`), { status: response.status });
}
return response.status === 204 ? null : response.json();
}
// 1. Look the person up by employee number; create them if missing.
const { users } = await api("GET", `/api/v1/users?employee_no=${EMPLOYEE_NO}`);
if (users.length === 0) {
await api("POST", "/api/v1/users", {
employee_no: EMPLOYEE_NO,
first_name: "Anna",
last_name: "Example",
email: "anna.example@example.com",
department_name: DEPARTMENT_NAME,
});
}
// 2. Assign a course for this year's cycle (409 = already assigned).
try {
await api("POST", "/api/v1/enrollments", {
employee_no: EMPLOYEE_NO,
course_code: COURSE_CODE,
cycle_year: 2026,
due_date: "2026-12-31",
});
} catch (error) {
if (error.status !== 409) throw error;
}
// 3. Read the compliance matrix page by page.
const limit = 100;
for (let offset = 0; ; offset += limit) {
const { rows } = await api("GET", `/api/v1/compliance/matrix?limit=${limit}&offset=${offset}`);
for (const row of rows) {
// courses: course code → status cell
const statuses = Object.fromEntries(Object.entries(row.courses).map(([code, cell]) => [code, cell.status]));
console.log(row.employee_no, row.full_name, statuses);
}
if (rows.length < limit) break;
}using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = Environment.GetEnvironmentVariable("BASE_URL") ?? "https://api.elearning.edu.pl";
// Values your HR system already knows: employee number, department name, course code.
const string employeeNo = "E1001";
const string departmentName = "Logistics";
const string courseCode = "SAFETY-101";
using var http = new HttpClient { BaseAddress = new Uri(baseUrl) };
http.DefaultRequestHeaders.Add("X-Tenant-Code", Environment.GetEnvironmentVariable("TENANT_CODE"));
http.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("API_KEY"));
// 1. Look the person up by employee number; create them if missing.
var found = await http.GetFromJsonAsync<JsonElement>($"/api/v1/users?employee_no={employeeNo}");
if (found.GetProperty("users").GetArrayLength() == 0)
{
using var created = await http.PostAsJsonAsync("/api/v1/users", new
{
employee_no = employeeNo,
first_name = "Anna",
last_name = "Example",
email = "anna.example@example.com",
department_name = departmentName,
});
created.EnsureSuccessStatusCode();
}
// 2. Assign a course for this year's cycle (409 = already assigned).
using var assigned = await http.PostAsJsonAsync("/api/v1/enrollments", new
{
employee_no = employeeNo,
course_code = courseCode,
cycle_year = 2026,
due_date = "2026-12-31",
});
if (assigned.StatusCode != HttpStatusCode.Conflict)
{
assigned.EnsureSuccessStatusCode();
}
// 3. Read the compliance matrix page by page.
const int limit = 100;
for (var offset = 0; ; offset += limit)
{
var page = await http.GetFromJsonAsync<JsonElement>($"/api/v1/compliance/matrix?limit={limit}&offset={offset}");
var rows = page.GetProperty("rows");
foreach (var row in rows.EnumerateArray())
{
// courses: course code → status cell
var statuses = row.GetProperty("courses").EnumerateObject().Select(c => $"{c.Name}={c.Value.GetProperty("status")}");
Console.WriteLine($"{row.GetProperty("full_name")} {string.Join(",", statuses)}");
}
if (rows.GetArrayLength() < limit)
{
break;
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// Use a JSON library (Jackson, Gson, …) in real code; small regexes keep the example dependency-free.
public class Example {
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "https://api.elearning.edu.pl");
static final HttpClient HTTP = HttpClient.newHttpClient();
public static void main(String[] args) throws Exception {
// Values your HR system already knows: employee number, department name, course code.
String employeeNo = "E1001";
String departmentName = "Logistics";
String courseCode = "SAFETY-101";
// 1. Look the person up by employee number; create them if missing.
HttpResponse<String> found = call("GET", "/api/v1/users?employee_no=" + employeeNo, null);
if (firstId(found.body()) == null) {
call("POST", "/api/v1/users", """
{"employee_no": "%s", "first_name": "Anna", "last_name": "Example",
"email": "anna.example@example.com", "department_name": "%s"}
""".formatted(employeeNo, departmentName));
}
// 2. Assign a course for this year's cycle (409 = already assigned).
HttpResponse<String> assigned = call("POST", "/api/v1/enrollments", """
{"employee_no": "%s", "course_code": "%s", "cycle_year": 2026, "due_date": "2026-12-31"}
""".formatted(employeeNo, courseCode));
if (assigned.statusCode() != 201 && assigned.statusCode() != 409) {
throw new IllegalStateException("enrollment failed: " + assigned.statusCode() + " " + assigned.body());
}
// 3. Read the compliance matrix page by page.
int limit = 100;
for (int offset = 0; ; offset += limit) {
HttpResponse<String> page = call("GET", "/api/v1/compliance/matrix?limit=" + limit + "&offset=" + offset, null);
int rows = count(page.body(), "\"user_id\"");
System.out.println("page at offset " + offset + ": " + rows + " people");
if (rows < limit) {
break;
}
}
}
static HttpResponse<String> call(String method, String path, String json) throws Exception {
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(BASE_URL + path))
.header("X-Tenant-Code", System.getenv("TENANT_CODE"))
.header("X-API-Key", System.getenv("API_KEY"));
if (json != null) {
request.header("Content-Type", "application/json");
}
request.method(method, json == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(json));
HttpResponse<String> response = HTTP.send(request.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300 && response.statusCode() != 409) {
throw new IllegalStateException(method + " " + path + ": " + response.statusCode() + " " + response.body());
}
return response;
}
static String firstId(String body) {
Matcher match = Pattern.compile("\"id\"\\s*:\\s*\"([^\"]+)\"").matcher(body);
return match.find() ? match.group(1) : null;
}
static int count(String body, String needle) {
int n = 0;
for (int i = body.indexOf(needle); i >= 0; i = body.indexOf(needle, i + 1)) {
n++;
}
return n;
}
}curl https://api.elearning.edu.pl/api/v1/users -H "X-Tenant-Code: your-tenant" -H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"employee_no": "E1001", "first_name": "Anna", "last_name": "Example",
"email": "anna@example.com", "department_name": "Logistics", "worker_type": "internal_payroll"}'- Nowa osoba dostaje konto bez hasła oraz szkolenia wdrożeniowe swojego działu.
PATCH /api/v1/users/{id}aktualizuje pola standardowe i własne (attributes); pominięte pola pozostają bez zmian.- Konta zakładane przez SCIM należą do dostawcy tożsamości: ich zmiana przez to API zwraca
409 MANAGED_ELSEWHERE. - Dział:
department_name(porównywana bez rozróżniania wielkości liter, znaków diakrytycznych i nadmiarowych spacji) albodepartment_id. Nieznana nazwa zwraca422— dział musi najpierw istnieć. - Współpracownicy zewnętrzni:
worker_type: b2b_contractorzcontractor_namealbocontractor_id. Gdy dwie firmy mają tę samą nazwę, wyślijcontractor_id.
Wymagane zakresy: users:read, users:write.
2. Przydział szkoleń
curl https://api.elearning.edu.pl/api/v1/enrollments -H "X-Tenant-Code: your-tenant" -H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"employee_no": "E1001", "course_code": "SAFETY-101", "cycle_year": 2026, "due_date": "2026-12-31"}'Osobę wskaż przez employee_no albo user_id, a szkolenie przez course_code albo course_id. Przydział tego
samego szkolenia w tym samym cyklu zwraca 409. Szkolenia z biblioteki objęte licencją podlegają jej warunkom:
szkolenie licencjonowane tylko w ramach ścieżki albo z wygasłą licencją zwraca 422, a gdy licencja na stanowiska
nie ma wolnego miejsca — 409 SEAT_LIMIT_REACHED. Przydziały powstają też automatycznie (reguły działów,
grupy, odnowienia) — subskrybuj enrollment.assigned, żeby widzieć wszystkie. Wymagany zakres: enrollments:write.
3. Odczyt zgodności
curl "https://api.elearning.edu.pl/api/v1/compliance/matrix?course_code=SAFETY-101&compliance_flag=OVERDUE" \
-H "X-Tenant-Code: your-tenant" -H "X-API-Key: $KEY"Macierz jest odświeżana okresowo (refreshed_at w odpowiedzi). Aktualizacje niemal w czasie rzeczywistym dają
webhooki (course.completed, compliance.certified, compliance.expired). Wymagany zakres:
compliance:read.
Stronicowanie
Listy przyjmują limit i offset. Pobieraj kolejne strony, aż strona zwróci mniej pozycji niż limit.