Authentication
An integration client authenticates with an API key or with OAuth 2.0 client credentials. Both use the same secret; the IT administrator decides which methods the client may use.
API key
Send the secret on every request:
GET /api/v1/users HTTP/1.1
Host: https://api.elearning.edu.pl
X-Tenant-Code: your-tenant
X-API-Key: lms_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxOAuth 2.0 client credentials
Exchange the client id and secret for a bearer token (RFC 6749 §4.4), then use the token until it expires:
BASE_URL="${BASE_URL:-https://api.elearning.edu.pl}"
# 1. Exchange the client id and secret for a 15-minute token.
TOKEN=$(curl --fail-with-body -sS "$BASE_URL/api/v1/oauth/token" \
-H "X-Tenant-Code: $TENANT_CODE" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode grant_type=client_credentials \
--data-urlencode "scope=users:read compliance:read" | jq -r .access_token)
# 2. Use the token until it expires.
curl --fail-with-body -sS "$BASE_URL/api/v1/compliance/matrix?limit=10" \
-H "X-Tenant-Code: $TENANT_CODE" \
-H "Authorization: Bearer $TOKEN"import os
import time
import requests
BASE_URL = os.environ.get("BASE_URL", "https://api.elearning.edu.pl")
TENANT_CODE = os.environ["TENANT_CODE"]
class TokenSource:
"""Fetches a client-credentials token and reuses it until shortly before it expires."""
def __init__(self, client_id: str, client_secret: str, scope: str):
self.auth = (client_id, client_secret)
self.scope = scope
self.token = None
self.expires_at = 0.0
def get(self) -> str:
if self.token is None or time.time() > self.expires_at - 60:
response = requests.post(
f"{BASE_URL}/api/v1/oauth/token",
headers={"X-Tenant-Code": TENANT_CODE},
auth=self.auth,
data={"grant_type": "client_credentials", "scope": self.scope},
timeout=30,
)
response.raise_for_status()
body = response.json()
self.token = body["access_token"]
self.expires_at = time.time() + body["expires_in"]
return self.token
tokens = TokenSource(os.environ["CLIENT_ID"], os.environ["CLIENT_SECRET"], "users:read compliance:read")
response = requests.get(
f"{BASE_URL}/api/v1/compliance/matrix",
headers={"X-Tenant-Code": TENANT_CODE, "Authorization": f"Bearer {tokens.get()}"},
params={"limit": 10},
timeout=30,
)
response.raise_for_status()
print(response.json()["summary"])<?php
require __DIR__ . '/vendor/autoload.php';
$client = new GuzzleHttp\Client([
'base_uri' => getenv('BASE_URL') ?: 'https://api.elearning.edu.pl',
'timeout' => 30,
'headers' => ['X-Tenant-Code' => getenv('TENANT_CODE')],
]);
// 1. Exchange the client id and secret for a 15-minute token (reuse it until it expires).
$response = $client->request('POST', '/api/v1/oauth/token', [
'auth' => [getenv('CLIENT_ID'), getenv('CLIENT_SECRET')],
'form_params' => [
'grant_type' => 'client_credentials',
'scope' => 'users:read compliance:read',
],
]);
$token = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
// 2. Call the API with the token.
$response = $client->request('GET', '/api/v1/compliance/matrix', [
'headers' => ['Authorization' => 'Bearer ' . $token['access_token']],
'query' => ['limit' => 10],
]);
$matrix = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
print_r($matrix['summary']);package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
)
func main() {
baseURL := os.Getenv("BASE_URL")
if baseURL == "" {
baseURL = "https://api.elearning.edu.pl"
}
tenant := os.Getenv("TENANT_CODE")
// 1. Exchange the client id and secret for a 15-minute token (reuse it until it expires).
form := url.Values{
"grant_type": {"client_credentials"},
"scope": {"users:read compliance:read"},
}
req, err := http.NewRequest("POST", baseURL+"/api/v1/oauth/token", strings.NewReader(form.Encode()))
if err != nil {
panic(err)
}
req.Header.Set("X-Tenant-Code", tenant)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET"))
var token struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
if err := do(req, &token); err != nil {
panic(err)
}
// 2. Call the API with the token.
req, err = http.NewRequest("GET", baseURL+"/api/v1/compliance/matrix?limit=10", nil)
if err != nil {
panic(err)
}
req.Header.Set("X-Tenant-Code", tenant)
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
var matrix struct {
Summary map[string]int `json:"summary"`
}
if err := do(req, &matrix); err != nil {
panic(err)
}
fmt.Println(matrix.Summary)
}
func do(req *http.Request, out any) error {
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
body, _ := io.ReadAll(res.Body)
return fmt.Errorf("%s: %s", res.Status, body)
}
return json.NewDecoder(res.Body).Decode(out)
}const BASE_URL = process.env.BASE_URL ?? "https://api.elearning.edu.pl";
const TENANT_CODE = process.env.TENANT_CODE ?? "";
const CLIENT_ID = process.env.CLIENT_ID ?? "";
const CLIENT_SECRET = process.env.CLIENT_SECRET ?? "";
let token = "";
let expiresAt = 0;
// Fetches a client-credentials token and reuses it until shortly before it expires.
async function accessToken() {
if (!token || Date.now() > expiresAt - 60_000) {
const response = await fetch(`${BASE_URL}/api/v1/oauth/token`, {
method: "POST",
headers: {
"X-Tenant-Code": TENANT_CODE,
Authorization: `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
},
body: new URLSearchParams({ grant_type: "client_credentials", scope: "users:read compliance:read" }),
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const body = await response.json();
token = body.access_token;
expiresAt = Date.now() + body.expires_in * 1000;
}
return token;
}
const response = await fetch(`${BASE_URL}/api/v1/compliance/matrix?limit=10`, {
headers: { "X-Tenant-Code": TENANT_CODE, Authorization: `Bearer ${await accessToken()}` },
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
console.log((await response.json()).summary);using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
var baseUrl = Environment.GetEnvironmentVariable("BASE_URL") ?? "https://api.elearning.edu.pl";
using var http = new HttpClient { BaseAddress = new Uri(baseUrl) };
http.DefaultRequestHeaders.Add("X-Tenant-Code", Environment.GetEnvironmentVariable("TENANT_CODE"));
// 1. Exchange the client id and secret for a 15-minute token (reuse it until it expires).
using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/oauth/token")
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["scope"] = "users:read compliance:read",
}),
};
var credentials = $"{Environment.GetEnvironmentVariable("CLIENT_ID")}:{Environment.GetEnvironmentVariable("CLIENT_SECRET")}";
tokenRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials)));
using var tokenResponse = await http.SendAsync(tokenRequest);
tokenResponse.EnsureSuccessStatusCode();
var token = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>();
// 2. Call the API with the token.
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/compliance/matrix?limit=10");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.GetProperty("access_token").GetString());
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var matrix = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(matrix.GetProperty("summary"));import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Example {
public static void main(String[] args) throws Exception {
String baseUrl = System.getenv().getOrDefault("BASE_URL", "https://api.elearning.edu.pl");
String tenant = System.getenv("TENANT_CODE");
HttpClient http = HttpClient.newHttpClient();
// 1. Exchange the client id and secret for a 15-minute token (reuse it until it expires).
String credentials = System.getenv("CLIENT_ID") + ":" + System.getenv("CLIENT_SECRET");
String form = "grant_type=client_credentials&scope="
+ URLEncoder.encode("users:read compliance:read", StandardCharsets.UTF_8);
HttpRequest tokenRequest = HttpRequest.newBuilder(URI.create(baseUrl + "/api/v1/oauth/token"))
.header("X-Tenant-Code", tenant)
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
HttpResponse<String> tokenResponse = http.send(tokenRequest, HttpResponse.BodyHandlers.ofString());
if (tokenResponse.statusCode() != 200) {
throw new IllegalStateException(tokenResponse.statusCode() + " " + tokenResponse.body());
}
// Use a JSON library (Jackson, Gson, …) in real code; a regex keeps the example dependency-free.
Matcher match = Pattern.compile("\"access_token\"\\s*:\\s*\"([^\"]+)\"").matcher(tokenResponse.body());
if (!match.find()) {
throw new IllegalStateException("no access_token in " + tokenResponse.body());
}
// 2. Call the API with the token.
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/api/v1/compliance/matrix?limit=10"))
.header("X-Tenant-Code", tenant)
.header("Authorization", "Bearer " + match.group(1))
.GET()
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
if (response.statusCode() != 200) {
System.exit(1);
}
}
}{"access_token": "eyJ…", "token_type": "Bearer", "expires_in": 900, "scope": "users:read compliance:read"}GET /api/v1/compliance/matrix HTTP/1.1
Host: https://api.elearning.edu.pl
X-Tenant-Code: your-tenant
Authorization: Bearer eyJ…- Tokens are valid for 15 minutes. Reuse a token until it expires; the token endpoint allows only 10 tokens per minute per client.
scopeis optional; by default the token gets all scopes of the client.- The client's state (disabled, revoked secret, IP allowlist, expiry) is checked on every request, so revoking a client takes effect immediately, even for tokens already issued.
Failures
Authentication failures return 401 without a reason (wrong secret, expired or disabled client, address outside the
allowlist) — this is deliberate. The token endpoint returns OAuth errors (invalid_client, invalid_scope, …) as
defined in RFC 6749 §5.2.
SCIM
Identity providers (e.g. Microsoft Entra ID) use a client with the scim scope. Configure the provider with:
- Tenant URL:
https://api.elearning.edu.pl/scim/v2/your-tenant - Secret token: the client secret (long-lived) or an OAuth token.