MENU navbar-image

Introduction

Mintfit V2 LMS - A modular monolith learning management system with microservice-style boundaries.

This documentation provides comprehensive information about the Mintfit V2 API.

## Architecture

Mintfit V2 is built as a **modular monolith** with strict module boundaries:
- Each module is self-contained with Services as the public API
- Modules follow Domain-Driven Design principles (Domain, Application, Infrastructure layers)
- Inter-module communication is only allowed through Service classes in the Application layer

<aside>As you scroll, you'll see code examples in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>

Authenticating requests

This API is not authenticated.

Ad Delivery

Get ads for a specific placement (public endpoint) This endpoint is used by frontends to fetch ads for display. It returns ads based on: - Frontend and placement - Tenant context (global + tenant-specific ads) - Time constraints (starts_at/ends_at) - Priority ordering

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads/delivery?frontend=architecto&placement=architecto&tenant_id=architecto&limit=16" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"frontend\": \"b\",
    \"placement\": \"n\",
    \"tenant_id\": \"architecto\",
    \"limit\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/ads/delivery"
);

const params = {
    "frontend": "architecto",
    "placement": "architecto",
    "tenant_id": "architecto",
    "limit": "16",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "frontend": "b",
    "placement": "n",
    "tenant_id": "architecto",
    "limit": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/delivery';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'frontend' => 'architecto',
            'placement' => 'architecto',
            'tenant_id' => 'architecto',
            'limit' => '16',
        ],
        'json' => [
            'frontend' => 'b',
            'placement' => 'n',
            'tenant_id' => 'architecto',
            'limit' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads/delivery

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

frontend   string     

The frontend ID (e.g., 'web-app', 'mobile-app') Example: architecto

placement   string     

The placement key (e.g., 'header', 'sidebar') Example: architecto

tenant_id   string  optional    

optional The tenant ID (for multi-tenant context) Example: architecto

limit   integer  optional    

optional Max number of ads to return (default: 10) Example: 16

Body Parameters

frontend   string     

Must not be greater than 100 characters. Example: b

placement   string     

Must not be greater than 100 characters. Example: n

tenant_id   string  optional    

Example: architecto

limit   integer  optional    

Must be at least 1. Must not be greater than 50. Example: 22

Ad Management

List all ads with optional filtering

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads?per_page=20&page=1&type=banner&status=active&tenant_id=01JH...&search=promo&created_at_gte=2024-01-01&created_at_lte=2024-12-31&sort=-priority" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads"
);

const params = {
    "per_page": "20",
    "page": "1",
    "type": "banner",
    "status": "active",
    "tenant_id": "01JH...",
    "search": "promo",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "sort": "-priority",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'type' => 'banner',
            'status' => 'active',
            'tenant_id' => '01JH...',
            'search' => 'promo',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'sort' => '-priority',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 20. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

type   string  optional    

Filter by ad type (banner, native, video, html). Example: banner

status   string  optional    

Filter by status (active, paused, draft, expired). Example: active

tenant_id   string  optional    

Filter by tenant ID (includes global ads). Example: 01JH...

search   string  optional    

Search in name. Example: promo

created_at_gte   string  optional    

Filter ads created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter ads created on or before date. Example: 2024-12-31

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -priority

Create a new ad

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get ad by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update ad by ID

Example request:
curl --request PUT \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/ads/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete ad by ID (soft delete)

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/ads/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Activate an ad

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/activate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/activate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/activate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads/{id}/activate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Pause an ad

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/pause" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/pause"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/pause';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads/{id}/pause

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Assign ad to placement

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads/{adId}/placements/{placementId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

adId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

placementId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Unassign ad from placement

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/ads/{adId}/placements/{placementId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

adId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

placementId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Ad Statistics

Get summary statistics for all ads

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads/statistics/summary" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"tenant_id\": \"architecto\",
    \"period_start\": \"2026-11-03T00:49:05\",
    \"period_end\": \"2052-11-26\"
}"
const url = new URL(
    "http://localhost:8000/v1/ads/statistics/summary"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "tenant_id": "architecto",
    "period_start": "2026-11-03T00:49:05",
    "period_end": "2052-11-26"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/statistics/summary';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'tenant_id' => 'architecto',
            'period_start' => '2026-11-03T00:49:05',
            'period_end' => '2052-11-26',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads/statistics/summary

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

tenant_id   string  optional    

The id of an existing record in the tenants table. Example: architecto

period_start   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

period_end   string  optional    

Must be a valid date. Must be a date after or equal to period_start. Example: 2052-11-26

Get statistics for an ad

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/statistics" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/statistics"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/statistics';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads/{id}/statistics

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Get statistics for a placement

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6/statistics" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6/statistics"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6/statistics';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/placements/{id}/statistics

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the placement. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Ad Tracking

Track an ad impression (public endpoint) This endpoint is called when an ad is displayed to a user. The tracking is processed asynchronously via a queued job.

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads/track/impression" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ad_id\": \"architecto\",
    \"placement_id\": \"architecto\",
    \"placement_key\": \"architecto\",
    \"frontend\": \"architecto\",
    \"tenant_id\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/ads/track/impression"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ad_id": "architecto",
    "placement_id": "architecto",
    "placement_key": "architecto",
    "frontend": "architecto",
    "tenant_id": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/track/impression';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'ad_id' => 'architecto',
            'placement_id' => 'architecto',
            'placement_key' => 'architecto',
            'frontend' => 'architecto',
            'tenant_id' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads/track/impression

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ad_id   string     

The id of an existing record in the ads table. Example: architecto

placement_id   string  optional    

This field is required when placement_key is not present. The id of an existing record in the placements table. Example: architecto

placement_key   string  optional    

This field is required when placement_id is not present. Example: architecto

frontend   string  optional    

This field is required when placement_key is present. Example: architecto

tenant_id   string  optional    

The id of an existing record in the tenants table. Example: architecto

Track an ad click (public endpoint) This endpoint is called when a user clicks on an ad. The tracking is processed asynchronously via a queued job.

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads/track/click" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ad_id\": \"architecto\",
    \"placement_id\": \"architecto\",
    \"placement_key\": \"architecto\",
    \"frontend\": \"architecto\",
    \"tenant_id\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/ads/track/click"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ad_id": "architecto",
    "placement_id": "architecto",
    "placement_key": "architecto",
    "frontend": "architecto",
    "tenant_id": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/track/click';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'ad_id' => 'architecto',
            'placement_id' => 'architecto',
            'placement_key' => 'architecto',
            'frontend' => 'architecto',
            'tenant_id' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads/track/click

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ad_id   string     

The id of an existing record in the ads table. Example: architecto

placement_id   string  optional    

This field is required when placement_key is not present. The id of an existing record in the placements table. Example: architecto

placement_key   string  optional    

This field is required when placement_id is not present. Example: architecto

frontend   string  optional    

This field is required when placement_key is present. Example: architecto

tenant_id   string  optional    

The id of an existing record in the tenants table. Example: architecto

Anonymous Sessions

List anonymous sessions Returns a paginated list of the tenant's anonymous sessions with optional filtering. Requires `anonymous_sessions.read` permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/anonymous-sessions?per_page=10&page=1&anonymous_id=01kav3fcmme10dwbeexyqkfndn&search=192.168&logged_in_at_gte=2026-01-01&logged_in_at_lte=2026-12-31&is_upgraded=1&sort=-logged_in_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/anonymous-sessions"
);

const params = {
    "per_page": "10",
    "page": "1",
    "anonymous_id": "01kav3fcmme10dwbeexyqkfndn",
    "search": "192.168",
    "logged_in_at_gte": "2026-01-01",
    "logged_in_at_lte": "2026-12-31",
    "is_upgraded": "1",
    "sort": "-logged_in_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/anonymous-sessions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '10',
            'page' => '1',
            'anonymous_id' => '01kav3fcmme10dwbeexyqkfndn',
            'search' => '192.168',
            'logged_in_at_gte' => '2026-01-01',
            'logged_in_at_lte' => '2026-12-31',
            'is_upgraded' => '1',
            'sort' => '-logged_in_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Anonymous sessions retrieved successfully",
    "data": {
        "data": [],
        "total": 0,
        "per_page": 10,
        "current_page": 1,
        "last_page": 1
    }
}
 

Request      

GET v1/anonymous-sessions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Items per page (1-100). Example: 10

page   integer  optional    

Page number. Example: 1

anonymous_id   string  optional    

Filter by anonymous ID. Example: 01kav3fcmme10dwbeexyqkfndn

search   string  optional    

Search in IP address and user agent. Example: 192.168

logged_in_at_gte   string  optional    

date Filter sessions after this date. Example: 2026-01-01

logged_in_at_lte   string  optional    

date Filter sessions before this date. Example: 2026-12-31

is_upgraded   boolean  optional    

Filter by upgrade status. Example: true

sort   string  optional    

Sort field (prefix with - for desc). Example: -logged_in_at

Get anonymous session Returns a single anonymous session of the tenant by ID. Requires `anonymous_sessions.read` permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/anonymous-sessions/01kav3fcmme10dwbeexyqkfndn" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/anonymous-sessions/01kav3fcmme10dwbeexyqkfndn"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/anonymous-sessions/01kav3fcmme10dwbeexyqkfndn';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Anonymous session retrieved successfully",
    "data": {
        "id": "01kav3fcmme10dwbeexyqkfndn",
        "tenant_id": "01kav3fbpbpb3wnhtadsmrmt7m",
        "anonymous_id": "01kav3fcmme10dwbeexyqkfndn",
        "ip_address": "127.0.0.1",
        "user_agent": "Mozilla/5.0",
        "logged_in_at": "2026-03-04T00:00:00+00:00",
        "last_refreshed_at": null,
        "refresh_count": 0,
        "upgraded_to_identity_id": null,
        "upgraded_at": null,
        "expires_at": "2026-04-03T00:00:00+00:00"
    }
}
 

Example response (404):


{
    "success": false,
    "message": "Anonymous session not found"
}
 

Request      

GET v1/anonymous-sessions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The anonymous session ID. Example: 01kav3fcmme10dwbeexyqkfndn

Attempt Management

Start a new attempt

Creates a new attempt for an attemptable entity (e.g., quiz_version). The attempt is created in 'in_progress' state. Its questions are drawn now and returned in delivery order; every later read of the attempt returns the same questions. See AttemptView for what they contain. The identity_id is automatically set from the authenticated user.

Example request:
curl --request POST \
    "http://localhost:8000/v1/attempts/start" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"attemptable_type\": \"quiz_version\",
    \"attemptable_id\": \"01JGQUIZVERSION0000001\",
    \"metadata\": []
}"
const url = new URL(
    "http://localhost:8000/v1/attempts/start"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "attemptable_type": "quiz_version",
    "attemptable_id": "01JGQUIZVERSION0000001",
    "metadata": []
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/start';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'attemptable_type' => 'quiz_version',
            'attemptable_id' => '01JGQUIZVERSION0000001',
            'metadata' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{
    "success": true,
    "message": "Attempt started successfully",
    "data": {
        "id": "01JGATTEMPT000000000001",
        "identity_id": "01JGUSER00000000000001",
        "attemptable_type": "quiz_version",
        "attemptable_id": "01JGQUIZVERSION0000001",
        "status": "in_progress",
        "started_at": "2024-01-15T10:30:00Z",
        "submitted_at": null,
        "abandoned_at": null,
        "total_score": null,
        "max_possible_score": null,
        "metadata": {},
        "questions": [
            {
                "slot_id": "01JGSLOT000000000000001",
                "slot_option_id": "01JGQUESTION00000000001",
                "sequence_number": 1,
                "points": 2,
                "question": {
                    "type": "short_answer",
                    "title": {
                        "en": "Q1"
                    },
                    "prompt": {
                        "en": "Enter 42"
                    },
                    "instructions": null,
                    "media_id": null,
                    "logic": []
                }
            }
        ],
        "responses": []
    }
}
 

Request      

POST v1/attempts/start

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

attemptable_type   string     

The type of attemptable entity. Example: quiz_version

attemptable_id   string     

The attemptable entity ID (ULID). Example: 01JGQUIZVERSION0000001

metadata   object  optional    

Optional metadata for the attempt.

Get attempts for attemptable

Retrieves paginated attempts for a specific identity and attemptable entity. Users can only view their own attempts. Staff with 'attempts.read_all' view every identity's attempts, and omit identity_id to list them all, on quizzes their tenant owns.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/attempts/for-attemptable?identity_id=01JGUSER00000000000001&attemptable_type=quiz_version&attemptable_id=01JGQUIZVERSION0000001&per_page=10&page=1" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"identity_id\": \"bngzmiyvdljnikhwaykcmyuwpw\",
    \"attemptable_type\": \"architecto\",
    \"attemptable_id\": \"ngzmiyvdljnikhwaykcmyuwpwl\",
    \"per_page\": 20,
    \"page\": 43
}"
const url = new URL(
    "http://localhost:8000/v1/attempts/for-attemptable"
);

const params = {
    "identity_id": "01JGUSER00000000000001",
    "attemptable_type": "quiz_version",
    "attemptable_id": "01JGQUIZVERSION0000001",
    "per_page": "10",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "identity_id": "bngzmiyvdljnikhwaykcmyuwpw",
    "attemptable_type": "architecto",
    "attemptable_id": "ngzmiyvdljnikhwaykcmyuwpwl",
    "per_page": 20,
    "page": 43
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/for-attemptable';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'identity_id' => '01JGUSER00000000000001',
            'attemptable_type' => 'quiz_version',
            'attemptable_id' => '01JGQUIZVERSION0000001',
            'per_page' => '10',
            'page' => '1',
        ],
        'json' => [
            'identity_id' => 'bngzmiyvdljnikhwaykcmyuwpw',
            'attemptable_type' => 'architecto',
            'attemptable_id' => 'ngzmiyvdljnikhwaykcmyuwpwl',
            'per_page' => 20,
            'page' => 43,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Attempts retrieved successfully",
    "data": {
        "current_page": 1,
        "data": [],
        "total": 0
    }
}
 

Request      

GET v1/attempts/for-attemptable

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

identity_id   string     

The identity ID. Example: 01JGUSER00000000000001

attemptable_type   string     

The attemptable type. Example: quiz_version

attemptable_id   string     

The attemptable ID. Example: 01JGQUIZVERSION0000001

per_page   integer  optional    

Optional items per page. Example: 10

page   integer  optional    

Optional page number. Example: 1

Body Parameters

identity_id   string  optional    

Must be 26 characters. Example: bngzmiyvdljnikhwaykcmyuwpw

attemptable_type   string     

Example: architecto

attemptable_id   string     

Must be 26 characters. Example: ngzmiyvdljnikhwaykcmyuwpwl

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 20

page   integer  optional    

Must be at least 1. Example: 43

Get attempts for a quiz across all versions.

Retrieves paginated attempts for a specific quiz (all versions). Users can only view their own attempts. Staff with 'attempts.read_all' view every identity's attempts on quizzes their tenant owns.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/attempts/for-quiz?identity_id=01JGUSER00000000000001&quiz_id=01JGQUIZ00000000000001&per_page=10&page=1" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"identity_id\": \"bngzmiyvdljnikhwaykcmyuwpw\",
    \"quiz_id\": \"lvqwrsitcpscqldzsnrwtujwvl\",
    \"per_page\": 9,
    \"page\": 80
}"
const url = new URL(
    "http://localhost:8000/v1/attempts/for-quiz"
);

const params = {
    "identity_id": "01JGUSER00000000000001",
    "quiz_id": "01JGQUIZ00000000000001",
    "per_page": "10",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "identity_id": "bngzmiyvdljnikhwaykcmyuwpw",
    "quiz_id": "lvqwrsitcpscqldzsnrwtujwvl",
    "per_page": 9,
    "page": 80
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/for-quiz';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'identity_id' => '01JGUSER00000000000001',
            'quiz_id' => '01JGQUIZ00000000000001',
            'per_page' => '10',
            'page' => '1',
        ],
        'json' => [
            'identity_id' => 'bngzmiyvdljnikhwaykcmyuwpw',
            'quiz_id' => 'lvqwrsitcpscqldzsnrwtujwvl',
            'per_page' => 9,
            'page' => 80,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/attempts/for-quiz

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

identity_id   string  optional    

optional The identity ID. Example: 01JGUSER00000000000001

quiz_id   string     

The quiz ID. Example: 01JGQUIZ00000000000001

per_page   integer  optional    

Optional items per page. Example: 10

page   integer  optional    

Optional page number. Example: 1

Body Parameters

identity_id   string  optional    

Must be 26 characters. Example: bngzmiyvdljnikhwaykcmyuwpw

quiz_id   string     

Must be 26 characters. Example: lvqwrsitcpscqldzsnrwtujwvl

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 9

page   integer  optional    

Must be at least 1. Example: 80

Record an answer

Records or updates a user's answer to a question. Supports optimistic locking via current_version parameter.

Example request:
curl --request POST \
    "http://localhost:8000/v1/attempts/record-answer" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"attempt_id\": \"01JGATTEMPT000000000001\",
    \"slot_option_id\": \"01JGQUESTION00000000001\",
    \"answer_data\": {
        \"selected_option_id\": \"b\"
    },
    \"time_spent_seconds\": 16,
    \"current_version\": 16,
    \"metadata\": []
}"
const url = new URL(
    "http://localhost:8000/v1/attempts/record-answer"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "attempt_id": "01JGATTEMPT000000000001",
    "slot_option_id": "01JGQUESTION00000000001",
    "answer_data": {
        "selected_option_id": "b"
    },
    "time_spent_seconds": 16,
    "current_version": 16,
    "metadata": []
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/record-answer';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'attempt_id' => '01JGATTEMPT000000000001',
            'slot_option_id' => '01JGQUESTION00000000001',
            'answer_data' => [
                'selected_option_id' => 'b',
            ],
            'time_spent_seconds' => 16,
            'current_version' => 16,
            'metadata' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Answer recorded successfully",
    "data": {
        "id": "01JGRESPONSE0000000001",
        "attempt_id": "01JGATTEMPT000000000001",
        "slot_option_id": "01JGQUESTION00000000001",
        "answer_snapshot": {
            "selected_option_id": "b"
        },
        "submission_state": "answered",
        "points_awarded": null,
        "is_correct": null,
        "response_version": 1,
        "first_responded_at": "2024-01-15T10:31:00Z",
        "last_responded_at": "2024-01-15T10:31:00Z",
        "time_spent_seconds": 45,
        "metadata": null
    }
}
 

Request      

POST v1/attempts/record-answer

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

attempt_id   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

slot_option_id   string     

The slot option ID (ULID). Example: 01JGQUESTION00000000001

answer_data   object     

The answer data.

time_spent_seconds   integer  optional    

Optional time spent on question. Example: 16

current_version   integer  optional    

Optional current response version for optimistic locking. Example: 16

metadata   object  optional    

Optional response metadata.

Record slot display

Records that a slot/question was displayed to the user, for navigation analytics. A question option must be one the attempt was scheduled; a display never changes the attempt's questions.

Example request:
curl --request POST \
    "http://localhost:8000/v1/attempts/record-slot-display" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"attempt_id\": \"01JGATTEMPT000000000001\",
    \"slot_id\": \"01JGSLOT000000000000001\",
    \"slot_option_id\": \"architecto\",
    \"navigation_method\": \"sequential\",
    \"metadata\": []
}"
const url = new URL(
    "http://localhost:8000/v1/attempts/record-slot-display"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "attempt_id": "01JGATTEMPT000000000001",
    "slot_id": "01JGSLOT000000000000001",
    "slot_option_id": "architecto",
    "navigation_method": "sequential",
    "metadata": []
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/record-slot-display';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'attempt_id' => '01JGATTEMPT000000000001',
            'slot_id' => '01JGSLOT000000000000001',
            'slot_option_id' => 'architecto',
            'navigation_method' => 'sequential',
            'metadata' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Slot display recorded successfully",
    "data": {
        "id": "01JGHISTORY00000000001",
        "attempt_id": "01JGATTEMPT000000000001",
        "slot_id": "01JGSLOT000000000000001",
        "slot_option_id": "01JGQUESTION00000000001",
        "displayed_at": "2024-01-15T10:30:30Z",
        "display_sequence": 1,
        "navigation_method": "sequential",
        "metadata": null
    }
}
 

Request      

POST v1/attempts/record-slot-display

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

attempt_id   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

slot_id   string     

The slot ID (ULID). Example: 01JGSLOT000000000000001

slot_option_id   string  optional    

Optional slot option ID if slot contains a question. Example: architecto

navigation_method   string  optional    

Optional navigation method. Example: sequential

metadata   object  optional    

Optional display metadata.

Get attempt by ID

Retrieves the attempt with its questions in delivery order and its responses. Answer keys, formula variants and grading output are only included once the attempt is submitted (review). grading_error, the stored reason a grading run failed, is only included for staff of the tenant owning the quiz; everyone else sees grading_status.

Learners read their own attempts. Staff with 'attempts.read_all' read every attempt on quizzes their tenant owns. Any other attempt is reported as not found.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/01JGATTEMPT000000000001';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Attempt retrieved successfully",
    "data": {
        "id": "01JGATTEMPT000000000001",
        "identity_id": "01JGUSER00000000000001",
        "attemptable_type": "quiz_version",
        "attemptable_id": "01JGQUIZVERSION0000001",
        "status": "in_progress",
        "started_at": "2024-01-15T10:30:00Z",
        "submitted_at": null,
        "abandoned_at": null,
        "total_score": null,
        "max_possible_score": null,
        "metadata": {},
        "questions": [],
        "responses": []
    }
}
 

Request      

GET v1/attempts/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

Delete attempt

Requires 'attempts.delete'. Deletes the caller's own attempts and, for staff, attempts on quizzes their tenant owns. Any other attempt is reported as not found and left unchanged.

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/01JGATTEMPT000000000001';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/attempts/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

Submit attempt

Submits the attempt for grading. Makes the attempt immutable.

Example request:
curl --request POST \
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/submit" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/submit"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/submit';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Attempt submitted successfully",
    "data": {
        "id": "01JGATTEMPT000000000001",
        "status": "submitted",
        "submitted_at": "2024-01-15T10:45:00Z"
    }
}
 

Request      

POST v1/attempts/{id}/submit

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

Abandon attempt

Abandons the attempt. Makes the attempt immutable.

Example request:
curl --request POST \
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/abandon" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/abandon"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/abandon';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Attempt abandoned successfully",
    "data": {
        "id": "01JGATTEMPT000000000001",
        "status": "abandoned",
        "abandoned_at": "2024-01-15T10:45:00Z"
    }
}
 

Request      

POST v1/attempts/{id}/abandon

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

Skip a question

Marks a question as explicitly skipped.

Example request:
curl --request POST \
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/skip/01JGQUESTION00000000001" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/skip/01JGQUESTION00000000001"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/attempts/01JGATTEMPT000000000001/skip/01JGQUESTION00000000001';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Question skipped successfully",
    "data": {
        "id": "01JGRESPONSE0000000001",
        "attempt_id": "01JGATTEMPT000000000001",
        "slot_option_id": "01JGQUESTION00000000001",
        "answer_snapshot": {},
        "submission_state": "skipped",
        "points_awarded": null,
        "is_correct": null,
        "response_version": 1,
        "first_responded_at": "2024-01-15T10:31:00Z",
        "last_responded_at": "2024-01-15T10:31:00Z",
        "time_spent_seconds": null,
        "metadata": null
    }
}
 

Request      

POST v1/attempts/{attemptId}/skip/{slotOptionId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

attemptId   string     

The attempt ID (ULID). Example: 01JGATTEMPT000000000001

slotOptionId   string     

The slot option ID (ULID). Example: 01JGQUESTION00000000001

Block Management

Get all block schemas Returns JSON schemas for all supported block types. Schemas define the structure and validation rules for each block type. Public endpoint - no authentication required.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/blocks/schemas" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/blocks/schemas"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/blocks/schemas';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Block schemas retrieved successfully",
    "data": [
        {
            "type": "paragraph",
            "schema": {
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string"
                    }
                }
            }
        },
        {
            "type": "headline",
            "schema": {
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string"
                    },
                    "level": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 6
                    }
                }
            }
        }
    ]
}
 

Request      

GET v1/blocks/schemas

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get schema for specific block type Returns the JSON schema for a specific block type. Supported types: paragraph, headline, image, table. Public endpoint - no authentication required.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/blocks/schemas/paragraph" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/blocks/schemas/paragraph"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/blocks/schemas/paragraph';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Schema retrieved successfully for type: paragraph",
    "data": {
        "type": "paragraph",
        "schema": {
            "type": "object",
            "properties": {
                "content": {
                    "type": "string"
                }
            },
            "required": [
                "content"
            ]
        }
    }
}
 

Example response (404):


{
    "success": false,
    "message": "Block type not found: unknown"
}
 

Request      

GET v1/blocks/schemas/{type}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

type   string     

Block type. Example: paragraph

Content Hierarchy

Get child pages of a content page

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/children" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/children"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/children';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/children

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Get navigation context (parent, children, breadcrumbs, next/prev)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/navigation" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/navigation"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/navigation';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/navigation

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Reorder child pages

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/reorder" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ordered_ids\": [
        \"architecto\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/reorder"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ordered_ids": [
        "architecto"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/reorder';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'ordered_ids' => [
                'architecto',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{parentId}/reorder

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

parentId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

ordered_ids   string[]     

The id of an existing record in the contents table.

Content History

Get version history for a content page

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/history

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

Get a specific version from history

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/history/{historyId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

historyId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Compare two versions

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/compare" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"version1\": \"architecto\",
    \"version2\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/compare"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "version1": "architecto",
    "version2": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/compare';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'version1' => 'architecto',
            'version2' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/history/compare

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

version1   string     

Example: architecto

version2   string     

Example: architecto

Restore a previous version

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/history/{historyId}/restore

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

historyId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Content Management

List all content pages (paginated, filtered)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content?per_page=20&page=1&type=page&status=published&is_public=1&search=welcome&created_at_gte=2024-01-01&created_at_lte=2024-12-31&tags=category%3Anews%2Cfeatured&tag_mode=and&sort=-created_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content"
);

const params = {
    "per_page": "20",
    "page": "1",
    "type": "page",
    "status": "published",
    "is_public": "1",
    "search": "welcome",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "tags": "category:news,featured",
    "tag_mode": "and",
    "sort": "-created_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'type' => 'page',
            'status' => 'published',
            'is_public' => '1',
            'search' => 'welcome',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'tags' => 'category:news,featured',
            'tag_mode' => 'and',
            'sort' => '-created_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 20. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

type   string  optional    

Filter by content type (page, article, faq). Example: page

status   string  optional    

Filter by status (draft, published, scheduled, archived). Example: published

is_public   boolean  optional    

Filter by public status. Example: true

search   string  optional    

Search in SEO title (multilingual). Example: welcome

created_at_gte   string  optional    

Filter content created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter content created on or before date. Example: 2024-12-31

tags   string  optional    

Comma-separated tag slugs. Example: category:news,featured

tag_mode   string  optional    

Tag matching mode: 'and' (all tags) or 'or' (any tag). Defaults to 'or'. Example: and

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -created_at

Get content by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Get content by slug

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/slug/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/slug/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/slug/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/slug/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the slug. Example: architecto

Create new content page

Example request:
curl --request POST \
    "http://localhost:8000/v1/content" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Update content by ID

Example request:
curl --request PUT \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/content/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete content by ID (soft delete)

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/content/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update content by slug

Example request:
curl --request PUT \
    "http://localhost:8000/v1/content/slug/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/slug/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/slug/architecto';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/content/slug/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the slug. Example: architecto

Delete content by slug (soft delete)

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/content/slug/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/slug/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/slug/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/content/slug/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the slug. Example: architecto

Publish content page

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/publish" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/publish"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/publish';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/publish

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Unpublish content page (revert to draft)

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/unpublish" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/unpublish"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/unpublish';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/unpublish

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Schedule content for future publication

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/schedule" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"published_at\": \"2052-11-26\"
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/schedule"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "published_at": "2052-11-26"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/schedule';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'published_at' => '2052-11-26',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/schedule

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

published_at   string     

Must be a valid date. Must be a date after now. Example: 2052-11-26

Content Progress

Mark content page as completed

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress/complete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress/complete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress/complete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/progress/complete

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update progress percentage for a content page

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"progress_percentage\": 1
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "progress_percentage": 1
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'progress_percentage' => 1,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/progress

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

progress_percentage   integer     

Must be at least 0. Must not be greater than 100. Example: 1

Get current user's progress for a content page

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/progress

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Get progress for entire document tree (multi-page document)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress/tree" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress/tree"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/progress/tree';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{rootId}/progress/tree

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

rootId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Content Rendering

Render content to HTML

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/html" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/html"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/html';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/render/html

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Render content to Markdown

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/markdown" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/markdown"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/markdown';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/render/markdown

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Render content to plain text

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/text" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/text"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/render/text';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/render/text

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Content Tagging

List all tags used by contents

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"search\": \"b\",
    \"per_page\": 22,
    \"page\": 67
}"
const url = new URL(
    "http://localhost:8000/v1/content/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "search": "b",
    "per_page": 22,
    "page": 67
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/tags';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'search' => 'b',
            'per_page' => 22,
            'page' => 67,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (403):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Insufficient permissions: You do not have permission to view contents",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

search   string  optional    

Must not be greater than 255 characters. Example: b

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

page   integer  optional    

Must be at least 1. Example: 67

List contexts available for contents

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/tags/contexts" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/tags/contexts"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/tags/contexts';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (403):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Insufficient permissions: You do not have permission to view contents",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/tags/contexts

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get tags for a specific content

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (403):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Insufficient permissions: You do not have permission to view contents",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/content/{id}/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Assign tag(s) to content

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"tag_slugs\": [
        \"architecto\"
    ],
    \"context\": \"n\"
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "tag_slugs": [
        "architecto"
    ],
    "context": "n"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'tag_slugs' => [
                'architecto',
            ],
            'context' => 'n',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/{id}/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

tag_slugs   string[]     
context   string  optional    

Must not be greater than 50 characters. Example: n

Remove tag(s) from content

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"tag_slugs\": [
        \"architecto\"
    ],
    \"context\": \"n\"
}"
const url = new URL(
    "http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "tag_slugs": [
        "architecto"
    ],
    "context": "n"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'tag_slugs' => [
                'architecto',
            ],
            'context' => 'n',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/content/{id}/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the content. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

tag_slugs   string[]     
context   string  optional    

Must not be greater than 50 characters. Example: n

Bulk assign tags to multiple contents

Example request:
curl --request POST \
    "http://localhost:8000/v1/content/tags/bulk-assign" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"assignments\": [
        {
            \"resource_id\": \"ngzmiyvdljnikhwaykcmyuwpwl\",
            \"tag_slugs\": [
                \"architecto\"
            ]
        }
    ],
    \"context\": \"b\"
}"
const url = new URL(
    "http://localhost:8000/v1/content/tags/bulk-assign"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "assignments": [
        {
            "resource_id": "ngzmiyvdljnikhwaykcmyuwpwl",
            "tag_slugs": [
                "architecto"
            ]
        }
    ],
    "context": "b"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/content/tags/bulk-assign';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'assignments' => [
                [
                    'resource_id' => 'ngzmiyvdljnikhwaykcmyuwpwl',
                    'tag_slugs' => [
                        'architecto',
                    ],
                ],
            ],
            'context' => 'b',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/content/tags/bulk-assign

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

assignments   object[]     

Must have at least 1 items.

resource_id   string     

Must be 26 characters. Example: ngzmiyvdljnikhwaykcmyuwpwl

tag_slugs   string[]     
context   string  optional    

Must not be greater than 50 characters. Example: b

Endpoints

POST livewire/update

Example request:
curl --request POST \
    "http://localhost:8000/livewire/update" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/livewire/update"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/livewire/update';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST livewire/update

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET livewire/livewire.js

Example request:
curl --request GET \
    --get "http://localhost:8000/livewire/livewire.js" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/livewire/livewire.js"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/livewire/livewire.js';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):

Show headers
content-type: application/javascript; charset=utf-8
expires: Wed, 03 Nov 2027 00:49:04 GMT
cache-control: max-age=31536000, public
accept-ranges: bytes
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
 


 

Request      

GET livewire/livewire.js

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET livewire/livewire.min.js.map

Example request:
curl --request GET \
    --get "http://localhost:8000/livewire/livewire.min.js.map" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/livewire/livewire.min.js.map"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/livewire/livewire.min.js.map';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):

Show headers
content-type: application/javascript; charset=utf-8
expires: Wed, 03 Nov 2027 00:49:05 GMT
cache-control: max-age=31536000, public
accept-ranges: bytes
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
 


 

Request      

GET livewire/livewire.min.js.map

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

POST livewire/upload-file

Example request:
curl --request POST \
    "http://localhost:8000/livewire/upload-file" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/livewire/upload-file"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/livewire/upload-file';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST livewire/upload-file

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET livewire/preview-file/{filename}

Example request:
curl --request GET \
    --get "http://localhost:8000/livewire/preview-file/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/livewire/preview-file/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/livewire/preview-file/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
set-cookie: XSRF-TOKEN=eyJpdiI6IkRJVnNXcGxnRDZrbk50YkNLNkdvelE9PSIsInZhbHVlIjoiSVlKWGFXWk0zc3VrU3k3SWNFUHZIMGFtVzJZTHlXR3BTQ0hZa0R3ek9oczZuakI5VldqaXJSc0dFYTcwNTlmL2ZsY3ZLT3o1ME02R3VWRHh5a0lBUFNVTzJTa083UVVqWGdYTGQ1RmtDSTFLV2dZZzdXN3pPUC9OT3BTMytqK08iLCJtYWMiOiIxNDU3MWRkNzdhZDhkNzczYTc4MGJlZGI2NDJjZTk4OTVmZjg3YTY0ZTE2ZDc0MjQ0ZTI3MWRlMGE1ODZhZTUyIiwidGFnIjoiIn0%3D; expires=Tue, 03 Nov 2026 02:49:05 GMT; Max-Age=7200; path=/; samesite=lax; laravel-session=eyJpdiI6Ijg2SkJrUWpzR3UxeUtRR1hNbXVkQXc9PSIsInZhbHVlIjoiU054aTRkNmVHbFhoWWhrZGJwcU5NM05qTCtESytKNWFLL0ZXOW5XVGViVnNlYzJYYTRuUmtteTU4OWk4N1lyY3MyRkZMdFVleGVJRmZRR1RFR3hsUEo0Y2pNL0FjNkZlVFcxbGVLZklpb1I4ZE5yeUhib21maDZVRXdJUmdSZXIiLCJtYWMiOiJlYjVjMjZkZTVkOWEwNDg3NjFkOGQ4ZGRmYWRlNjEwMjliYzVjYzA2NGI1MjA5NzBlZTUyOGQwNDJiZjZkY2Y3IiwidGFnIjoiIn0%3D; expires=Tue, 03 Nov 2026 02:49:05 GMT; Max-Age=7200; path=/; httponly; samesite=lax
 

{
    "success": false,
    "message": "Unauthorized",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET livewire/preview-file/{filename}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

filename   string     

Example: architecto

GET prometheus

Example request:
curl --request GET \
    --get "http://localhost:8000/prometheus" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/prometheus"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/prometheus';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):

Show headers
content-type: text/plain; version=0.0.4; charset=utf-8
cache-control: no-cache, private
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
 


 

Request      

GET prometheus

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET v1/ads/{id}/history

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/history" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/history"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/history';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads/{id}/history

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

GET v1/ads/{id}/history/{historyId}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/ads/{id}/history/{historyId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

historyId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/ads/{id}/restore/{historyId}

Example request:
curl --request POST \
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/ads/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/ads/{id}/restore/{historyId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the ad. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

historyId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/certificate-definitions

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificate-definitions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22,
    \"certifiable_type\": \"architecto\",
    \"certifiable_id\": \"architecto\",
    \"is_active\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/certificate-definitions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22,
    "certifiable_type": "architecto",
    "certifiable_id": "architecto",
    "is_active": "architecto"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificate-definitions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
            'certifiable_type' => 'architecto',
            'certifiable_id' => 'architecto',
            'is_active' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificate-definitions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

certifiable_type   string  optional    

Example: architecto

certifiable_id   string  optional    

Example: architecto

is_active   string  optional    

Example: architecto

POST v1/certificate-definitions

Example request:
curl --request POST \
    "http://localhost:8000/v1/certificate-definitions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificate-definitions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificate-definitions';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/certificate-definitions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET v1/certificate-definitions/entity/{type}/{id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificate-definitions/entity/architecto/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificate-definitions/entity/architecto/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificate-definitions/entity/architecto/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificate-definitions/entity/{type}/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

type   string     

Example: architecto

id   string     

The ID of the {type}. Example: architecto

GET v1/certificate-definitions/{id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificate-definitions/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificate-definitions/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificate-definitions/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificate-definitions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate definition. Example: architecto

PUT v1/certificate-definitions/{id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/certificate-definitions/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificate-definitions/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificate-definitions/architecto';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/certificate-definitions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate definition. Example: architecto

DELETE v1/certificate-definitions/{id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/certificate-definitions/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificate-definitions/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificate-definitions/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/certificate-definitions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate definition. Example: architecto

Verify certificate authenticity (public endpoint)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificates/verify/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/verify/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/verify/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificates/verify/{verificationHash}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

verificationHash   string     

Example: architecto

Get current user's certificates

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificates/my" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/my"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/my';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificates/my

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Issue a participation certificate for an enrollment.

Example request:
curl --request POST \
    "http://localhost:8000/v1/certificates/enrollments/architecto/issue" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"signed_by\": \"bngzmiyvdljnikhwaykcmyuwpw\",
    \"signed_by_name\": \"l\"
}"
const url = new URL(
    "http://localhost:8000/v1/certificates/enrollments/architecto/issue"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "signed_by": "bngzmiyvdljnikhwaykcmyuwpw",
    "signed_by_name": "l"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/enrollments/architecto/issue';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'signed_by' => 'bngzmiyvdljnikhwaykcmyuwpw',
            'signed_by_name' => 'l',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/certificates/enrollments/{enrollmentId}/issue

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

enrollmentId   string     

Example: architecto

Body Parameters

signed_by   string  optional    

Must be 26 characters. Example: bngzmiyvdljnikhwaykcmyuwpw

signed_by_name   string  optional    

Must not be greater than 255 characters. Example: l

metadata   object  optional    

Get certificate by number

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificates/number/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/number/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/number/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificates/number/{certificateNumber}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

certificateNumber   string     

Example: architecto

Get certificate by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificates/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificates/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate. Example: architecto

Download certificate PDF

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/certificates/architecto/download" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/architecto/download"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/architecto/download';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/certificates/{id}/download

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate. Example: architecto

Manually issue a certificate (admin)

Example request:
curl --request POST \
    "http://localhost:8000/v1/certificates/issue" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/issue"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/issue';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/certificates/issue

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Revoke a certificate (admin). Its PDF is deleted: a revoked certificate has none.

Example request:
curl --request POST \
    "http://localhost:8000/v1/certificates/architecto/revoke" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reason\": \"b\"
}"
const url = new URL(
    "http://localhost:8000/v1/certificates/architecto/revoke"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reason": "b"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/architecto/revoke';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'reason' => 'b',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/certificates/{id}/revoke

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate. Example: architecto

Body Parameters

reason   string     

Must not be greater than 500 characters. Example: b

Regenerate certificate PDF (admin). A revoked certificate answers 422.

Example request:
curl --request POST \
    "http://localhost:8000/v1/certificates/architecto/regenerate-pdf" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/certificates/architecto/regenerate-pdf"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/architecto/regenerate-pdf';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/certificates/{id}/regenerate-pdf

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate. Example: architecto

Re-certify an expired certificate (admin)

Example request:
curl --request POST \
    "http://localhost:8000/v1/certificates/architecto/recertify" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
const url = new URL(
    "http://localhost:8000/v1/certificates/architecto/recertify"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/certificates/architecto/recertify';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/certificates/{id}/recertify

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the certificate. Example: architecto

Body Parameters

metadata   object  optional    

Handle password authentication.

Example request:
curl --request POST \
    "http://localhost:8000/dashboard/auth" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"password\": \"|]|{+-\"
}"
const url = new URL(
    "http://localhost:8000/dashboard/auth"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "password": "|]|{+-"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/dashboard/auth';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'password' => '|]|{+-',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST dashboard/auth

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

password   string     

Example: |]|{+-

Logout from dashboard.

Example request:
curl --request POST \
    "http://localhost:8000/dashboard/logout" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/dashboard/logout"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/dashboard/logout';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST dashboard/logout

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List the packages of the caller's tenant

Learners and guests see published packages; package staff also see drafts and archived packages.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages?per_page=20&page=1&status=published&type=quiz_path&is_public=1&search=introduction&created_at_gte=2024-01-01&created_at_lte=2024-12-31&tags=level%3Abeginner%2Ctopic%3Amath&tag_mode=and&sort=-created_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages"
);

const params = {
    "per_page": "20",
    "page": "1",
    "status": "published",
    "type": "quiz_path",
    "is_public": "1",
    "search": "introduction",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "tags": "level:beginner,topic:math",
    "tag_mode": "and",
    "sort": "-created_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'status' => 'published',
            'type' => 'quiz_path',
            'is_public' => '1',
            'search' => 'introduction',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'tags' => 'level:beginner,topic:math',
            'tag_mode' => 'and',
            'sort' => '-created_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": true,
    "message": "Packages retrieved successfully",
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00",
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "total": 0,
            "last_page": 1
        }
    },
    "data": []
}
 

Request      

GET v1/packages

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 15. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

status   string  optional    

Filter by status (draft, published, archived). Example: published

type   string  optional    

Filter by type (course, quiz_path). Example: quiz_path

is_public   boolean  optional    

Filter by public status. Example: true

search   string  optional    

Search in title (multilingual). Example: introduction

created_at_gte   string  optional    

Filter packages created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter packages created on or before date. Example: 2024-12-31

tags   string  optional    

Comma-separated tag slugs. Example: level:beginner,topic:math

tag_mode   string  optional    

Tag matching mode: 'and' (all tags) or 'or' (any tag). Defaults to 'or'. Example: and

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -created_at

Get a specific package

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Get a package by slug

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/slug/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/slug/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/slug/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (404):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Package with slug architecto not found",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/slug/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the slug. Example: architecto

Get package with its activities

data.activities lists the activities in order; quiz activities also name their quiz and pinned version.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/architecto/with-activities" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/with-activities"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/with-activities';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/{id}/with-activities

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Get activity content

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/activities/architecto/content" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/activities/architecto/content"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/activities/architecto/content';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/activities/{activityId}/content

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

activityId   string     

Example: architecto

Create a new package

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Update a package

Example request:
curl --request PUT \
    "http://localhost:8000/v1/packages/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/packages/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Delete a package

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/packages/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/packages/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Publish a package

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages/architecto/publish" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/publish"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/publish';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages/{id}/publish

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Archive a package

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages/architecto/archive" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/archive"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/archive';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages/{id}/archive

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Add activity to package

The activity is appended after the package's last activity. unlock_rules.prerequisite_activity_ids names activities of the same package the activity requires, unlock_rules.required_score_percentage the score each of them needs.

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages/architecto/activities" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/activities"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/activities';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages/{id}/activities

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Update activity

Prerequisites in unlock_rules must be other activities of the same package and must not require the activity in turn; unlock_rules: null removes all unlock rules.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/packages/activities/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/activities/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/activities/architecto';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/packages/activities/{activityId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

activityId   string     

Example: architecto

Delete activity

Refused with 422 while navigation rules still use the activity: errors.packages lists the slugs of the packages whose rules use it. Activities requiring it lose it from their prerequisites.

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/packages/activities/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/activities/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/activities/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/packages/activities/{activityId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

activityId   string     

Example: architecto

Reorder a package's activities

activity_ids lists every activity of the package exactly once, in the new order. Returns the package with its activities, as with-activities.

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages/architecto/activities/reorder" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/activities/reorder"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/activities/reorder';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages/{id}/activities/reorder

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Preview a quiz path's navigation

What a learner would be suggested with the given results, before the navigation is saved. The activities in results count as completed with these results; last_activity_id names the test just taken. Navigation errors are those of saving it. Each suggestion names its stage (1: a rule about the test just taken, 2: a rule about an earlier test, 3: the fallback order) and rule_index; locked_activity_ids lists the tests a learner would not be sent to yet.

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages/architecto/navigation/preview" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/navigation/preview"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/navigation/preview';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages/{id}/navigation/preview

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the package. Example: architecto

Get or create enrollment for current user (implicit enrollment)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/enrollments/packages/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/packages/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/packages/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/enrollments/packages/{packageId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

packageId   string     

Example: architecto

Get current user's enrollments (My Packages)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/enrollments/my" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"b\",
    \"per_page\": 22,
    \"page\": 67
}"
const url = new URL(
    "http://localhost:8000/v1/enrollments/my"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "b",
    "per_page": 22,
    "page": 67
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/my';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'status' => 'b',
            'per_page' => 22,
            'page' => 67,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/enrollments/my

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

status   string  optional    

Must not be greater than 50 characters. Example: b

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

page   integer  optional    

Must be at least 1. Example: 67

Get one of the current user's enrollments

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/enrollments/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/enrollments/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the enrollment. Example: architecto

Get package progress for an enrollment

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/enrollments/architecto/progress" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto/progress"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto/progress';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/enrollments/{enrollmentId}/progress

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

enrollmentId   string     

Example: architecto

Get next activity for an enrollment

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/enrollments/architecto/next-activity" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto/next-activity"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto/next-activity';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/enrollments/{enrollmentId}/next-activity

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

enrollmentId   string     

Example: architecto

Start an activity

Example request:
curl --request POST \
    "http://localhost:8000/v1/enrollments/architecto/activities/architecto/start" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto/activities/architecto/start"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto/activities/architecto/start';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/enrollments/{enrollmentId}/activities/{activityId}/start

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

enrollmentId   string     

Example: architecto

activityId   string     

Example: architecto

Complete an activity

Completion and score come from the linked item: a fully graded attempt on the quiz version, or completed content. Responds with 422 while the item is not completed.

Example request:
curl --request POST \
    "http://localhost:8000/v1/enrollments/architecto/activities/architecto/complete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto/activities/architecto/complete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto/activities/architecto/complete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/enrollments/{enrollmentId}/activities/{activityId}/complete

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

enrollmentId   string     

Example: architecto

activityId   string     

Example: architecto

Get enrollments for a specific package (admin)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/enrollments/packages/architecto/list" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/enrollments/packages/architecto/list"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/packages/architecto/list';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/enrollments/packages/{packageId}/list

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

packageId   string     

Example: architecto

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

Suspend enrollment (admin)

Example request:
curl --request POST \
    "http://localhost:8000/v1/enrollments/architecto/suspend" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto/suspend"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto/suspend';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/enrollments/{id}/suspend

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the enrollment. Example: architecto

Reactivate enrollment (admin)

Example request:
curl --request POST \
    "http://localhost:8000/v1/enrollments/architecto/reactivate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/enrollments/architecto/reactivate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/enrollments/architecto/reactivate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/enrollments/{id}/reactivate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the enrollment. Example: architecto

GET v1/quizzes/scheduler-drivers

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/scheduler-drivers" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/scheduler-drivers"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/scheduler-drivers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/scheduler-drivers

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

The title languages, subjects and difficulties the quiz list can be filtered by, as stored; base subjects and difficulty levels with their German and English labels.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/facets" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/facets"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/facets';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/facets

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

The complete version for authoring: every slot with all of its options and their question snapshots, answer keys included. Only the owning tenant's quiz staff may read it.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/structure" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/structure"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/structure';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/versions/{version_id}/structure

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/quizzes/slot-options/{option_id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/slot-options/{option_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

option_id   string     

The ID of the option. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/quizzes

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes?per_page=20&page=1&is_public=1&search=math&created_at_gte=2024-01-01&created_at_lte=2024-12-31&tags=difficulty%3Ahard%2Ctopic%3Amath&tag_mode=and&languages=de%2Cde-DE&subjects=chemistry%2CRobotik&difficulties=easy&status=draft%2Cnone&published=1&has_versions=1&has_questions=1&include_summary=1&sort=-versions_count%2Ctitle.de" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes"
);

const params = {
    "per_page": "20",
    "page": "1",
    "is_public": "1",
    "search": "math",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "tags": "difficulty:hard,topic:math",
    "tag_mode": "and",
    "languages": "de,de-DE",
    "subjects": "chemistry,Robotik",
    "difficulties": "easy",
    "status": "draft,none",
    "published": "1",
    "has_versions": "1",
    "has_questions": "1",
    "include_summary": "1",
    "sort": "-versions_count,title.de",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'is_public' => '1',
            'search' => 'math',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'tags' => 'difficulty:hard,topic:math',
            'tag_mode' => 'and',
            'languages' => 'de,de-DE',
            'subjects' => 'chemistry,Robotik',
            'difficulties' => 'easy',
            'status' => 'draft,none',
            'published' => '1',
            'has_versions' => '1',
            'has_questions' => '1',
            'include_summary' => '1',
            'sort' => '-versions_count,title.de',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 15. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

is_public   boolean  optional    

Filter by public status. Example: true

search   string  optional    

Literal search in slugs, in titles and descriptions in any language, in subjects and the difficulty as stored or by their German or English label, or an exact ID; with draft access also in the status of the latest version, as stored or by its German or English label. Example: math

created_at_gte   string  optional    

Filter quizzes created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter quizzes created on or before date. Example: 2024-12-31

tags   string  optional    

Comma-separated tag slugs. Example: difficulty:hard,topic:math

tag_mode   string  optional    

Tag matching mode: 'and' (all tags) or 'or' (any tag). Defaults to 'or'. Example: and

languages   string  optional    

Comma-separated title languages; a quiz matches with any of them. Example: de,de-DE

subjects   string  optional    

Comma-separated subjects from the metadata; any known spelling of a base subject finds its key. Example: chemistry,Robotik

difficulties   string  optional    

Comma-separated difficulties from the metadata. Example: easy

status   string  optional    

Comma-separated statuses of the latest version; none matches quizzes without versions. Needs draft access. Example: draft,none

published   boolean  optional    

Whether a version is published. Needs draft access. Example: true

has_versions   boolean  optional    

Whether the quiz has versions. Needs draft access. Example: true

has_questions   boolean  optional    

Whether the latest version schedules questions. Needs draft access. Example: true

include_summary   boolean  optional    

Add each quiz's summary: version count, latest version number and status, published version number and the number of questions an attempt on the latest version is given. Needs draft access. Example: true

sort   string  optional    

Sort fields in order of priority, comma-separated: created_at, updated_at, slug, title.de, title.en (the title the admin shows in that language), subject (a base subject by its label in the request's language, from Accept-Language, German or English, by default German; another subject by its text), difficulty (easy, medium, difficult, then other values), languages (the title's language codes), and with draft access versions_count, latest_status, published_version_number or questions_count. Prefix each with '-' for descending; quizzes without a value come last either way, ties keep creation order. Example: -versions_count,title.de

GET v1/quizzes/{id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the quiz. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/quizzes/{slug}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the quiz. Example: architecto

GET v1/quizzes/{quiz_id}/versions

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/{quiz_id}/versions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quiz_id   string     

The ID of the quiz. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/quizzes/{quiz_id}/versions/latest-published

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions/latest-published" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions/latest-published"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions/latest-published';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/{quiz_id}/versions/latest-published

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quiz_id   string     

The ID of the quiz. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/quizzes/versions/{version_id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/versions/{version_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

The version as a learner sees it before and around an attempt: sections, information pages and question slots without their question options.

question_count is the number of questions an attempt is given. The questions themselves are delivered by the attempt.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/outline" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/outline"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/outline';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/versions/{version_id}/outline

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

List a version's feedback bands.

With ?score_percent=, the list is narrowed to the single band that covers that result, so a client never has to reimplement the range rule.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/feedback-bands?score_percent=17" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"score_percent\": 1
}"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/feedback-bands"
);

const params = {
    "score_percent": "17",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "score_percent": 1
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/feedback-bands';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'score_percent' => '17',
        ],
        'json' => [
            'score_percent' => 1,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Feedback band retrieved successfully",
    "data": [
        {
            "id": "01k8a2b3c4d5e6f7g8h9j0k1m2",
            "quiz_version_id": "01k8a2b3c4d5e6f7g8h9j0k1m3",
            "min_percent": 0,
            "max_percent": 50,
            "content": [
                {
                    "type": "paragraph",
                    "text": {
                        "de": "Hier lohnt sich eine Wiederholung."
                    }
                }
            ],
            "summary": {
                "de": "Da geht noch was.",
                "en": "There is room to grow."
            },
            "sort_order": 0,
            "metadata": null,
            "created_at": "2026-10-23T09:00:00+00:00"
        }
    ],
    "meta": {
        "timestamp": "2026-10-23T09:00:00+00:00"
    }
}
 

Request      

GET v1/quizzes/versions/{version_id}/feedback-bands

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Query Parameters

score_percent   number  optional    

Only the band covering this result in percent (0–100). Example: 17

Body Parameters

score_percent   number  optional    

Must be at least 0. Must not be greater than 100. Example: 1

Response

Response Fields

summary   object|null     

One plain-text sentence per language code about a result in the band's range, or null when the author wrote none.

GET v1/quizzes/sections/{section_id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/sections/{section_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

section_id   string     

The ID of the section. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/quizzes/slots/{slot_id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/quizzes/slots/{slot_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slot_id   string     

The ID of the slot. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/quizzes

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

PUT v1/quizzes/{id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the quiz. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/quizzes/{id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/quizzes/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the quiz. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

PUT v1/quizzes/{slug}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/architecto';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the quiz. Example: architecto

DELETE v1/quizzes/{slug}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/quizzes/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/quizzes/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the quiz. Example: architecto

POST v1/quizzes/{quiz_id}/versions

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/58C6f7nnFCJ9M6rjyc6rTKMzn6/versions';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/{quiz_id}/versions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quiz_id   string     

The ID of the quiz. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

PUT v1/quizzes/versions/{version_id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/versions/{version_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Publish a draft. The version comes back with `warnings`: problems that did not keep it from being published (survey slot candidates that are not active).

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/publish" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/publish"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/publish';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/versions/{version_id}/publish

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/quizzes/versions/{version_id}/archive

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/versions/{version_id}/archive

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/quizzes/versions/{version_id}/duplicate

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/duplicate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/duplicate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/duplicate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/versions/{version_id}/duplicate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/quizzes/versions/{version_id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/quizzes/versions/{version_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Compare two quiz versions.

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/versions/compare" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"old_version_id\": \"architecto\",
    \"new_version_id\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/compare"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "old_version_id": "architecto",
    "new_version_id": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/compare';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'old_version_id' => 'architecto',
            'new_version_id' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/versions/compare

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

old_version_id   string     

Example: architecto

new_version_id   string     

Example: architecto

POST v1/quizzes/versions/{version_id}/sections

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/sections" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/sections"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/sections';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/versions/{version_id}/sections

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

PUT v1/quizzes/sections/{section_id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/sections/{section_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

section_id   string     

The ID of the section. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/quizzes/sections/{section_id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/quizzes/sections/{section_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

section_id   string     

The ID of the section. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/quizzes/versions/{version_id}/sections/reorder

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/sections/reorder" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"section_ids\": [
        \"bngzmiyvdljnikhwaykcmyuwpw\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/sections/reorder"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "section_ids": [
        "bngzmiyvdljnikhwaykcmyuwpw"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/sections/reorder';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'section_ids' => [
                'bngzmiyvdljnikhwaykcmyuwpw',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/versions/{version_id}/sections/reorder

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

section_ids   string[]     

Must be 26 characters.

Replace the version's whole band set.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/feedback-bands" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"bands\": [
        {
            \"min_percent\": 0,
            \"max_percent\": 50,
            \"content\": [
                []
            ],
            \"summary\": [],
            \"sort_order\": 16,
            \"metadata\": []
        }
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/feedback-bands"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "bands": [
        {
            "min_percent": 0,
            "max_percent": 50,
            "content": [
                []
            ],
            "summary": [],
            "sort_order": 16,
            "metadata": []
        }
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/feedback-bands';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'bands' => [
                [
                    'min_percent' => 0.0,
                    'max_percent' => 50.0,
                    'content' => [
                        [],
                    ],
                    'summary' => [],
                    'sort_order' => 16,
                    'metadata' => [],
                ],
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/versions/{version_id}/feedback-bands

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

version_id   string     

The ID of the version. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

bands   object[]     

The complete band set; an empty list removes every band.

min_percent   number     

Lower bound in percent, inclusive. Example: 0

max_percent   number     

Upper bound in percent, exclusive; the band reaching 100 also covers 100. Example: 50

content   object[]     

Blocks shown on the result page.

summary   object  optional    

Optional short sentence per language code about a result in this range, e.g. {"de": "Solide Basis, ein bisschen was geht noch."}. Plain text without markup, at most 200 characters per language. Texts are trimmed and blank ones dropped; a summary without texts is stored as null.

sort_order   integer  optional    

Ignored; bands are stored lowest range first. Example: 16

metadata   object  optional    

Free-form band settings.

POST v1/quizzes/sections/{section_id}/slots

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/sections/{section_id}/slots

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

section_id   string     

The ID of the section. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

PUT v1/quizzes/slots/{slot_id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/slots/{slot_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slot_id   string     

The ID of the slot. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/quizzes/slots/{slot_id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/quizzes/slots/{slot_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slot_id   string     

The ID of the slot. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/quizzes/sections/{section_id}/slots/reorder

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots/reorder" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"slot_ids\": [
        \"bngzmiyvdljnikhwaykcmyuwpw\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots/reorder"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "slot_ids": [
        "bngzmiyvdljnikhwaykcmyuwpw"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/sections/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots/reorder';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'slot_ids' => [
                'bngzmiyvdljnikhwaykcmyuwpw',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/sections/{section_id}/slots/reorder

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

section_id   string     

The ID of the section. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

slot_ids   string[]     

Must be 26 characters.

Bulk create slot options for a slot.

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6/slot-options/bulk" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"options\": [
        {
            \"original_question_id\": \"b\",
            \"points\": 39
        }
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6/slot-options/bulk"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "options": [
        {
            "original_question_id": "b",
            "points": 39
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6/slot-options/bulk';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'options' => [
                [
                    'original_question_id' => 'b',
                    'points' => 39,
                ],
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/slots/{slot_id}/slot-options/bulk

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slot_id   string     

The ID of the slot. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

options   object[]     

Must have at least 1 items.

original_question_id   string     

Must be at least 26 characters. Must not be greater than 26 characters. Example: b

points   number  optional    

Must be at least 0. Example: 39

metadata   object  optional    

POST v1/quizzes/slots/{slot_id}/slot-options

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6/slot-options" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6/slot-options"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slots/58C6f7nnFCJ9M6rjyc6rTKMzn6/slot-options';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/slots/{slot_id}/slot-options

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slot_id   string     

The ID of the slot. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

PUT v1/quizzes/slot-options/{option_id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/quizzes/slot-options/{option_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

option_id   string     

The ID of the option. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/quizzes/slot-options/{option_id}/refresh

Example request:
curl --request POST \
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6/refresh" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6/refresh"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6/refresh';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/quizzes/slot-options/{option_id}/refresh

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

option_id   string     

The ID of the option. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/quizzes/slot-options/{option_id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/quizzes/slot-options/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/quizzes/slot-options/{option_id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

option_id   string     

The ID of the option. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Prometheus scrape endpoint.

Returns metrics in Prometheus text exposition format.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/metrics/prometheus" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/metrics/prometheus"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/metrics/prometheus';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):

Show headers
content-type: text/plain; version=0.0.4; charset=utf-8
cache-control: no-cache, private
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

# HELP mintfit_system_php_memory_bytes Current PHP memory usage in bytes
# TYPE mintfit_system_php_memory_bytes gauge
mintfit_system_php_memory_bytes{type="peak"} 82313216
mintfit_system_php_memory_bytes{type="used"} 82313216
# HELP mintfit_system_queue_jobs_pending Number of pending jobs in the queue
# TYPE mintfit_system_queue_jobs_pending gauge
mintfit_system_queue_jobs_pending{queue="default"} 0
mintfit_system_queue_jobs_pending{queue="imports"} 0
mintfit_system_queue_jobs_pending{queue="notifications"} 0
mintfit_system_queue_jobs_pending{queue="pdf"} 0
mintfit_system_queue_jobs_pending{queue="statistics"} 0

 

Request      

GET v1/metrics/prometheus

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

JSON metrics endpoint for debugging.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/metrics/json" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/metrics/json"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/metrics/json';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/metrics/json

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List all metric definitions.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/metrics/definitions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/metrics/definitions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/metrics/definitions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/metrics/definitions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List registered providers.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/metrics/providers" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/metrics/providers"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/metrics/providers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/metrics/providers

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET v1/moodle-import

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/moodle-import" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/moodle-import"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/moodle-import';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/moodle-import

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET v1/moodle-import/{id}/results

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/moodle-import/58C6f7nnFCJ9M6rjyc6rTKMzn6/results" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/moodle-import/58C6f7nnFCJ9M6rjyc6rTKMzn6/results"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/moodle-import/58C6f7nnFCJ9M6rjyc6rTKMzn6/results';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/moodle-import/{id}/results

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the moodle import. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/surveys/settings

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/settings" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/settings"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/settings';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/settings

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

PUT v1/surveys/settings

Example request:
curl --request PUT \
    "http://localhost:8000/v1/surveys/settings" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/settings"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/settings';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/surveys/settings

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

POST v1/surveys/categories

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/categories" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/categories"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/categories';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/categories

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

PUT v1/surveys/categories/{id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/surveys/categories/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/categories/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/categories/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/surveys/categories/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the category. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/surveys/categories/{id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/surveys/categories/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/categories/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/categories/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/surveys/categories/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the category. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/surveys/questions

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/questions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/questions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

PUT v1/surveys/questions/{id}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/surveys/questions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/surveys/questions/{id}/archive

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/questions/{id}/archive

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/surveys/questions/{id}/activate

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/activate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/activate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/activate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/questions/{id}/activate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/surveys/questions/{id}/copy

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/copy" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/copy"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/copy';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/questions/{id}/copy

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

DELETE v1/surveys/questions/{id}

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/surveys/questions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/surveys/selection-check

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/selection-check" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"place\": \"architecto\",
    \"quiz_id\": \"architecto\",
    \"quiz_version_id\": \"architecto\",
    \"slot_id\": \"architecto\",
    \"package_id\": \"architecto\",
    \"refused_category_ids\": [
        \"architecto\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/selection-check"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "place": "architecto",
    "quiz_id": "architecto",
    "quiz_version_id": "architecto",
    "slot_id": "architecto",
    "package_id": "architecto",
    "refused_category_ids": [
        "architecto"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/selection-check';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'place' => 'architecto',
            'quiz_id' => 'architecto',
            'quiz_version_id' => 'architecto',
            'slot_id' => 'architecto',
            'package_id' => 'architecto',
            'refused_category_ids' => [
                'architecto',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/selection-check

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

place   string     

Example: architecto

quiz_id   string  optional    

Example: architecto

quiz_version_id   string  optional    

This field is required when place is in_test. Example: architecto

slot_id   string  optional    

This field is required when place is in_test. Example: architecto

package_id   string  optional    

Example: architecto

answers   object  optional    
refused_category_ids   string[]  optional    

GET v1/surveys/categories

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/categories" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/categories"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/categories';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/categories

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET v1/surveys/questions

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/questions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"category_id\": \"architecto\",
    \"search\": \"n\",
    \"per_page\": 7,
    \"page\": 66
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "category_id": "architecto",
    "search": "n",
    "per_page": 7,
    "page": 66
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'category_id' => 'architecto',
            'search' => 'n',
            'per_page' => 7,
            'page' => 66,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/questions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

status   string  optional    
category_id   string  optional    

Example: architecto

search   string  optional    

Must not be greater than 200 characters. Example: n

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 7

page   integer  optional    

Must be at least 1. Example: 66

GET v1/surveys/questions/{id}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/questions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/surveys/questions/{id}/results

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/results" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"revision\": \"(all\",
    \"from\": \"2026-11-03T00:49:05\",
    \"to\": \"2026-11-03T00:49:05\",
    \"quiz_id\": \"architecto\",
    \"package_id\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/results"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "revision": "(all",
    "from": "2026-11-03T00:49:05",
    "to": "2026-11-03T00:49:05",
    "quiz_id": "architecto",
    "package_id": "architecto"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/results';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'revision' => '(all',
            'from' => '2026-11-03T00:49:05',
            'to' => '2026-11-03T00:49:05',
            'quiz_id' => 'architecto',
            'package_id' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/questions/{id}/results

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

revision   string  optional    

Must match the regex /^(all. Example: (all

from   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

to   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

quiz_id   string  optional    

Example: architecto

package_id   string  optional    

Example: architecto

place   string  optional    

The raw answers as CSV (UTF-8 with byte order mark, for spreadsheets).

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/export" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"question_ids\": [
        \"architecto\"
    ],
    \"from\": \"2026-11-03T00:49:05\",
    \"to\": \"2026-11-03T00:49:05\"
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/export"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "question_ids": [
        "architecto"
    ],
    "from": "2026-11-03T00:49:05",
    "to": "2026-11-03T00:49:05"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/export';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'question_ids' => [
                'architecto',
            ],
            'from' => '2026-11-03T00:49:05',
            'to' => '2026-11-03T00:49:05',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/export

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

question_ids   string[]  optional    
from   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

to   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

GET v1/surveys/export/codebook

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/export/codebook" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"question_ids\": [
        \"architecto\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/export/codebook"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "question_ids": [
        "architecto"
    ]
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/export/codebook';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'question_ids' => [
                'architecto',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/export/codebook

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

question_ids   string[]  optional    

GET v1/surveys/questions/options

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/questions/options" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/questions/options"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/questions/options';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/questions/options

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Prepare a new test start: the card before the test, if any.

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/preparations" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"quiz_version_id\": \"architecto\",
    \"package_id\": \"architecto\",
    \"package_activity_id\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/preparations"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "quiz_version_id": "architecto",
    "package_id": "architecto",
    "package_activity_id": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/preparations';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'quiz_version_id' => 'architecto',
            'package_id' => 'architecto',
            'package_activity_id' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/preparations

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

quiz_version_id   string     

Example: architecto

package_id   string  optional    

This field is required when package_activity_id is present. Example: architecto

package_activity_id   string  optional    

This field is required when package_id is present. Example: architecto

The in-test cards of the caller's attempt, by survey slot.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/attempts/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/attempts/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/attempts/58C6f7nnFCJ9M6rjyc6rTKMzn6/slots';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/attempts/{attemptId}/slots

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

attemptId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

The card at a place after a test or in a topic area, or null.

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/places/before_result|after_result|topic_return|topic_complete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/places/before_result|after_result|topic_return|topic_complete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/places/before_result|after_result|topic_return|topic_complete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/places/{place}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

place   string     

The place. Example: before_result|after_result|topic_return|topic_complete

POST v1/surveys/deliveries/{id}/shown

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/deliveries/58C6f7nnFCJ9M6rjyc6rTKMzn6/shown" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/deliveries/58C6f7nnFCJ9M6rjyc6rTKMzn6/shown"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/deliveries/58C6f7nnFCJ9M6rjyc6rTKMzn6/shown';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/deliveries/{id}/shown

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the delivery. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

POST v1/surveys/deliveries/{id}/response

Example request:
curl --request POST \
    "http://localhost:8000/v1/surveys/deliveries/58C6f7nnFCJ9M6rjyc6rTKMzn6/response" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"outcome\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/deliveries/58C6f7nnFCJ9M6rjyc6rTKMzn6/response"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "outcome": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/deliveries/58C6f7nnFCJ9M6rjyc6rTKMzn6/response';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'outcome' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/surveys/deliveries/{id}/response

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the delivery. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

outcome   string     

Example: architecto

answer   object  optional    

GET v1/surveys/preferences

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/surveys/preferences" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/surveys/preferences"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/preferences';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/surveys/preferences

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

PUT v1/surveys/preferences/{categoryId}

Example request:
curl --request PUT \
    "http://localhost:8000/v1/surveys/preferences/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"refused\": true
}"
const url = new URL(
    "http://localhost:8000/v1/surveys/preferences/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "refused": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/surveys/preferences/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'refused' => true,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/surveys/preferences/{categoryId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

categoryId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

refused   boolean     

Example: true

How transfers work in the acting tenant: this instance's label and whether it is production, the tenant, the upload limit, how long archives stay, the archive format and what the caller may do.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/transfer/config" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/config"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/config';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/transfer/config

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Runs of the acting tenant the caller may see (exports with `transfer.export`, imports with `transfer.import`), newest first, with counts per kind and status group in `meta.facets`.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/transfer/runs" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/transfer/runs

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

GET v1/transfer/runs/{run}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/transfer/runs/{run}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Cancel an import that is not being applied; its archive is deleted.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/cancel" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/cancel"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/cancel';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/runs/{run}/cancel

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Start the next generation of a failed run: validation again, a new plan after a failed analysis or apply, or the export again.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/retry" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/retry"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/retry';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/runs/{run}/retry

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

The archive of a completed export, while it is kept.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/archive';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/transfer/runs/{run}/archive

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

What an export of the selection would bring, computed now.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/exports/preview" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/exports/preview"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/exports/preview';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/exports/preview

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Start an export run; its archive is built in the background.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/exports" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/exports"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/exports';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/exports

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Upload an archive (multipart `file`): it is stored and validated in the background.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/imports" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/imports"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/imports';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/imports

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Plan a validated import with its options; a changed mode discards the decisions.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/plan" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/plan"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/plan';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/runs/{run}/plan

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Analyze a planned or stale import again; decisions carry over where they fit.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/replan" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/replan"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/replan';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/runs/{run}/replan

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Change whether new topic areas, pages and ads stay drafts; the plan is resolved again.

Example request:
curl --request PATCH \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/options" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/options"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/options';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PATCH v1/transfer/runs/{run}/options

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

A page of the plan's or report's items, decisions first.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/items" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/items"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/items';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/transfer/runs/{run}/items

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

GET v1/transfer/runs/{run}/items/{item}

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/items/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/items/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/items/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/transfer/runs/{run}/items/{item}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

item   string     

The item. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Decide items of a ready plan, one by one or several of one kind with one choice; the plan is resolved again for the caller.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/decisions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/decisions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/decisions';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/transfer/runs/{run}/decisions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Resolve the stored plan for the caller.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/resolve" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/resolve"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/resolve';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/runs/{run}/resolve

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Confirm the plan as the caller saw it and queue the import.

Example request:
curl --request POST \
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/apply" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/apply"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/transfer/runs/58C6f7nnFCJ9M6rjyc6rTKMzn6/apply';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/transfer/runs/{run}/apply

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

run   string     

The run. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Evaluation

Get manual grading queue.

requires authentication

Returns paginated list of assignments awaiting grading.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/evaluation/manual-grading" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"pending\",
    \"per_page\": 1
}"
const url = new URL(
    "http://localhost:8000/v1/evaluation/manual-grading"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "status": "pending",
    "per_page": 1
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluation/manual-grading';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'status' => 'pending',
            'per_page' => 1,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/evaluation/manual-grading

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

status   string  optional    

Example: pending

Must be one of:
  • pending
  • in_progress
  • graded
assigned_to   string  optional    
attempt_id   string  optional    
per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

Get manual grading assignment details.

requires authentication

Returns full context for grading (question, answer, rubric).

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/evaluation/manual-grading/{assignmentId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

assignmentId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Submit manual grade for assignment.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6/grade" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"points_awarded\": 27,
    \"feedback\": \"n\",
    \"rubric_scores\": [
        84
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6/grade"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "points_awarded": 27,
    "feedback": "n",
    "rubric_scores": [
        84
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6/grade';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'points_awarded' => 27,
            'feedback' => 'n',
            'rubric_scores' => [
                84,
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/evaluation/manual-grading/{assignmentId}/grade

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

assignmentId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

points_awarded   number     

Must be at least 0. Example: 27

feedback   string  optional    

Must not be greater than 10000 characters. Example: n

rubric_scores   number[]  optional    

Must be at least 0.

Assign grading assignment to instructor.

requires authentication

Example request:
curl --request PUT \
    "http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6/assign" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"assigned_to\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6/assign"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "assigned_to": "architecto"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluation/manual-grading/58C6f7nnFCJ9M6rjyc6rTKMzn6/assign';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'assigned_to' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/evaluation/manual-grading/{assignmentId}/assign

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

assignmentId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

assigned_to   string     

Example: architecto

Regrade a single attempt.

requires authentication

Resets scores and re-dispatches grading job. Useful for fixing grading errors or updating after config changes. Only attempts on quizzes owned by the caller's tenant can be regraded; any other attempt is reported as not found.

Example request:
curl --request POST \
    "http://localhost:8000/v1/evaluation/attempts/58C6f7nnFCJ9M6rjyc6rTKMzn6/regrade" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluation/attempts/58C6f7nnFCJ9M6rjyc6rTKMzn6/regrade"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluation/attempts/58C6f7nnFCJ9M6rjyc6rTKMzn6/regrade';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/evaluation/attempts/{attemptId}/regrade

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

attemptId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Regrade all attempts for a quiz version.

requires authentication

Batch regrading for all submitted attempts of a quiz version. Useful after updating grading configuration.

Example request:
curl --request POST \
    "http://localhost:8000/v1/evaluation/quiz-versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/regrade" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluation/quiz-versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/regrade"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluation/quiz-versions/58C6f7nnFCJ9M6rjyc6rTKMzn6/regrade';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/evaluation/quiz-versions/{versionId}/regrade

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

versionId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

List all evaluations

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/evaluations" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluations"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluations';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/evaluations

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Create new evaluation

Example request:
curl --request POST \
    "http://localhost:8000/v1/evaluations" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluations"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluations';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/evaluations

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Show evaluation details

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/evaluations/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the evaluation. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update evaluation

Example request:
curl --request PUT \
    "http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/evaluations/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the evaluation. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete evaluation

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/evaluations/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/evaluations/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the evaluation. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Health

Check API Health

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/health" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/health"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/health';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "status": "healthy"
}
 

Example response (503):


{
    "status": "unhealthy"
}
 

Request      

GET v1/health

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Identity

Register new user Creates a new user account with identity (password). Requires at least one identifier (email, phone, or username) plus a password. `profile` accepts what account holders change about themselves (see `PATCH /v1/users/me`) except the avatar. Tenants that admit learners through single sign-on only refuse it with 403 and `errors.code` = `local_login_disabled`.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/register" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/register"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/register';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{
    "success": true,
    "message": "Registration successful",
    "data": {
        "user": {
            "id": "01kav3fcmme10dwbeexyqkfndn",
            "email": "user@example.com",
            "tenant_id": "01kav3fbpbpb3wnhtadsmrmt7m"
        },
        "tokens": {
            "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "token_type": "Bearer",
            "expires_in": 86400
        }
    }
}
 

Request      

POST v1/identity/register

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Login with credentials Authenticate with email/phone/username and password. Returns JWT access token (24h) and refresh token (30d). An account accepts 10 attempts per hour, counted together for its email address, username and phone number in any letter case; a successful login starts the count over. Accounts without a password are refused like a wrong password. Tenants that admit learners through single sign-on only accept passwords from their staff (any role other than the default role); everyone else gets 403 with `errors.code` = `local_login_disabled`.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/login" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/login"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/login';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Login successful",
    "data": {
        "user": {
            "id": "01kav3fcmme10dwbeexyqkfndn",
            "email": "user@example.com"
        },
        "tokens": {
            "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "token_type": "Bearer",
            "expires_in": 86400
        }
    }
}
 

Request      

POST v1/identity/login

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Refresh access token Exchange refresh token for new access and refresh tokens. Implements OAuth2 token rotation: the old refresh token is used up. Each refresh token renews once; requests sending it until 30 seconds after its first renewal, as tabs renewing together do, receive the same new tokens. A used refresh token sent later counts as copied and ends its login. Refresh tokens of a login that was signed out or ended by the account (password reset or change, email change, signing out everywhere) are refused with 401, as is renewing a login older than the login lifetime (`JWT_LOGIN_LIFETIME`, no limit by default); its holder signs in again. A renewal that takes longer than a request may run answers 503 and does not end the login. Its refresh token may have been used up by another request meanwhile, so read the stored tokens again before renewing again.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/refresh" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/refresh"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/refresh';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Token refreshed successfully",
    "data": {
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
        "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
        "token_type": "Bearer",
        "expires_in": 86400
    }
}
 

Request      

POST v1/identity/refresh

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Sign out Ends the login the refresh token belongs to: its access and refresh tokens are refused from now on. The account's other logins continue. Send the refresh token in the body and no Authorization header, as for a refresh. Signing out a login again succeeds as well.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/logout" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/logout"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/logout';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Signed out",
    "data": null
}
 

Example response (401):


{
    "success": false,
    "message": "Invalid or expired refresh token"
}
 

Request      

POST v1/identity/logout

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Anonymous login Generate anonymous session token without creating database records. Returns a tracking ID and tokens that can be used to link requests across modules. User can later "upgrade" to authenticated by registering with this ID.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/anonymous" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/anonymous"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/anonymous';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Anonymous session created",
    "data": {
        "anonymous_id": "01kav3fcmme10dwbeexyqkfndn",
        "tokens": {
            "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "token_type": "Bearer",
            "expires_in": 86400
        }
    }
}
 

Request      

POST v1/identity/anonymous

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Claim anonymous account Converts anonymous session to authenticated account by claiming it with credentials. Requires anonymous token in Authorization header. Creates User and Identity records with the anonymous ID. Preserves data linkage across modules via shared ID. The anonymous tokens stop working; continue with the returned ones. `profile` accepts what account holders change about themselves (see `PATCH /v1/users/me`) except the avatar.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/anonymous/claim" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/anonymous/claim"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/anonymous/claim';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Account upgraded successfully",
    "data": {
        "user": {
            "id": "01kav3fcmme10dwbeexyqkfndn",
            "email": "user@example.com",
            "is_anonymous": false
        },
        "tokens": {
            "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "token_type": "Bearer",
            "expires_in": 86400
        }
    }
}
 

Example response (401):


{
    "success": false,
    "message": "Missing or invalid authorization token"
}
 

Example response (409):


{
    "success": false,
    "message": "Anonymous account has already been upgraded"
}
 

Request      

POST v1/identity/anonymous/claim

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

anonymous   string     

Anonymous identifier (from route: /identity/anonymous/claim) Example: architecto

Change password Change password for authenticated user. Requires current password for verification. Protected route - requires valid access token. Ends the account's other logins: their access and refresh tokens are refused from now on. The login making the request stays signed in.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/change-password" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/change-password"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/change-password';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Password changed successfully",
    "data": null
}
 

Request      

POST v1/identity/change-password

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Set a password Gives a password to an account that signs in through single sign-on only, as an additional way to sign in. Uses the password rules of registration. Refused with 409 (`password_already_set`) when the account has a password and with 403 (`local_login_disabled`) in tenants that admit learners through single sign-on only.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/password/set" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/password/set"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/password/set';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Password set successfully",
    "data": null
}
 

Request      

POST v1/identity/password/set

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Sign out everywhere Ends all logins of the account, including the current one: every access and refresh token issued so far is refused from now on.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/logout-all" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/logout-all"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/logout-all';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Signed out everywhere",
    "data": null
}
 

Request      

POST v1/identity/logout-all

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Login Methods

List login methods The password and the linked external accounts of the signed-in account, and the providers it can still add.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/identity/login-methods" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/login-methods"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/login-methods';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Login methods retrieved successfully",
    "data": {
        "has_password": true,
        "can_set_password": false,
        "methods": [
            {
                "id": "01J...",
                "provider": {
                    "key": "google",
                    "display_name": "Google",
                    "driver": "google",
                    "icon": "ri-google-fill"
                },
                "email": "anna@gmail.com",
                "created_at": "2026-10-14T10:00:00+00:00",
                "last_login_at": null,
                "usable": true
            }
        ],
        "available": [
            {
                "key": "apple",
                "display_name": "Apple",
                "driver": "apple",
                "icon": "ri-apple-fill",
                "kind": "platform"
            }
        ]
    }
}
 

Request      

GET v1/identity/login-methods

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Claim a pending external account After `sso_error=account_exists` (or `email_required`), the person signs in as usual and links the external account to it with the pending ticket.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/login-methods/claim" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"pending\": \"architecto\",
    \"browser_secret\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/identity/login-methods/claim"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "pending": "architecto",
    "browser_secret": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/login-methods/claim';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'pending' => 'architecto',
            'browser_secret' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (409):


{
    "success": false,
    "message": "This external account belongs to another account.",
    "errors": {
        "code": "already_linked_other"
    }
}
 

Request      

POST v1/identity/login-methods/claim

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

pending   string     

The sso_pending ticket from the callback. Example: architecto

browser_secret   string     

The secret behind the sign-in's browser_challenge. Example: architecto

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/login-methods/link-intent" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"provider\": \"google\",
    \"return_to\": \"http:\\/\\/localhost:3001\\/profile\"
}"
const url = new URL(
    "http://localhost:8000/v1/identity/login-methods/link-intent"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "provider": "google",
    "return_to": "http:\/\/localhost:3001\/profile"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/login-methods/link-intent';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'provider' => 'google',
            'return_to' => 'http://localhost:3001/profile',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Link started",
    "data": {
        "redirect_url": "http://localhost:8000/v1/sso/redirect?tenant=...&provider=google&intent=link&ticket=..."
    }
}
 

Remove a login method Refused with 422 (`last_login_method`) when it is the account's last way to sign in that works under the tenant's login mode.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/identity/login-methods/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/identity/login-methods/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/login-methods/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (422):


{
    "success": false,
    "message": "The last way to sign in cannot be removed.",
    "errors": {
        "code": "last_login_method"
    }
}
 

Request      

DELETE v1/identity/login-methods/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the login method. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Start signing in again Confirms the person with one of the account's linked providers, for deleting an account without password. Returns the URL to navigate the browser to. The browser returns to `return_to` with `sso_reauth`, valid for five minutes, or with `sso_error`.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/sso/reauth-intent" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"provider\": \"google\",
    \"return_to\": \"http:\\/\\/localhost:3001\\/profile\"
}"
const url = new URL(
    "http://localhost:8000/v1/identity/sso/reauth-intent"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "provider": "google",
    "return_to": "http:\/\/localhost:3001\/profile"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/sso/reauth-intent';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'provider' => 'google',
            'return_to' => 'http://localhost:3001/profile',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Sign-in started",
    "data": {
        "redirect_url": "http://localhost:8000/v1/sso/redirect?tenant=...&provider=google&intent=reauth&ticket=..."
    }
}
 

Request      

POST v1/identity/sso/reauth-intent

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

provider   string     

Key of a linked provider. Example: google

return_to   string     

Absolute frontend URL to return to. Example: http://localhost:3001/profile

Delete the account after signing in again For accounts without password, which cannot confirm the deletion with one. Needs the `sso_reauth` proof of a fresh sign-in.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/identity/sso/account" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reauth\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/identity/sso/account"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reauth": "architecto"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/sso/account';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'reauth' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):


{
    "success": false,
    "message": "This sign-in has expired or was already used. Please start again.",
    "errors": {
        "code": "expired"
    }
}
 

Request      

DELETE v1/identity/sso/account

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

reauth   string     

The sso_reauth value from the callback. Example: architecto

Media Aliases

Resolve alias for current user/tenant (e.g., user-avatar/current)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/alias/architecto/current" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/alias/architecto/current"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/alias/architecto/current';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (404):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "No media found for alias 'architecto/current'",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/alias/{type}/current

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

type   string     

Example: architecto

Resolve alias for current user/tenant variant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/alias/architecto/current/variants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/alias/architecto/current/variants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/alias/architecto/current/variants/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (404):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "No media found for alias 'architecto/current'",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/alias/{type}/current/variants/{variant}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

type   string     

Example: architecto

variant   string     

The variant. Example: architecto

Resolve alias by specific ID (e.g., user-avatar/{userId})

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/alias/architecto/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/alias/architecto/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/alias/architecto/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/alias/{type}/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

type   string     

Example: architecto

id   string     

The ID of the {type}. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Resolve alias by specific ID with variant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/alias/architecto/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/alias/architecto/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/alias/architecto/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/alias/{type}/{id}/variants/{variant}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

type   string     

Example: architecto

id   string     

The ID of the {type}. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

variant   string     

The variant. Example: architecto

Resolve global alias (no related_id)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/alias/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/alias/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/alias/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/alias/{alias}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

alias   string     

The alias. Example: architecto

Resolve global alias with variant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/alias/architecto/variants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/alias/architecto/variants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/alias/architecto/variants/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/alias/{alias}/variants/{variant}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

alias   string     

The alias. Example: architecto

variant   string     

The variant. Example: architecto

Media Management

Upload new media file

Example request:
curl --request POST \
    "http://localhost:8000/v1/media" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/media

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Upload media from URL The URL must lead to a public http(s) host; addresses of private networks and redirects are refused with 422. The file has to meet the collection's file types and size limit, like a direct upload.

Example request:
curl --request POST \
    "http://localhost:8000/v1/media/from-url" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"url\": \"http:\\/\\/www.bailey.biz\\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html\",
    \"privacy\": \"tenant\",
    \"collection_name\": \"i\",
    \"title\": \"k\",
    \"description\": \"Aut ab provident perspiciatis quo omnis nostrum aut.\",
    \"alt_text\": \"w\",
    \"expires_at\": \"2026-11-03T00:49:05\"
}"
const url = new URL(
    "http://localhost:8000/v1/media/from-url"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "url": "http:\/\/www.bailey.biz\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html",
    "privacy": "tenant",
    "collection_name": "i",
    "title": "k",
    "description": "Aut ab provident perspiciatis quo omnis nostrum aut.",
    "alt_text": "w",
    "expires_at": "2026-11-03T00:49:05"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/from-url';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'url' => 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html',
            'privacy' => 'tenant',
            'collection_name' => 'i',
            'title' => 'k',
            'description' => 'Aut ab provident perspiciatis quo omnis nostrum aut.',
            'alt_text' => 'w',
            'expires_at' => '2026-11-03T00:49:05',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/media/from-url

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

url   string     

Must be a valid URL. Must not be greater than 2048 characters. Example: http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html

privacy   string  optional    

Example: tenant

Must be one of:
  • public
  • tenant
  • identity
collection_name   string  optional    

Must not be greater than 100 characters. Example: i

title   string  optional    

Must not be greater than 255 characters. Example: k

description   string  optional    

Must not be greater than 1000 characters. Example: Aut ab provident perspiciatis quo omnis nostrum aut.

alt_text   string  optional    

Must not be greater than 500 characters. Example: w

expires_at   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

Batch upload multiple media files

Example request:
curl --request POST \
    "http://localhost:8000/v1/media/batch" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"files\": null,
    \"privacy\": \"tenant\",
    \"collection_name\": \"question-block-images\",
    \"titles\": [
        \"Image 1\",
        \"Image 2\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/media/batch"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "files": null,
    "privacy": "tenant",
    "collection_name": "question-block-images",
    "titles": [
        "Image 1",
        "Image 2"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/batch';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'files' => null,
            'privacy' => 'tenant',
            'collection_name' => 'question-block-images',
            'titles' => [
                'Image 1',
                'Image 2',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{"success": true, "message": "3 files uploaded successfully", "data": {"media": [{"id": "01...", ...}], "errors": []}}
 

Request      

POST v1/media/batch

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

files   file[]     

Array of files to upload.

privacy   string     

Privacy level for all files. Example: tenant

collection_name   string  optional    

nullable Collection name for all files. Example: question-block-images

titles   string[]  optional    

nullable Array of titles (one per file).

List all media

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media?per_page=20&page=1&mime_type=image%2F&privacy=public&status=ready&collection_name=avatars&search=photo&created_at_gte=2024-01-01&created_at_lte=2024-12-31&sort=-created_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media"
);

const params = {
    "per_page": "20",
    "page": "1",
    "mime_type": "image/",
    "privacy": "public",
    "status": "ready",
    "collection_name": "avatars",
    "search": "photo",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "sort": "-created_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'mime_type' => 'image/',
            'privacy' => 'public',
            'status' => 'ready',
            'collection_name' => 'avatars',
            'search' => 'photo',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'sort' => '-created_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 20. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

mime_type   string  optional    

Filter by MIME type prefix. Example: image/

privacy   string  optional    

Filter by privacy level (public, tenant, identity). Example: public

status   string  optional    

Filter by status (processing, ready, failed). Example: ready

collection_name   string  optional    

Filter by collection name. Example: avatars

search   string  optional    

Search in name and file_name. Example: photo

created_at_gte   string  optional    

Filter media created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter media created on or before date. Example: 2024-12-31

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -created_at

Get media by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the medium. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Download media file binary Returns the raw file with its stored Content-Type. Formats a browser renders passively (pictures, film, sound, PDF) are served inline so question content can embed them with `<img src>`; anything else — an SVG above all, which is a script-capable document — is served as a download under a sandboxing CSP. Public media may be cached for good; other media is served with `no-store`, so every download is checked again. See MediaDelivery.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/download" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/download"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/download';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/{id}/download

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the medium. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update media metadata (file content is immutable) Only media of the caller's tenant can be changed. Another tenant's public media is refused with 403; media the caller cannot see returns 404.

Example request:
curl --request PATCH \
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PATCH v1/media/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the medium. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete media by ID (soft delete) Only media of the caller's tenant can be deleted. Another tenant's public media is refused with 403; media the caller cannot see returns 404.

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/media/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the medium. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Bulk delete media files Every ID must name media the caller may delete, as for a single delete. Otherwise the request fails with that media's 403 or 404 and nothing is deleted.

Example request:
curl --request POST \
    "http://localhost:8000/v1/media/bulk-delete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        \"architecto\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/media/bulk-delete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        "architecto"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/bulk-delete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'ids' => [
                'architecto',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/media/bulk-delete

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   string[]     

Get media usage statistics

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/stats" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/stats"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/stats';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/stats

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Cleanup expired media files of the caller's tenant

Example request:
curl --request POST \
    "http://localhost:8000/v1/media/cleanup" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/cleanup"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/cleanup';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/media/cleanup

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List all variants for a media file

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/{id}/variants

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the medium. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Download a specific variant Note: This endpoint returns a redirect to the actual file URL

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/media/58C6f7nnFCJ9M6rjyc6rTKMzn6/variants/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/media/{id}/variants/{variant}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the medium. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

variant   string     

The variant. Example: architecto

MoodleImport

Import a Moodle activity backup (.mbz)

Example request:
curl --request POST \
    "http://localhost:8000/v1/moodle-import" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/moodle-import"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/moodle-import';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/moodle-import

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Import a Moodle question bank export (qformat_xml) A different operation from importing an activity backup: the file holds questions and categories only, so no quiz, section or slot is created. The questions land in the tenant's question bank, tagged with their Moodle category, and re-importing the same file under the same `bank_key` updates them rather than adding a second copy.

Example request:
curl --request POST \
    "http://localhost:8000/v1/moodle-import/question-bank" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/moodle-import/question-bank"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/moodle-import/question-bank';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/moodle-import/question-bank

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get import run status

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/moodle-import/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/moodle-import/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/moodle-import/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/moodle-import/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the moodle import. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Notification Management

Get in-app notifications for current user

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/notifications" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/notifications"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/notifications

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

Mark all in-app notifications of the current user as read

Example request:
curl --request POST \
    "http://localhost:8000/v1/notifications/read-all" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/notifications/read-all"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications/read-all';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/notifications/read-all

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get an in-app notification of the current user by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/notifications/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/notifications/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/notifications/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the notification. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Mark an in-app notification of the current user as read

Example request:
curl --request PATCH \
    "http://localhost:8000/v1/notifications/58C6f7nnFCJ9M6rjyc6rTKMzn6/read" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/notifications/58C6f7nnFCJ9M6rjyc6rTKMzn6/read"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications/58C6f7nnFCJ9M6rjyc6rTKMzn6/read';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PATCH v1/notifications/{id}/read

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the notification. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Notification Preferences

Get user notification preferences

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/notifications/preferences" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/notifications/preferences"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications/preferences';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/notifications/preferences

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Update notification preferences With `enabled` false, no notification of the category reaches the user in this tenant. Without a preference, `info` notifications do not reach the user and those of the other categories do; a new preference without `enabled` starts from that default. Required categories (`critical`, such as account security emails) are delivered regardless.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/notifications/preferences" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/notifications/preferences"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications/preferences';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/notifications/preferences

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Delete notification preference (reset to defaults)

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/notifications/preferences" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"category\": \"info\"
}"
const url = new URL(
    "http://localhost:8000/v1/notifications/preferences"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "category": "info"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/notifications/preferences';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'category' => 'info',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/notifications/preferences

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

category   string     

Example: info

Must be one of:
  • critical
  • urgent
  • info
  • marketing

Pdf Management

List all available PDF templates Returns a list of all PDF templates available for document generation.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/pdf/templates" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/pdf/templates"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/pdf/templates';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "data": {
        "templates": [
            {
                "slug": "quiz_certificate",
                "name": "Quiz Certificate",
                "description": "..."
            }
        ]
    }
}
 

Request      

GET v1/pdf/templates

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get template details Returns detailed information about a specific PDF template including default variables and validation rules.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/pdf/templates/quiz_certificate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/pdf/templates/quiz_certificate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/pdf/templates/quiz_certificate';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": true,
    "message": "Template retrieved successfully",
    "data": {
        "slug": "quiz_certificate",
        "name": "Quiz Zertifikat",
        "description": "Zertifikat für erfolgreich absolvierte Tests. Empfohlene PDF-Optionen: margin_top=0, margin_bottom=0, margin_left=0, margin_right=0, print_background=true",
        "defaultVariables": {
            "student_name": "Ottilie Otterstedt",
            "student_email": "ottilie.otterstedt@mail.de",
            "quiz_title": "MINTfit Test",
            "score": "99",
            "stars": "5",
            "completion_date": "22.07.2024",
            "description_text": "hat den MINTfit Test erfolgreich absolviert.",
            "certificate_number": "CERT-2026-A7K2M",
            "verification_url": "https://mintfit.hamburg/certificate",
            "verification_display_url": "https://mintfit.hamburg/certificate",
            "quiz_color": "#e2b2ff",
            "quiz_label": "",
            "quiz_image_url": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgd2lkdGg9IjEzOC42ODciCiAgIGhlaWdodD0iMTA3LjQiCiAgIHZpZXdCb3g9IjAgMCAxMzguNjg3IDEwNy40IgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmcyNDEiCiAgIHNvZGlwb2RpOmRvY25hbWU9ImNvbXB1dGVyU2NpZW5jZS5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMi4yIChiMGE4NDg2NTQxLCAyMDIyLTEyLTAxKSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzMjQ1IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0ibmFtZWR2aWV3MjQzIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzAwMDAwMCIKICAgICBib3JkZXJvcGFjaXR5PSIwLjI1IgogICAgIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgICBpbmtzY2FwZTpkZXNrY29sb3I9IiNkMWQxZDEiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGlua3NjYXBlOnpvb209IjkuNTYyMzgzNSIKICAgICBpbmtzY2FwZTpjeD0iNDcuNTI5OTkxIgogICAgIGlua3NjYXBlOmN5PSI1My44MDQ1NzciCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIyNTYwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEzNTUiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjAiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJJbmZvcm1hdGlrIiAvPgogIDxnCiAgICAgaWQ9IkluZm9ybWF0aWsiCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTAuMDA1KSI+CiAgICA8ZwogICAgICAgaWQ9IkdydXBwZV8zIgogICAgICAgZGF0YS1uYW1lPSJHcnVwcGUgMyI+CiAgICAgIDxyZWN0CiAgICAgICAgIGlkPSJSZWNodGVja18xIgogICAgICAgICBkYXRhLW5hbWU9IlJlY2h0ZWNrIDEiCiAgICAgICAgIHdpZHRoPSIxMTMuMzYiCiAgICAgICAgIGhlaWdodD0iNjQuMzkiCiAgICAgICAgIHJ4PSI1IgogICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNC40IDEwLjk3KSIKICAgICAgICAgZmlsbD0iI2ZmZiIgLz4KICAgICAgPHBhdGgKICAgICAgICAgaWQ9IlJlY2h0ZWNrXzIiCiAgICAgICAgIGRhdGEtbmFtZT0iUmVjaHRlY2sgMiIKICAgICAgICAgZD0iTTUtMkgxMDguMzZhNy4wMDgsNy4wMDgsMCwwLDEsNyw3VjU5LjM5YTcuMDA4LDcuMDA4LDAsMCwxLTcsN0g1YTcuMDA4LDcuMDA4LDAsMCwxLTctN1Y1QTcuMDA4LDcuMDA4LDAsMCwxLDUtMlpNMTA4LjM2LDYyLjM5YTMsMywwLDAsMCwzLTNWNWEzLDMsMCwwLDAtMy0zSDVBMywzLDAsMCwwLDIsNVY1OS4zOWEzLDMsMCwwLDAsMywzWiIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQuNCAxMC45NykiCiAgICAgICAgIGZpbGw9IiMwMDRhNmIiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJQZmFkXzEyIgogICAgICAgICBkYXRhLW5hbWU9IlBmYWQgMTIiCiAgICAgICAgIGQ9Ik0xOC40NSw4Mi4zNmE5LjYxLDkuNjEsMCwwLDAtNy41MSw0LjMzTDIuNiwxMDEuMDhjLTEuMzksMi4zOS0uMjcsNC4zMiwyLjQ5LDQuMzJsMTI4LjMxLS4wMmMyLjc2LDAsNC4wNC0yLjAyLDIuODUtNC41MWwtNi42Ny0xMy45OWE4LjY0NCw4LjY0NCwwLDAsMC03LjE1LTQuNTFIMTguNDVaIgogICAgICAgICBmaWxsPSIjZmZmIiAvPgogICAgICA8cGF0aAogICAgICAgICBpZD0iUGZhZF8xMyIKICAgICAgICAgZGF0YS1uYW1lPSJQZmFkIDEzIgogICAgICAgICBkPSJNMTguNDUsODAuMzZxLjEsMCwuMi4wMUgxMjIuNDNhMTAuNjI4LDEwLjYyOCwwLDAsMSw4Ljk1NSw1LjY0OGw2LjY3MSwxMy45OTJhNC44ODIsNC44ODIsMCwwLDEtNC42NTUsNy4zNzFMNS4wOSwxMDcuNGE1LDUsMCwwLDEtNC41MzctMi4yNyw0Ljk4OCw0Ljk4OCwwLDAsMSwuMzE4LTUuMDU2TDkuMjEsODUuNjg3QTEwLjg4MSwxMC44ODEsMCwwLDEsMTMuMTgyLDgxLjksMTAuODMsMTAuODMsMCwwLDEsMTguNDUsODAuMzZaTTUuMDksMTAzLjRsMTI4LjMxLS4wMmExLjUyLDEuNTIsMCwwLDAsMS4yMTctLjM5MWMuMTE4LS4xODcuMTM1LS42MTYtLjE3MS0xLjI1N2wtNi42NzEtMTMuOTkyYTYuNjcxLDYuNjcxLDAsMCwwLTUuMzQ1LTMuMzcxSDE4LjQ1cS0uMDgzLDAtLjE2NS0uMDA3QTcuNyw3LjcsMCwwLDAsMTIuNjY5LDg3LjdMNC4zMywxMDIuMDgzYy0uMzc5LjY1MS0uMzQsMS0uMzEsMS4wNTNTNC4zMzYsMTAzLjQsNS4wOSwxMDMuNFoiCiAgICAgICAgIGZpbGw9IiMwMDRhNmIiIC8+CiAgICAgIDxyZWN0CiAgICAgICAgIGlkPSJSZWNodGVja18zIgogICAgICAgICBkYXRhLW5hbWU9IlJlY2h0ZWNrIDMiCiAgICAgICAgIHdpZHRoPSIxMTMuMzYiCiAgICAgICAgIGhlaWdodD0iNjQuMzkiCiAgICAgICAgIHJ4PSI1IgogICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzIDIpIgogICAgICAgICBmaWxsPSIjMDA0YTZiIiAvPgogICAgICA8cGF0aAogICAgICAgICBpZD0iUmVjaHRlY2tfNCIKICAgICAgICAgZGF0YS1uYW1lPSJSZWNodGVjayA0IgogICAgICAgICBkPSJNNS0ySDEwOC4zNmE3LjAwOCw3LjAwOCwwLDAsMSw3LDdWNTkuMzlhNy4wMDgsNy4wMDgsMCwwLDEtNyw3SDVhNy4wMDgsNy4wMDgsMCwwLDEtNy03VjVBNy4wMDgsNy4wMDgsMCwwLDEsNS0yWk0xMDguMzYsNjIuMzlhMywzLDAsMCwwLDMtM1Y1YTMsMywwLDAsMC0zLTNINUEzLDMsMCwwLDAsMiw1VjU5LjM5YTMsMywwLDAsMCwzLDNaIgogICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzIDIpIgogICAgICAgICBmaWxsPSIjMDA0YTZiIiAvPgogICAgPC9nPgogICAgPHBhdGgKICAgICAgIGlkPSJQZmFkXzE0IgogICAgICAgZGF0YS1uYW1lPSJQZmFkIDE0IgogICAgICAgZD0iTTM0Ljk4Myw0Ny43bC0xMi4zMy0xMC4zN2EyLDIsMCwwLDEtLjEtMi45NzJsMTEuNjEtMTEuMTcsMi43NzMsMi44ODNMMjYuOTI4LDM1LjdsMTAuNjI5LDguOTM5WiIKICAgICAgIGZpbGw9IiM5OTljZmYiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9IlBmYWRfMTUiCiAgICAgICBkYXRhLW5hbWU9IlBmYWQgMTUiCiAgICAgICBkPSJNODUuMiw0Ny43bC0yLjc3My0yLjg4MywxMC4wMDktOS42MjlMODEuODEyLDI2LjI1bDIuNTc2LTMuMDYsMTIuMzIsMTAuMzdhMiwyLDAsMCwxLC4xLDIuOTcxWiIKICAgICAgIGZpbGw9IiM5OTljZmYiIC8+CiAgICA8dGV4dAogICAgICAgaWQ9ImhpIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDUuMzQsNDYuODcpIgogICAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICAgIGZvbnQtc2l6ZT0iMzBweCIKICAgICAgIGZvbnQtZmFtaWx5PSJWQ1JPU0RNb25vLCAnVkNSIE9TRCBNb25vJyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MzBweDtmb250LWZhbWlseTpWQ1JPU0RNb25vLCAnVkNSIE9TRCBNb25vJztmaWxsOiNmZmZmZmYiPjx0c3BhbgogICAgICAgICB4PSIwIgogICAgICAgICB5PSIwIgogICAgICAgICBpZD0idHNwYW4yMzciCiAgICAgICAgIHN0eWxlPSJmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2ZvbnQtc2l6ZTozMHB4O2ZvbnQtZmFtaWx5OidVYnVudHUgTW9ubyc7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjonVWJ1bnR1IE1vbm8sIE5vcm1hbCc7Zm9udC12YXJpYW50LWxpZ2F0dXJlczpub3JtYWw7Zm9udC12YXJpYW50LWNhcHM6bm9ybWFsO2ZvbnQtdmFyaWFudC1udW1lcmljOm5vcm1hbDtmb250LXZhcmlhbnQtZWFzdC1hc2lhbjpub3JtYWwiPmhpPC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K"
        },
        "validationRules": {
            "student_name": "required|string|max:255",
            "student_email": "required|string|email|max:255",
            "quiz_title": "required|string|max:255",
            "score": "required|string",
            "stars": "sometimes|string",
            "completion_date": "required|string",
            "description_text": "required|string",
            "certificate_number": "sometimes|string|max:20",
            "verification_url": "sometimes|string|url",
            "verification_display_url": "sometimes|string|url",
            "qr_code_data_uri": [
                "sometimes",
                "string",
                "regex:~^data:image/(png|jpeg|gif|webp|svg\\+xml);base64,[A-Za-z0-9+/]+={0,2}$~"
            ],
            "quiz_color": "sometimes|string|regex:/^#[0-9a-fA-F]{6}$/",
            "quiz_image_url": [
                "sometimes",
                "nullable",
                "string",
                "regex:~^(data:image/(png|jpeg|gif|webp|svg\\+xml);base64,[A-Za-z0-9+/]+={0,2}|/v1/media/[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}/download)$~"
            ],
            "quiz_label": "sometimes|string|max:50"
        }
    },
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/pdf/templates/{template}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

template   string     

The template slug. Example: quiz_certificate

Preview template with default data Generates a preview PDF using the template's default data. Useful for testing templates without providing custom data.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/pdf/templates/quiz_certificate/preview" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/pdf/templates/quiz_certificate/preview"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/pdf/templates/quiz_certificate/preview';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/pdf/templates/{template}/preview

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

template   string     

The template slug. Example: quiz_certificate

Placement Management

List all placements

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/placements" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/placements"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/placements';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/placements

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

Create a new placement

Example request:
curl --request POST \
    "http://localhost:8000/v1/placements" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/placements"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/placements';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/placements

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get placement by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/placements/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the placement. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update placement by ID

Example request:
curl --request PUT \
    "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/placements/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the placement. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete placement by ID (soft delete)

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/placements/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/placements/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the placement. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Public

Public tenant lookup by slug (for login flow) Returns minimal tenant info (id, slug, name) without requiring authentication. This endpoint is used during login to resolve a tenant slug to its ID.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tenants/lookup/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tenants/lookup/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/lookup/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "data": {
        "id": "01ARZ3...",
        "slug": "acme-corp",
        "name": "Acme Corp"
    }
}
 

Example response (404):


{
    "success": false,
    "message": "Tenant not found"
}
 

Request      

GET v1/tenants/lookup/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the lookup. Example: architecto

Question History

Compare two versions

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/history/compare?old=01ARZ3NDEKTSV4RRFFQ69G5FAV&new=01ARZ4NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"old\": \"bngzmiyvdljnikhwaykcmyuwpw\",
    \"new\": \"lvqwrsitcpscqldzsnrwtujwvl\"
}"
const url = new URL(
    "http://localhost:8000/v1/questions/history/compare"
);

const params = {
    "old": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "new": "01ARZ4NDEKTSV4RRFFQ69G5FAV",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "old": "bngzmiyvdljnikhwaykcmyuwpw",
    "new": "lvqwrsitcpscqldzsnrwtujwvl"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/history/compare';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'old' => '01ARZ3NDEKTSV4RRFFQ69G5FAV',
            'new' => '01ARZ4NDEKTSV4RRFFQ69G5FAV',
        ],
        'json' => [
            'old' => 'bngzmiyvdljnikhwaykcmyuwpw',
            'new' => 'lvqwrsitcpscqldzsnrwtujwvl',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Version comparison completed", "data": {"old_version": {...}, "new_version": {...}, "differences": {...}}}
 

Example response (404):


{
    "success": false,
    "message": "History record not found"
}
 

Request      

GET v1/questions/history/compare

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

old   string     

ID of the old history record. Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

new   string     

ID of the new history record. Example: 01ARZ4NDEKTSV4RRFFQ69G5FAV

Body Parameters

old   string     

Must be 26 characters. Example: bngzmiyvdljnikhwaykcmyuwpw

new   string     

Must be 26 characters. Example: lvqwrsitcpscqldzsnrwtujwvl

Get version history for a question

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/history?per_page=20&page=1" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/history"
);

const params = {
    "per_page": "20",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/history';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Question history retrieved successfully", "data": [...], "meta": {"pagination": {...}}}
 

Example response (404):


{
    "success": false,
    "message": "Question not found"
}
 

Request      

GET v1/questions/{id}/history

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Query Parameters

per_page   integer  optional    

Number of history records per page (1-100). Defaults to 20. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

Get specific version from history

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/history/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Question version retrieved successfully", "data": {...}}
 

Example response (404):


{
    "success": false,
    "message": "History record not found"
}
 

Request      

GET v1/questions/{id}/history/{historyId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

historyId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Restore question to a previous version (owner only)

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"version_note\": \"Restored from version before error\"
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "version_note": "Restored from version before error"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/restore/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'version_note' => 'Restored from version before error',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Question restored successfully", "data": {...}}
 

Example response (403):


{
    "success": false,
    "message": "Question with ID ... belongs to another tenant"
}
 

Example response (404):


{
    "success": false,
    "message": "Question or history record not found"
}
 

Request      

POST v1/questions/{id}/restore/{historyId}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

historyId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

version_note   string  optional    

nullable Note describing this restoration. Example: Restored from version before error

Question Management

List all questions with filters

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions?per_page=20&page=1&ids=01ARZ3NDEKTSV4RRFFQ69G5FAV&type=multiplechoice%2Cmultiple_choice&is_public=1&search=math&created_at_gte=2024-01-01&created_at_lte=2024-12-31&tags=difficulty%3Ahard%2Ctopic%3Amath&tag_mode=and&languages=de%2Cde-DE&subjects=physics&difficulties=easy&has_versions=1&sort=-versions_count%2Ctitle.de&include_public=1&include_summary=1" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/questions"
);

const params = {
    "per_page": "20",
    "page": "1",
    "ids": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "type": "multiplechoice,multiple_choice",
    "is_public": "1",
    "search": "math",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "tags": "difficulty:hard,topic:math",
    "tag_mode": "and",
    "languages": "de,de-DE",
    "subjects": "physics",
    "difficulties": "easy",
    "has_versions": "1",
    "sort": "-versions_count,title.de",
    "include_public": "1",
    "include_summary": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'ids' => '01ARZ3NDEKTSV4RRFFQ69G5FAV',
            'type' => 'multiplechoice,multiple_choice',
            'is_public' => '1',
            'search' => 'math',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'tags' => 'difficulty:hard,topic:math',
            'tag_mode' => 'and',
            'languages' => 'de,de-DE',
            'subjects' => 'physics',
            'difficulties' => 'easy',
            'has_versions' => '1',
            'sort' => '-versions_count,title.de',
            'include_public' => '1',
            'include_summary' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Questions retrieved successfully", "data": [...], "meta": {"pagination": {...}}}
 

Request      

GET v1/questions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 15. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

ids   string  optional    

Comma-separated question IDs, for instance to compare a test's question snapshots with the current questions. Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

type   string  optional    

Comma-separated question types. Example: multiplechoice,multiple_choice

is_public   boolean  optional    

Filter by public status. Example: true

search   string  optional    

Literal text search in titles, prompts and instruction block text in any language, including regional locale keys, or an exact question ID. Example: math

created_at_gte   string  optional    

Filter questions created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter questions created on or before date. Example: 2024-12-31

tags   string  optional    

Comma-separated tag slugs. Example: difficulty:hard,topic:math

tag_mode   string  optional    

Tag matching mode: 'and' (all tags) or 'or' (any tag). Defaults to 'or'. Example: and

languages   string  optional    

Comma-separated title languages; a question matches with any of them. Example: de,de-DE

subjects   string  optional    

Comma-separated subjects from the metadata; any known spelling of a base subject finds its key. Example: physics

difficulties   string  optional    

Comma-separated difficulties from the metadata. Example: easy

has_versions   boolean  optional    

Whether the question has history versions. Example: true

sort   string  optional    

Sort fields in order of priority, comma-separated: created_at, updated_at, type, title.de, title.en (the title the admin shows in that language), subject (a base subject by its label in the request's language, from Accept-Language, German or English, by default German; another subject by its text), difficulty (easy, medium, difficult, then other values), languages (the title's language codes) or versions_count (history versions). Prefix each with '-' for descending; questions without a value come last either way, ties keep creation order. Example: -versions_count,title.de

include_public   boolean  optional    

Include public questions from other tenants. Defaults to true. Example: true

include_summary   boolean  optional    

Add each question's summary with its number of history versions. Example: true

Create new question

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "title[]=architecto"\
    --form "prompt[]=architecto"\
    --form "type=multiple_choice"\
    --form "is_public=1"\
    --form "instructions[]=architecto"\
    --form "feedbacks[]=architecto"\
    --form "media_id=01ARZ3NDEKTSV4RRFFQ69G5FAV"\
    --form "media_url=https://example.com/image.jpg"\
    --form "logic[]=architecto"\
    --form "version_note=Initial version"\
    --form "media_file=@/tmp/php4c1115a66j6fegrknoY" 
const url = new URL(
    "http://localhost:8000/v1/questions"
);

const headers = {
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('title[]', 'architecto');
body.append('prompt[]', 'architecto');
body.append('type', 'multiple_choice');
body.append('is_public', '1');
body.append('instructions[]', 'architecto');
body.append('feedbacks[]', 'architecto');
body.append('media_id', '01ARZ3NDEKTSV4RRFFQ69G5FAV');
body.append('media_url', 'https://example.com/image.jpg');
body.append('logic[]', 'architecto');
body.append('version_note', 'Initial version');
body.append('media_file', document.querySelector('input[name="media_file"]').files[0]);

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'multipart/form-data',
            'Accept' => 'application/json',
        ],
        'multipart' => [
            [
                'name' => 'title[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'prompt[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'type',
                'contents' => 'multiple_choice'
            ],
            [
                'name' => 'is_public',
                'contents' => '1'
            ],
            [
                'name' => 'instructions[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'feedbacks[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'media_id',
                'contents' => '01ARZ3NDEKTSV4RRFFQ69G5FAV'
            ],
            [
                'name' => 'media_url',
                'contents' => 'https://example.com/image.jpg'
            ],
            [
                'name' => 'logic[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'version_note',
                'contents' => 'Initial version'
            ],
            [
                'name' => 'media_file',
                'contents' => fopen('/tmp/php4c1115a66j6fegrknoY', 'r')
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{"success": true, "message": "Question created successfully", "data": {...}}
 

Request      

POST v1/questions

Headers

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters

title   string[]     

Multilingual title (JSON object with language codes as keys).

prompt   string[]  optional    

nullable Optional multilingual question text. Blank translations are dropped; an empty prompt is stored as null. A question needs text or instruction blocks.

type   string     

Question type. Example: multiple_choice

is_public   boolean  optional    

nullable Make question public to all tenants. Defaults to false. Example: true

instructions   string[]  optional    

nullable Array of block objects.

feedbacks   string[]  optional    

nullable Array of block objects.

media_id   string  optional    

nullable Media attachment ID (mutually exclusive with media_file and media_url). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

media_file   file  optional    

nullable Direct file upload (mutually exclusive with media_id and media_url). Example: /tmp/php4c1115a66j6fegrknoY

media_url   string  optional    

nullable URL to download image from (mutually exclusive with media_id and media_file). Example: https://example.com/image.jpg

logic   string[]  optional    

nullable Custom logic/scoring rules.

version_note   string  optional    

nullable Note describing this version. Example: Initial version

Preview unsaved authoring content as a learner question. Uses the create body, or partial update body when an ID is supplied.

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/preview" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"seed\": 16
}"
const url = new URL(
    "http://localhost:8000/v1/questions/preview"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "seed": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/preview';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'seed' => 16,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "data": {
        "snapshot": {},
        "seed": 37
    }
}
 

Example response (422):


{
    "success": false,
    "message": "Formula preview could not be resolved."
}
 

Example response (429):


{
    "success": false,
    "message": "Too Many Attempts."
}
 

Example response (503):


{
    "success": false,
    "message": "Formula preview is unavailable. Please try again later."
}
 

Request      

POST v1/questions/preview

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

seed   integer  optional    

Optional reproducible formula seed, from 0 to 4294967295. Example: 16

Preview unsaved authoring content as a learner question. Uses the create body, or partial update body when an ID is supplied.

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/preview" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"seed\": 16
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/preview"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "seed": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/preview';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'seed' => 16,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "data": {
        "snapshot": {},
        "seed": 37
    }
}
 

Example response (422):


{
    "success": false,
    "message": "Formula preview could not be resolved."
}
 

Example response (429):


{
    "success": false,
    "message": "Too Many Attempts."
}
 

Example response (503):


{
    "success": false,
    "message": "Formula preview is unavailable. Please try again later."
}
 

Request      

POST v1/questions/{id}/preview

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

seed   integer  optional    

Optional reproducible formula seed, from 0 to 4294967295. Example: 16

List all public questions (across all tenants)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/public?per_page=20&page=1" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"per_page\": 1,
    \"page\": 22
}"
const url = new URL(
    "http://localhost:8000/v1/questions/public"
);

const params = {
    "per_page": "20",
    "page": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "per_page": 1,
    "page": 22
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/public';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
        ],
        'json' => [
            'per_page' => 1,
            'page' => 22,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Public questions retrieved successfully", "data": [...], "meta": {"pagination": {...}}}
 

Request      

GET v1/questions/public

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 15. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

Body Parameters

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 1

page   integer  optional    

Must be at least 1. Example: 22

List question types currently enabled for authoring. Mirrors `config/module_question.enabled_types`. Admins use this to hide disabled types from the question creation UI. Disabled types remain queryable in list endpoints for legacy data — only authoring is gated.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/meta/types" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/questions/meta/types"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/meta/types';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Enabled question types", "data": {"types": [{"key": "multiplechoice"}, ...]}}
 

Request      

GET v1/questions/meta/types

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

The title languages, subjects and difficulties the question list can be filtered by, as stored, in the list's scope; base subjects and difficulty levels with their German and English labels.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/facets?include_public=1" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
const url = new URL(
    "http://localhost:8000/v1/questions/facets"
);

const params = {
    "include_public": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/facets';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'include_public' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/questions/facets

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

include_public   boolean  optional    

Include public questions from other tenants. Defaults to true. Example: true

Body Parameters

include_public   string  optional    

Bulk delete questions (owner only)

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/bulk-delete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        \"01ARZ3...\",
        \"01ARZ4...\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/questions/bulk-delete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        "01ARZ3...",
        "01ARZ4..."
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/bulk-delete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'ids' => [
                '01ARZ3...',
                '01ARZ4...',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Bulk delete completed",
    "data": {
        "deleted_count": 5,
        "errors": []
    }
}
 

Request      

POST v1/questions/bulk-delete

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   string[]     

Array of question IDs to delete.

Get question by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Question retrieved successfully", "data": {...}}
 

Example response (404):


{
    "success": false,
    "message": "Question not found"
}
 

Request      

GET v1/questions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update question by ID (owner only)

Example request:
curl --request PUT \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "title[]=architecto"\
    --form "prompt[]=architecto"\
    --form "type=architecto"\
    --form "is_public="\
    --form "instructions[]=architecto"\
    --form "feedbacks[]=architecto"\
    --form "media_id=architecto"\
    --form "media_url=http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html"\
    --form "logic[]=architecto"\
    --form "version_note=Fixed typo in title"\
    --form "media_file=@/tmp/phpccqh8eng5ddo8A6XhIU" 
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "multipart/form-data",
    "Accept": "application/json",
};

const body = new FormData();
body.append('title[]', 'architecto');
body.append('prompt[]', 'architecto');
body.append('type', 'architecto');
body.append('is_public', '');
body.append('instructions[]', 'architecto');
body.append('feedbacks[]', 'architecto');
body.append('media_id', 'architecto');
body.append('media_url', 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html');
body.append('logic[]', 'architecto');
body.append('version_note', 'Fixed typo in title');
body.append('media_file', document.querySelector('input[name="media_file"]').files[0]);

fetch(url, {
    method: "PUT",
    headers,
    body,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'multipart/form-data',
            'Accept' => 'application/json',
        ],
        'multipart' => [
            [
                'name' => 'title[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'prompt[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'type',
                'contents' => 'architecto'
            ],
            [
                'name' => 'is_public',
                'contents' => ''
            ],
            [
                'name' => 'instructions[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'feedbacks[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'media_id',
                'contents' => 'architecto'
            ],
            [
                'name' => 'media_url',
                'contents' => 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html'
            ],
            [
                'name' => 'logic[]',
                'contents' => 'architecto'
            ],
            [
                'name' => 'version_note',
                'contents' => 'Fixed typo in title'
            ],
            [
                'name' => 'media_file',
                'contents' => fopen('/tmp/phpccqh8eng5ddo8A6XhIU', 'r')
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Question updated successfully", "data": {...}}
 

Example response (403):


{
    "success": false,
    "message": "Question with ID ... belongs to another tenant"
}
 

Example response (404):


{
    "success": false,
    "message": "Question not found"
}
 

Request      

PUT v1/questions/{id}

Headers

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

title   string[]  optional    

nullable Multilingual title.

prompt   string[]  optional    

nullable Multilingual prompt.

type   string  optional    

nullable Question type. Example: architecto

is_public   boolean  optional    

nullable Public visibility flag. Example: false

instructions   string[]  optional    

nullable Array of block objects.

feedbacks   string[]  optional    

nullable Array of block objects.

media_id   string  optional    

nullable Media attachment ID (mutually exclusive with media_file and media_url). Example: architecto

media_file   file  optional    

nullable Direct file upload (mutually exclusive with media_id and media_url). Example: /tmp/phpccqh8eng5ddo8A6XhIU

media_url   string  optional    

nullable URL to download image from (mutually exclusive with media_id and media_file). Example: http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html

logic   string[]  optional    

nullable Custom logic/scoring rules.

version_note   string  optional    

nullable Note describing this version. Example: Fixed typo in title

Delete question by ID (soft delete, owner only)

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"version_note\": \"Removed outdated content\"
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "version_note": "Removed outdated content"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'version_note' => 'Removed outdated content',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Question deleted successfully", "data": {...}}
 

Example response (403):


{
    "success": false,
    "message": "Question with ID ... belongs to another tenant"
}
 

Example response (404):


{
    "success": false,
    "message": "Question not found"
}
 

Request      

DELETE v1/questions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

version_note   string  optional    

nullable Note describing this deletion. Example: Removed outdated content

Duplicate a question

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/duplicate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"overrides\": [
        \"architecto\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/duplicate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "overrides": [
        "architecto"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/duplicate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'overrides' => [
                'architecto',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{"success": true, "message": "Question duplicated successfully", "data": {...}}
 

Example response (404):


{
    "success": false,
    "message": "Question not found"
}
 

Request      

POST v1/questions/{id}/duplicate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

overrides   string[]  optional    

nullable Fields to override in the duplicate.

Question Tagging

List all tags used by questions

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"search\": \"b\",
    \"per_page\": 22,
    \"page\": 67
}"
const url = new URL(
    "http://localhost:8000/v1/questions/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "search": "b",
    "per_page": 22,
    "page": 67
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/tags';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'search' => 'b',
            'per_page' => 22,
            'page' => 67,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/questions/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

search   string  optional    

Must not be greater than 255 characters. Example: b

per_page   integer  optional    

Must be at least 1. Must not be greater than 100. Example: 22

page   integer  optional    

Must be at least 1. Example: 67

List contexts available for questions

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/tags/contexts" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/questions/tags/contexts"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/tags/contexts';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/questions/tags/contexts

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get tags for a specific question

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/questions/{id}/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Assign tag(s) to question

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"tag_slugs\": [
        \"architecto\"
    ],
    \"context\": \"n\"
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "tag_slugs": [
        "architecto"
    ],
    "context": "n"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'tag_slugs' => [
                'architecto',
            ],
            'context' => 'n',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/questions/{id}/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

tag_slugs   string[]     
context   string  optional    

Must not be greater than 50 characters. Example: n

Remove tag(s) from question

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"tag_slugs\": [
        \"architecto\"
    ],
    \"context\": \"n\"
}"
const url = new URL(
    "http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "tag_slugs": [
        "architecto"
    ],
    "context": "n"
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/58C6f7nnFCJ9M6rjyc6rTKMzn6/tags';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'tag_slugs' => [
                'architecto',
            ],
            'context' => 'n',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/questions/{id}/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the question. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

tag_slugs   string[]     
context   string  optional    

Must not be greater than 50 characters. Example: n

Bulk assign tags to multiple questions

Example request:
curl --request POST \
    "http://localhost:8000/v1/questions/tags/bulk-assign" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"assignments\": [
        {
            \"resource_id\": \"ngzmiyvdljnikhwaykcmyuwpwl\",
            \"tag_slugs\": [
                \"architecto\"
            ]
        }
    ],
    \"context\": \"b\"
}"
const url = new URL(
    "http://localhost:8000/v1/questions/tags/bulk-assign"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "assignments": [
        {
            "resource_id": "ngzmiyvdljnikhwaykcmyuwpwl",
            "tag_slugs": [
                "architecto"
            ]
        }
    ],
    "context": "b"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/questions/tags/bulk-assign';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'assignments' => [
                [
                    'resource_id' => 'ngzmiyvdljnikhwaykcmyuwpwl',
                    'tag_slugs' => [
                        'architecto',
                    ],
                ],
            ],
            'context' => 'b',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/questions/tags/bulk-assign

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

assignments   object[]     

Must have at least 1 items.

resource_id   string     

Must be 26 characters. Example: ngzmiyvdljnikhwaykcmyuwpwl

tag_slugs   string[]     
context   string  optional    

Must not be greater than 50 characters. Example: b

Quiz paths

Learners' view of quiz paths (packages of short tests). {package} is the package's ID or slug; only published quiz paths of the caller's tenant are visible to learners.

The learner's progress in every quiz path of the tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/progress" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/progress"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/progress';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/progress

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

A quiz path with the learner's state and suggested next tests

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/architecto/path?after_attempt=01J0000000000000000000000" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"after_attempt\": \"bngzmiyvdljnikhwaykcmyuwpw\"
}"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/path"
);

const params = {
    "after_attempt": "01J0000000000000000000000",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "after_attempt": "bngzmiyvdljnikhwaykcmyuwpw"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/path';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'after_attempt' => '01J0000000000000000000000',
        ],
        'json' => [
            'after_attempt' => 'bngzmiyvdljnikhwaykcmyuwpw',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/{package}/path

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

package   string     

The package. Example: architecto

Query Parameters

after_attempt   string  optional    

The learner's graded attempt the suggestions follow. Example: 01J0000000000000000000000

Body Parameters

after_attempt   string  optional    

Must be 26 characters. Example: bngzmiyvdljnikhwaykcmyuwpw

The combined evaluation of the learner's tests in a quiz path

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/packages/architecto/evaluation" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/evaluation"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/evaluation';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Authentication required. Please provide a valid access token.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/packages/{package}/evaluation

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

package   string     

The package. Example: architecto

Record which test the learner picked to continue with

Example request:
curl --request POST \
    "http://localhost:8000/v1/packages/architecto/choices" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/packages/architecto/choices"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/packages/architecto/choices';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/packages/{package}/choices

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

package   string     

The package. Example: architecto

RBAC Management

Get my effective permissions Returns the current authenticated user's effective permissions after grant/deny resolution. Public endpoint - no rbac.manage permission required. Superadmin users have all permissions automatically.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/me/permissions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/me/permissions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/me/permissions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Effective permissions retrieved",
    "data": [
        "users.read",
        "users.create",
        "tenants.read",
        "rbac.manage"
    ]
}
 

Request      

GET v1/rbac/me/permissions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get my role Returns the current authenticated user's assigned role. If no role is explicitly assigned, returns the tenant's default role. Public endpoint - no rbac.manage permission required.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/me/role" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/me/role"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/me/role';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Role retrieved",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Administrator",
        "slug": "admin",
        "description": "Full system access",
        "is_default": false,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Request      

GET v1/rbac/me/role

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List all permissions Returns all permissions in the tenant's RBAC system. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/permissions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/permissions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/permissions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Permissions retrieved",
    "data": [
        {
            "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
            "slug": "users.read",
            "name": "View Users",
            "description": "View user list and details",
            "resource": "users",
            "action": "read",
            "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
        }
    ]
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Request      

GET v1/rbac/permissions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Create new permission Creates a new permission in the tenant's RBAC system. The slug must be unique within the tenant. Requires rbac.manage permission.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/rbac/permissions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"slug\": \"courses.publish\",
    \"name\": \"Publish Courses\",
    \"description\": \"Allows publishing courses to students\",
    \"resource\": \"courses\",
    \"action\": \"publish\"
}"
const url = new URL(
    "http://localhost:8000/v1/rbac/permissions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "slug": "courses.publish",
    "name": "Publish Courses",
    "description": "Allows publishing courses to students",
    "resource": "courses",
    "action": "publish"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/permissions';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'slug' => 'courses.publish',
            'name' => 'Publish Courses',
            'description' => 'Allows publishing courses to students',
            'resource' => 'courses',
            'action' => 'publish',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{
    "success": true,
    "message": "Permission created successfully",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "slug": "courses.publish",
        "name": "Publish Courses",
        "description": "Allows publishing courses to students",
        "resource": "courses",
        "action": "publish",
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (400):


{
    "success": false,
    "message": "Permission with this slug already exists"
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Request      

POST v1/rbac/permissions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

slug   string     

Permission slug (lowercase, dots/underscores allowed). Example: courses.publish

name   string     

Permission display name. Example: Publish Courses

description   string  optional    

Description of what this permission allows. Example: Allows publishing courses to students

resource   string     

Resource this permission applies to. Example: courses

action   string     

Action this permission allows. Example: publish

Get permission by ID Returns a single permission by its ID. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Permission retrieved",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "slug": "users.create",
        "name": "Create Users",
        "description": "Create new user accounts",
        "resource": "users",
        "action": "create",
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Permission not found"
}
 

Request      

GET v1/rbac/permissions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Permission ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Update permission Updates an existing permission. The slug cannot be changed after creation. Requires rbac.manage permission.

requires authentication

Example request:
curl --request PUT \
    "http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Create Users\",
    \"description\": \"Create new user accounts\",
    \"resource\": \"users\",
    \"action\": \"create\"
}"
const url = new URL(
    "http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Create Users",
    "description": "Create new user accounts",
    "resource": "users",
    "action": "create"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Create Users',
            'description' => 'Create new user accounts',
            'resource' => 'users',
            'action' => 'create',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Permission updated successfully",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "slug": "users.create",
        "name": "Create Users",
        "description": "Create new user accounts",
        "resource": "users",
        "action": "create",
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Permission not found"
}
 

Request      

PUT v1/rbac/permissions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Permission ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Body Parameters

name   string     

Permission display name. Example: Create Users

description   string  optional    

Description of what this permission allows. Example: Create new user accounts

resource   string     

Resource this permission applies to. Example: users

action   string     

Action this permission allows. Example: create

Delete permission Deletes a permission from the tenant's RBAC system. Cannot delete permissions that are currently assigned to roles. Requires rbac.manage permission.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/permissions/01ARZ3NDEKTSV4RRFFQ69G5FAV';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Permission deleted successfully",
    "data": null
}
 

Example response (400):


{
    "success": false,
    "message": "Cannot delete permission: currently assigned to roles"
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Permission not found"
}
 

Request      

DELETE v1/rbac/permissions/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Permission ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

List all roles Returns all roles in the tenant's RBAC system. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/roles" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Roles retrieved",
    "data": [
        {
            "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
            "name": "Administrator",
            "slug": "admin",
            "description": "Full system access",
            "is_default": false,
            "is_superadmin": false,
            "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
        }
    ]
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Request      

GET v1/rbac/roles

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Create new role Creates a new role in the tenant's RBAC system. The slug must be unique within the tenant. Only one role can be marked as default per tenant. Superadmin roles bypass all permission checks. Requires rbac.manage permission.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/rbac/roles" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Content Manager\",
    \"slug\": \"content-manager\",
    \"description\": \"Manages course content and media\",
    \"is_default\": false,
    \"is_superadmin\": false
}"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Content Manager",
    "slug": "content-manager",
    "description": "Manages course content and media",
    "is_default": false,
    "is_superadmin": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Content Manager',
            'slug' => 'content-manager',
            'description' => 'Manages course content and media',
            'is_default' => false,
            'is_superadmin' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{
    "success": true,
    "message": "Role created successfully",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Content Manager",
        "slug": "content-manager",
        "description": "Manages course content and media",
        "is_default": false,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (400):


{
    "success": false,
    "message": "Role with this slug already exists"
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Request      

POST v1/rbac/roles

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Role display name. Example: Content Manager

slug   string     

Role slug (lowercase, hyphens/underscores allowed). Example: content-manager

description   string  optional    

Description of this role. Example: Manages course content and media

is_default   boolean     

Whether this is the default role for new users. Example: false

is_superadmin   boolean     

Whether this role bypasses all permission checks. Example: false

Get default role Returns the tenant's default role (assigned to new users automatically). Only one default role can exist per tenant. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/roles/default" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles/default"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles/default';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Default role retrieved",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Default User",
        "slug": "default",
        "description": "Default permissions for new users",
        "is_default": true,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Role not found: default role for tenant 01ARZ3NDEKTSV4RRFFQ69G5FAV"
}
 

Request      

GET v1/rbac/roles/default

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get role by ID Returns a single role by its ID, including granted and denied permissions. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Role retrieved",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Administrator",
        "slug": "admin",
        "description": "Full system access",
        "is_default": false,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "granted_permissions": [
            "users.read",
            "users.create"
        ],
        "denied_permissions": []
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Role not found"
}
 

Request      

GET v1/rbac/roles/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Role ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Update role Updates an existing role. The slug cannot be changed after creation. Setting is_default to true will unset any other default role. Requires rbac.manage permission.

requires authentication

Example request:
curl --request PUT \
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Content Manager\",
    \"description\": \"Manages course content and media\",
    \"is_default\": false,
    \"is_superadmin\": false
}"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Content Manager",
    "description": "Manages course content and media",
    "is_default": false,
    "is_superadmin": false
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Content Manager',
            'description' => 'Manages course content and media',
            'is_default' => false,
            'is_superadmin' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Role updated successfully",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Content Manager",
        "slug": "content-manager",
        "description": "Manages course content and media",
        "is_default": false,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Role not found"
}
 

Request      

PUT v1/rbac/roles/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Role ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Body Parameters

name   string     

Role display name. Example: Content Manager

description   string  optional    

Description of this role. Example: Manages course content and media

is_default   boolean     

Whether this is the default role for new users. Example: false

is_superadmin   boolean     

Whether this role bypasses all permission checks. Example: false

Delete role Deletes a role from the tenant's RBAC system. Cannot delete the default role or roles currently assigned to users. Requires rbac.manage permission.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Role deleted successfully",
    "data": null
}
 

Example response (400):


{
    "success": false,
    "message": "Cannot delete default role or role assigned to users"
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Role not found"
}
 

Request      

DELETE v1/rbac/roles/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Role ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Sync role permissions Synchronizes a role's granted and denied permissions. Permissions not in either list will be removed from the role. Explicit grants override denies (if a permission appears in both lists, it will be granted). Requires rbac.manage permission.

requires authentication

Example request:
curl --request PUT \
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV/permissions/sync" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"grant_permission_ids\": [
        \"01ARZ3NDEKTSV4RRFFQ69G5FAV\",
        \"01ARZ3NDEKTSV4RRFFQ69G5FAW\"
    ],
    \"deny_permission_ids\": [
        \"01ARZ3NDEKTSV4RRFFQ69G5FAX\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV/permissions/sync"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "grant_permission_ids": [
        "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "01ARZ3NDEKTSV4RRFFQ69G5FAW"
    ],
    "deny_permission_ids": [
        "01ARZ3NDEKTSV4RRFFQ69G5FAX"
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/roles/01ARZ3NDEKTSV4RRFFQ69G5FAV/permissions/sync';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'grant_permission_ids' => [
                '01ARZ3NDEKTSV4RRFFQ69G5FAV',
                '01ARZ3NDEKTSV4RRFFQ69G5FAW',
            ],
            'deny_permission_ids' => [
                '01ARZ3NDEKTSV4RRFFQ69G5FAX',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Role permissions synced successfully",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Content Manager",
        "slug": "content-manager",
        "description": "Manages course content",
        "is_default": false,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "granted_permissions": [
            "courses.read",
            "courses.create"
        ],
        "denied_permissions": [
            "courses.delete"
        ]
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Role not found or permission not found"
}
 

Request      

PUT v1/rbac/roles/{roleId}/permissions/sync

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

roleId   string     

Role ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Body Parameters

grant_permission_ids   string[]  optional    

Array of permission IDs to grant.

deny_permission_ids   string[]  optional    

Array of permission IDs to explicitly deny.

Assign role to user Assigns a role to a user (identity) in the tenant. If the user already has a role assignment, it will be updated. Requires rbac.manage permission.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/rbac/users/assign-role" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"identity_id\": \"01ARZ3NDEKTSV4RRFFQ69G5FAV\",
    \"role_id\": \"01ARZ3NDEKTSV4RRFFQ69G5FAW\"
}"
const url = new URL(
    "http://localhost:8000/v1/rbac/users/assign-role"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "identity_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "role_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/users/assign-role';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'identity_id' => '01ARZ3NDEKTSV4RRFFQ69G5FAV',
            'role_id' => '01ARZ3NDEKTSV4RRFFQ69G5FAW',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{
    "success": true,
    "message": "Role assigned successfully",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAY",
        "identity_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "role_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAZ",
        "assigned_by": "01ARZ3NDEKTSV4RRFFQ69G5FAA",
        "assigned_at": "2024-01-15T10:30:00Z"
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Identity not found or role not found"
}
 

Request      

POST v1/rbac/users/assign-role

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

identity_id   string     

Identity ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

role_id   string     

Role ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAW

Revoke user's role Revokes (removes) the role assignment from a user. The user will fall back to the tenant's default role if one exists. Requires rbac.manage permission.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/revoke-role" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/revoke-role"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/revoke-role';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Role revoked successfully",
    "data": null
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "User has no role assignment"
}
 

Request      

DELETE v1/rbac/users/{identityId}/revoke-role

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

identityId   string     

Identity ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Get user's role Returns the role assigned to a specific user. If no role is explicitly assigned, returns the tenant's default role. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/role" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/role"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/role';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "User role retrieved",
    "data": {
        "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "name": "Content Manager",
        "slug": "content-manager",
        "description": "Manages course content",
        "is_default": false,
        "is_superadmin": false,
        "tenant_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    }
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Identity not found"
}
 

Request      

GET v1/rbac/users/{identityId}/role

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

identityId   string     

Identity ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Get user's effective permissions Returns the effective permissions for a specific user after grant/deny resolution. Takes into account the user's assigned role and its permission grants/denies. Superadmin users will have all permissions. Requires rbac.manage permission.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/effective-permissions" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/effective-permissions"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/rbac/users/01ARZ3NDEKTSV4RRFFQ69G5FAV/effective-permissions';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "User effective permissions retrieved",
    "data": [
        "courses.read",
        "courses.create",
        "courses.update",
        "media.read"
    ]
}
 

Example response (403):


{
    "success": false,
    "message": "Insufficient permissions"
}
 

Example response (404):


{
    "success": false,
    "message": "Identity not found"
}
 

Request      

GET v1/rbac/users/{identityId}/effective-permissions

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

identityId   string     

Identity ID (ULID, 26 characters). Example: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Single Sign-On

List sign-in providers Providers the current tenant's sign-in page offers, sorted for display, with the tenant's login mode. `kind` is `platform` for providers that the platform operates and `tenant` for the tenant's own. Tenants with `local_only` offer none.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/providers" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/providers"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/providers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Providers retrieved successfully",
    "data": {
        "mode": "local_and_sso",
        "primary_provider": "tuhh",
        "providers": [
            {
                "key": "tuhh",
                "display_name": "TUHH-Login",
                "driver": "oidc",
                "icon": "ri-building-4-line",
                "kind": "tenant"
            },
            {
                "key": "google",
                "display_name": "Google",
                "driver": "google",
                "icon": "ri-google-fill",
                "kind": "platform"
            }
        ]
    }
}
 

Request      

GET v1/sso/providers

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Start a sign-in Browser navigation, not an XHR call. Redirects to the provider's sign-in page. Linking a provider (`intent=link`) and test logins (`intent=test`) need the ticket from their own endpoints. Sign-ins (`intent=login`) need `browser_challenge`: the base64url SHA-256 hash of a random secret that the browser keeps for the code exchange. When the provider is not available, the browser returns to `return_to` with `sso_error=provider_unavailable`. A `return_to` outside the allowed frontends is refused with 400.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/redirect?tenant=mintfit-hamburg&provider=google&return_to=http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fcallback&intent=login&ticket=architecto&browser_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/redirect"
);

const params = {
    "tenant": "mintfit-hamburg",
    "provider": "google",
    "return_to": "http://localhost:3001/auth/callback",
    "intent": "login",
    "ticket": "architecto",
    "browser_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/redirect';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'tenant' => 'mintfit-hamburg',
            'provider' => 'google',
            'return_to' => 'http://localhost:3001/auth/callback',
            'intent' => 'login',
            'ticket' => 'architecto',
            'browser_challenge' => 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (302, Redirect to the provider):



 

Example response (400):


{
    "success": false,
    "message": "The return URL is not an allowed frontend.",
    "errors": {
        "code": "invalid_return_to"
    }
}
 

Example response (500):

Show headers
cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 20
x-ratelimit-remaining: 19
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Internal Server Error",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/sso/redirect

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

tenant   string     

Tenant ID or slug. Example: mintfit-hamburg

provider   string     

Provider key. Example: google

return_to   string     

Absolute frontend URL to return to. Example: http://localhost:3001/auth/callback

intent   string  optional    

login (default), link or test. Example: login

ticket   string  optional    

Ticket for link and test. Example: architecto

browser_challenge   string  optional    

Required for login. Example: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

Provider callback Where every provider returns the browser to, for all tenants (GET, or POST for Apple). Redirects to the frontend's `return_to` with one of: `sso_code` (sign-in succeeded; exchange it within 60 seconds), `sso_result=linked&provider=…`, `sso_test=…`, or `sso_error=…&provider=…` with `sso_pending=…` for `account_exists` and `email_required`. An unknown or used state cannot be returned anywhere and gets 400 with `errors.code=expired`.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/callback" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/callback"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/callback';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (302, Redirect to the frontend):



 

Example response (400):


{
    "success": false,
    "message": "This sign-in has expired or was already used. Please start again.",
    "errors": {
        "code": "expired"
    }
}
 

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 20
x-ratelimit-remaining: 19
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "This sign-in has expired or was already used. Please start again.",
    "errors": {
        "code": "expired"
    },
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/sso/callback

POST v1/sso/callback

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Exchange the sign-in code Trades the one-time `sso_code` from the callback for tokens. The response equals the password login, plus `event`: `login` or `registered`. Only the browser that started the sign-in can trade the code: it sends the secret whose hash it passed as `browser_challenge`. A provider that the tenant stopped offering since the callback (disabled, deleted, sharing withdrawn, local login only) is refused with 409 `provider_unavailable`.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/sso/exchange" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"code\": \"architecto\",
    \"browser_secret\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/identity/sso/exchange"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "code": "architecto",
    "browser_secret": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/sso/exchange';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'code' => 'architecto',
            'browser_secret' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Login successful",
    "data": {
        "user": {
            "id": "01kav3fcmme10dwbeexyqkfndn",
            "email": "user@example.com"
        },
        "tokens": {
            "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
            "token_type": "Bearer",
            "expires_in": 86400
        },
        "event": "login"
    }
}
 

Example response (400):


{
    "success": false,
    "message": "This sign-in has expired or was already used. Please start again.",
    "errors": {
        "code": "expired"
    }
}
 

Example response (409):


{
    "success": false,
    "message": "The provider is no longer available.",
    "errors": {
        "code": "provider_unavailable"
    }
}
 

Request      

POST v1/identity/sso/exchange

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

code   string     

The sso_code from the callback. Example: architecto

browser_secret   string     

The secret behind the sign-in's browser_challenge. Example: architecto

Complete an account without email address After `sso_error=email_required`, creates the account with the email address the person entered. The address receives the usual verification mail. Responds like the code exchange with `event=registered`. An address of an existing account is refused with 409 (`account_exists`); the pending ticket then stays valid.

Example request:
curl --request POST \
    "http://localhost:8000/v1/identity/sso/complete-email" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"pending\": \"architecto\",
    \"email\": \"anna@example.com\",
    \"browser_secret\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/identity/sso/complete-email"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "pending": "architecto",
    "email": "anna@example.com",
    "browser_secret": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/identity/sso/complete-email';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'pending' => 'architecto',
            'email' => 'anna@example.com',
            'browser_secret' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (409):


{
    "success": false,
    "message": "An account with this email address already exists.",
    "errors": {
        "code": "account_exists"
    }
}
 

Request      

POST v1/identity/sso/complete-email

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

pending   string     

The sso_pending ticket from the callback. Example: architecto

email   string     

Email address for the new account. Example: anna@example.com

browser_secret   string     

The secret behind the sign-in's browser_challenge. Example: architecto

Single Sign-On Administration

Get sign-in settings Login mode, primary provider and the shared platform providers the tenant may offer. `identities_without_login_method_for_sso_only` counts the learners who could no longer sign in with `sso_only`: accounts without a linked provider and without a staff role.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/admin/settings" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/settings"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/settings';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Settings retrieved successfully",
    "data": {
        "mode": "local_and_sso",
        "primary_provider_id": null,
        "shared_providers": [
            {
                "id": "01J...",
                "key": "google",
                "display_name": "Google",
                "driver": "google",
                "icon": "ri-google-fill",
                "enabled_for_tenant": true
            }
        ],
        "is_platform_tenant": false,
        "identities_without_login_method_for_sso_only": 42
    }
}
 

Request      

GET v1/sso/admin/settings

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Update sign-in settings `sso_only` needs a primary provider that is an active provider of the tenant or one of the shared providers it offers. Returns the settings.

requires authentication

Example request:
curl --request PUT \
    "http://localhost:8000/v1/sso/admin/settings" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"mode\": \"local_and_sso\",
    \"primary_provider_id\": \"architecto\",
    \"shared_provider_ids\": [
        \"architecto\"
    ]
}"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/settings"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "mode": "local_and_sso",
    "primary_provider_id": "architecto",
    "shared_provider_ids": [
        "architecto"
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/settings';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'mode' => 'local_and_sso',
            'primary_provider_id' => 'architecto',
            'shared_provider_ids' => [
                'architecto',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/sso/admin/settings

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

mode   string     

local_and_sso, local_only or sso_only. Example: local_and_sso

primary_provider_id   string  optional    

Provider ID or null. Example: architecto

shared_provider_ids   string[]  optional    

Shared platform providers to offer.

List providers The acting tenant's own providers. Secrets are never returned, only `has_client_secret` and `apple.has_private_key`.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/admin/providers" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/sso/admin/providers

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a provider Partners create `oidc` providers. The platform tenant also creates one provider per preset driver (`google`, `microsoft`, `apple`, `facebook`, `github`, `tiktok`) and may share providers with partners. Slugs are unique per tenant and never overlap between partners and the platform. Outside local development, issuer and discovery URLs must use https.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/sso/admin/providers" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"slug\": \"tuhh\",
    \"driver\": \"oidc\",
    \"display_name\": \"TUHH-Login\",
    \"icon\": \"ri-building-4-line\",
    \"enabled\": true,
    \"shared\": false,
    \"sort_order\": 0,
    \"issuer_url\": \"https:\\/\\/login.tuhh.de\\/realms\\/tuhh\",
    \"discovery_url\": \"http:\\/\\/www.bailey.biz\\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html\",
    \"scopes\": [
        \"openid\",
        \"email\",
        \"profile\"
    ],
    \"client_id\": \"mintfit\",
    \"client_secret\": \"architecto\",
    \"apple\": [],
    \"settings\": []
}"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "slug": "tuhh",
    "driver": "oidc",
    "display_name": "TUHH-Login",
    "icon": "ri-building-4-line",
    "enabled": true,
    "shared": false,
    "sort_order": 0,
    "issuer_url": "https:\/\/login.tuhh.de\/realms\/tuhh",
    "discovery_url": "http:\/\/www.bailey.biz\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html",
    "scopes": [
        "openid",
        "email",
        "profile"
    ],
    "client_id": "mintfit",
    "client_secret": "architecto",
    "apple": [],
    "settings": []
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'slug' => 'tuhh',
            'driver' => 'oidc',
            'display_name' => 'TUHH-Login',
            'icon' => 'ri-building-4-line',
            'enabled' => true,
            'shared' => false,
            'sort_order' => 0,
            'issuer_url' => 'https://login.tuhh.de/realms/tuhh',
            'discovery_url' => 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html',
            'scopes' => [
                'openid',
                'email',
                'profile',
            ],
            'client_id' => 'mintfit',
            'client_secret' => 'architecto',
            'apple' => [],
            'settings' => [],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/sso/admin/providers

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

slug   string     

Public key of the provider. Example: tuhh

driver   string     

Example: oidc

display_name   string     

Example: TUHH-Login

icon   string  optional    

RemixIcon class. Example: ri-building-4-line

enabled   boolean  optional    

Example: true

shared   boolean  optional    

Platform tenant only. Example: false

sort_order   integer  optional    

Example: 0

issuer_url   string  optional    

oidc only. Example: https://login.tuhh.de/realms/tuhh

discovery_url   string  optional    

oidc only; fetch discovery here instead of below the issuer. Example: http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html

scopes   string[]  optional    
client_id   string  optional    

Example: mintfit

client_secret   string  optional    

Write-only. Example: architecto

apple   object  optional    

apple only: team_id, key_id, private_key (write-only, PEM of the .p8 file).

settings   object  optional    

allow_registration, trust_email, allowed_email_domains. New accounts get the tenant's default role.

Get a provider

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/sso/admin/providers/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the provider. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update a provider Same fields as creating. Secrets that are left out stay unchanged. The driver cannot change.

requires authentication

Example request:
curl --request PUT \
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/sso/admin/providers/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the provider. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete a provider Also removes the login methods that use it. Refused with 422 (`provider_in_use_as_primary`) while it is the tenant's primary provider.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/sso/admin/providers/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the provider. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Test a provider's connection Checks discovery, signing keys and client credentials, without signing anybody in.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6/test" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6/test"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6/test';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Provider tested",
    "data": {
        "ok": true,
        "checks": [
            {
                "name": "discovery",
                "ok": true,
                "message": "..."
            },
            {
                "name": "jwks",
                "ok": true,
                "message": "..."
            },
            {
                "name": "credentials",
                "ok": true,
                "message": "..."
            }
        ]
    }
}
 

Request      

POST v1/sso/admin/providers/{id}/test

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the provider. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Start a test login Returns the URL to navigate the browser to. The browser returns to `return_to` with `sso_test=<id>`. No account is created or changed.

requires authentication

Example request:
curl --request POST \
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6/test-login" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"return_to\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6/test-login"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "return_to": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/providers/58C6f7nnFCJ9M6rjyc6rTKMzn6/test-login';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'return_to' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Test login started",
    "data": {
        "redirect_url": "http://localhost:8000/v1/sso/redirect?...&intent=test&ticket=..."
    }
}
 

Request      

POST v1/sso/admin/providers/{id}/test-login

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the provider. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

return_to   string     

Absolute admin URL to return to. Example: architecto

Get a test login result What the provider said about the person, for ten minutes, to the administrator who started the test.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/admin/test-results/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/test-results/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/test-results/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Test result retrieved successfully",
    "data": {
        "provider_key": "tuhh",
        "subject": "f81d4fae",
        "email": "anna@tuhh.de",
        "email_verified": true,
        "name": "Anna Example",
        "raw_claims": {}
    }
}
 

Request      

GET v1/sso/admin/test-results/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the test result. Example: architecto

List failed sign-ins The tenant's latest 50 failed single sign-ons of the last 30 days, with the reason. Never contains tokens or secrets.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/admin/failures" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/failures"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/failures';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{
    "success": true,
    "message": "Failures retrieved successfully",
    "data": [
        {
            "created_at": "2026-10-14T10:00:00+00:00",
            "provider": {
                "key": "tuhh",
                "display_name": "TUHH-Login"
            },
            "code": "provider_error",
            "detail": "The ID token was issued by another issuer."
        }
    ]
}
 

Request      

GET v1/sso/admin/failures

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List an account's login methods Same shape as the participant's own list, without `available`.

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/sso/admin/identities/58C6f7nnFCJ9M6rjyc6rTKMzn6/login-methods" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/identities/58C6f7nnFCJ9M6rjyc6rTKMzn6/login-methods"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/identities/58C6f7nnFCJ9M6rjyc6rTKMzn6/login-methods';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/sso/admin/identities/{identityId}/login-methods

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

identityId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Remove an account's login method Refused with 422 (`last_login_method`) when it is the account's only way to sign in.

requires authentication

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/sso/admin/identities/58C6f7nnFCJ9M6rjyc6rTKMzn6/login-methods/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/sso/admin/identities/58C6f7nnFCJ9M6rjyc6rTKMzn6/login-methods/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/sso/admin/identities/58C6f7nnFCJ9M6rjyc6rTKMzn6/login-methods/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/sso/admin/identities/{identityId}/login-methods/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

identityId   string     

Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

id   string     

The ID of the login method. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Statistics

Get usage events (paginated) Returns a paginated list of usage events with optional filters.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/statistics/events" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"identity_id\": \"architecto\",
    \"event_type\": \"architecto\",
    \"event_category\": \"architecto\",
    \"entity_type\": \"architecto\",
    \"entity_id\": \"architecto\",
    \"start_date\": \"2026-11-03T00:49:05\",
    \"end_date\": \"2052-11-26\",
    \"per_page\": 22,
    \"page\": 67
}"
const url = new URL(
    "http://localhost:8000/v1/statistics/events"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "identity_id": "architecto",
    "event_type": "architecto",
    "event_category": "architecto",
    "entity_type": "architecto",
    "entity_id": "architecto",
    "start_date": "2026-11-03T00:49:05",
    "end_date": "2052-11-26",
    "per_page": 22,
    "page": 67
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/statistics/events';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'identity_id' => 'architecto',
            'event_type' => 'architecto',
            'event_category' => 'architecto',
            'entity_type' => 'architecto',
            'entity_id' => 'architecto',
            'start_date' => '2026-11-03T00:49:05',
            'end_date' => '2052-11-26',
            'per_page' => 22,
            'page' => 67,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/statistics/events

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

identity_id   string  optional    

Example: architecto

event_type   string  optional    

Example: architecto

event_category   string  optional    

Example: architecto

entity_type   string  optional    

Example: architecto

entity_id   string  optional    

Example: architecto

start_date   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-11-26

per_page   integer  optional    

Must be at least 1. Must not be greater than 1000. Example: 22

page   integer  optional    

Must be at least 1. Example: 67

Get aggregated statistics summary Returns aggregated statistics for a time period, grouped by event type and category.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/statistics/summary" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"start_date\": \"2026-11-03T00:49:05\",
    \"end_date\": \"2052-11-26\",
    \"group_by\": \"daily\"
}"
const url = new URL(
    "http://localhost:8000/v1/statistics/summary"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "start_date": "2026-11-03T00:49:05",
    "end_date": "2052-11-26",
    "group_by": "daily"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/statistics/summary';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'start_date' => '2026-11-03T00:49:05',
            'end_date' => '2052-11-26',
            'group_by' => 'daily',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/statistics/summary

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

start_date   string     

Must be a valid date. Example: 2026-11-03T00:49:05

end_date   string     

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-11-26

group_by   string  optional    

Example: daily

Must be one of:
  • hourly
  • daily
  • weekly
  • monthly

Compare two time periods Returns a comparison between two time periods with percentage changes.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/statistics/compare" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"current_start\": \"2026-11-03T00:49:05\",
    \"current_end\": \"2052-11-26\",
    \"previous_start\": \"2026-11-03T00:49:05\",
    \"previous_end\": \"2052-11-26\"
}"
const url = new URL(
    "http://localhost:8000/v1/statistics/compare"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "current_start": "2026-11-03T00:49:05",
    "current_end": "2052-11-26",
    "previous_start": "2026-11-03T00:49:05",
    "previous_end": "2052-11-26"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/statistics/compare';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'current_start' => '2026-11-03T00:49:05',
            'current_end' => '2052-11-26',
            'previous_start' => '2026-11-03T00:49:05',
            'previous_end' => '2052-11-26',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/statistics/compare

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

current_start   string     

Must be a valid date. Example: 2026-11-03T00:49:05

current_end   string     

Must be a valid date. Must be a date after or equal to current_start. Example: 2052-11-26

previous_start   string     

Must be a valid date. Example: 2026-11-03T00:49:05

previous_end   string     

Must be a valid date. Must be a date after or equal to previous_start. Example: 2052-11-26

Get event count by type Returns the count of events for a specific event type.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/statistics/count/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"start_date\": \"2026-11-03T00:49:05\",
    \"end_date\": \"2026-11-03T00:49:05\"
}"
const url = new URL(
    "http://localhost:8000/v1/statistics/count/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "start_date": "2026-11-03T00:49:05",
    "end_date": "2026-11-03T00:49:05"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/statistics/count/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'start_date' => '2026-11-03T00:49:05',
            'end_date' => '2026-11-03T00:49:05',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Example response (404):


{
    "success": false,
    "message": "Unknown event type 'user.unknown'."
}
 

Request      

GET v1/statistics/count/{eventType}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

eventType   string     

Example: architecto

Body Parameters

start_date   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

end_date   string  optional    

Must be a valid date. Example: 2026-11-03T00:49:05

Content overview Counts of the acting tenant's tests, draft tests and question pool and, for the platform tenant, of active banner ads. A count the caller may not read is null.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/statistics/overview" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/statistics/overview"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/statistics/overview';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/statistics/overview

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Record a usage event directly Records a usage event in the acting tenant. Events are queued for async processing.

Example request:
curl --request POST \
    "http://localhost:8000/v1/statistics/record" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/statistics/record"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/statistics/record';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/statistics/record

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Tag Contexts

List all contexts

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tags/contexts" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/contexts"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/contexts';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/tags/contexts

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get single context

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tags/contexts/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/contexts/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/contexts/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/tags/contexts/{context}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

context   string     

The context. Example: architecto

Create context

Example request:
curl --request POST \
    "http://localhost:8000/v1/tags/contexts" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/contexts"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/contexts';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/tags/contexts

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Update context

Example request:
curl --request PATCH \
    "http://localhost:8000/v1/tags/contexts/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/contexts/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/contexts/architecto';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PATCH v1/tags/contexts/{context}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

context   string     

The context. Example: architecto

Delete context

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/tags/contexts/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/contexts/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/contexts/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/tags/contexts/{context}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

context   string     

The context. Example: architecto

Tag Management

List all tags (global + tenant-specific)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tags?per_page=50&page=1&slug=difficulty%3Ahard&context=topic&search=difficulty&created_at_gte=2024-01-01&created_at_lte=2024-12-31&sort=-created_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags"
);

const params = {
    "per_page": "50",
    "page": "1",
    "slug": "difficulty:hard",
    "context": "topic",
    "search": "difficulty",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "sort": "-created_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '50',
            'page' => '1',
            'slug' => 'difficulty:hard',
            'context' => 'topic',
            'search' => 'difficulty',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'sort' => '-created_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 50. Example: 50

page   integer  optional    

Current page number. Defaults to 1. Example: 1

slug   string  optional    

Filter by exact slug. Example: difficulty:hard

context   string  optional    

Only tags assigned in this context. Example: topic

search   string  optional    

Search in label (multilingual). Example: difficulty

created_at_gte   string  optional    

Filter tags created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter tags created on or before date. Example: 2024-12-31

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -created_at

Create new tag

Example request:
curl --request POST \
    "http://localhost:8000/v1/tags" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/tags

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Bulk delete tags

Example request:
curl --request POST \
    "http://localhost:8000/v1/tags/bulk-delete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        \"bngzmiyvdljnikhwaykcmyuwpw\"
    ],
    \"force\": false
}"
const url = new URL(
    "http://localhost:8000/v1/tags/bulk-delete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        "bngzmiyvdljnikhwaykcmyuwpw"
    ],
    "force": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/bulk-delete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'ids' => [
                'bngzmiyvdljnikhwaykcmyuwpw',
            ],
            'force' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/tags/bulk-delete

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   string[]     

Must be 26 characters.

force   boolean  optional    

Example: false

Get tag by ID

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/tags/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the tag. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update tag

Example request:
curl --request PATCH \
    "http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PATCH",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PATCH v1/tags/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the tag. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete tag

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tags/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/tags/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the tag. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Tenant Management

List all tenants (public - no auth required)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tenants?per_page=20&page=1&enabled=1&search=acme&created_at_gte=2024-01-01&created_at_lte=2024-12-31&sort=-created_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tenants"
);

const params = {
    "per_page": "20",
    "page": "1",
    "enabled": "1",
    "search": "acme",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "sort": "-created_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'enabled' => '1',
            'search' => 'acme',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'sort' => '-created_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Tenants retrieved successfully", "data": [...], "meta": {"pagination": {"total": 50, "per_page": 10, "current_page": 1, "last_page": 5}}}
 

Request      

GET v1/tenants

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 10. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

enabled   boolean  optional    

Filter by enabled status. Example: true

search   string  optional    

Search in name, slug, and domain. Example: acme

created_at_gte   string  optional    

Filter tenants created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter tenants created on or before date. Example: 2024-12-31

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -created_at

Create new tenant with initial admin user.

Only identities of the platform tenant may create tenants.

Example request:
curl --request POST \
    "http://localhost:8000/v1/tenants" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Acme Corp\",
    \"slug\": \"acme\",
    \"domain\": \"acme.com\",
    \"description\": \"Eius et animi quos velit et.\",
    \"settings\": {
        \"default_locale\": \"en\"
    },
    \"enabled\": false,
    \"admin_email\": \"admin@acme.com\",
    \"admin_password\": \"architecto\"
}"
const url = new URL(
    "http://localhost:8000/v1/tenants"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Acme Corp",
    "slug": "acme",
    "domain": "acme.com",
    "description": "Eius et animi quos velit et.",
    "settings": {
        "default_locale": "en"
    },
    "enabled": false,
    "admin_email": "admin@acme.com",
    "admin_password": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Acme Corp',
            'slug' => 'acme',
            'domain' => 'acme.com',
            'description' => 'Eius et animi quos velit et.',
            'settings' => [
                'default_locale' => 'en',
            ],
            'enabled' => false,
            'admin_email' => 'admin@acme.com',
            'admin_password' => 'architecto',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (201):


{"success": true, "message": "Tenant and admin user created successfully", "data": {"tenant": {...}, "admin": {"email": "admin@acme.com", "generated_password": "kR8mP2vN9qT3wX7z"}}}
 

Example response (409):


{
    "success": false,
    "message": "Another tenant, possibly a deleted one, already uses this slug (tenants_slug_unique)."
}
 

Request      

POST v1/tenants

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Tenant name. Example: Acme Corp

slug   string     

Tenant slug. Example: acme

domain   string  optional    

nullable Tenant domain. Example: acme.com

description   string  optional    

nullable Tenant description. Example: Eius et animi quos velit et.

settings   object  optional    

nullable Tenant settings. They replace the stored settings as a whole, except module sections such as sso, which have their own endpoints.

default_locale   string  optional    

nullable Default language of the tenant, de or en: notifications to members whose profile names no language are written in it. Unset uses NOTIFICATION_DEFAULT_LOCALE. Example: en

enabled   boolean  optional    

nullable Tenant enabled status. Defaults to true. Example: false

admin_email   string     

Admin user email. Example: admin@acme.com

admin_password   string  optional    

nullable Admin password (min 8 chars). If omitted, a secure password is generated and returned. Example: architecto

Get tenant by ID (public - no auth required)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"id": "01ARZ3...", "name": "Acme Corp", ...}
 

Example response (404):


{
    "message": "Tenant not found"
}
 

Request      

GET v1/tenants/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the tenant. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update tenant by ID A tenant may change its own record; the platform tenant may change any tenant. The platform tenant's slug cannot be changed or taken.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"settings\": {
        \"default_locale\": \"en\"
    }
}"
const url = new URL(
    "http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "settings": {
        "default_locale": "en"
    }
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'settings' => [
                'default_locale' => 'en',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"id": "01ARZ3...", "name": "Updated Corp", ...}
 

Example response (404):


{
    "message": "Tenant not found"
}
 

Example response (409):


{
    "success": false,
    "message": "Another tenant, possibly a deleted one, already uses this slug (tenants_slug_unique)."
}
 

Request      

PUT v1/tenants/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the tenant. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Body Parameters

settings   object  optional    

nullable Tenant settings. They replace the stored settings as a whole, except module sections such as sso, which have their own endpoints.

default_locale   string  optional    

nullable Default language of the tenant, de or en: notifications to members whose profile names no language are written in it. Unset uses NOTIFICATION_DEFAULT_LOCALE. Example: en

Delete tenant by ID (soft delete) A tenant may delete itself; the platform tenant may delete any tenant.

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"id": "01ARZ3...", "deleted_at": "2025-12-03T10:30:00Z", ...}
 

Example response (404):


{
    "message": "Tenant not found"
}
 

Request      

DELETE v1/tenants/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the tenant. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Get tenant by slug (public - no auth required)

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/tenants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tenants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"id": "01ARZ3...", "name": "Acme Corp", "slug": "acme-corp", ...}
 

Example response (404):


{
    "message": "Tenant not found"
}
 

Request      

GET v1/tenants/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the tenant. Example: architecto

Update tenant by slug A tenant may change its own record; the platform tenant may change any tenant. The platform tenant's slug cannot be changed or taken.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/tenants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"settings\": {
        \"default_locale\": \"en\"
    }
}"
const url = new URL(
    "http://localhost:8000/v1/tenants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "settings": {
        "default_locale": "en"
    }
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/architecto';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'settings' => [
                'default_locale' => 'en',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"id": "01ARZ3...", "name": "Updated Corp", ...}
 

Example response (404):


{
    "message": "Tenant not found"
}
 

Example response (409):


{
    "success": false,
    "message": "Another tenant, possibly a deleted one, already uses this slug (tenants_slug_unique)."
}
 

Request      

PUT v1/tenants/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the tenant. Example: architecto

Body Parameters

settings   object  optional    

nullable Tenant settings. They replace the stored settings as a whole, except module sections such as sso, which have their own endpoints.

default_locale   string  optional    

nullable Default language of the tenant, de or en: notifications to members whose profile names no language are written in it. Unset uses NOTIFICATION_DEFAULT_LOCALE. Example: en

Delete tenant by slug (soft delete) A tenant may delete itself; the platform tenant may delete any tenant.

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/tenants/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/tenants/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/tenants/architecto';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"id": "01ARZ3...", "deleted_at": "2025-12-03T10:30:00Z", ...}
 

Example response (404):


{
    "message": "Tenant not found"
}
 

Request      

DELETE v1/tenants/{slug}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The slug of the tenant. Example: architecto

User Management

Get the currently authenticated user account `profile` contains what the account holder may see about themselves; data that staff keep about the account is left out.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/me" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/me"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/me';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/me

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Update the currently authenticated user's own profile Accepts the self-managed parts of `profile` only: name, first_name, last_name, bio, birthday (DD.MM.YYYY), state, status, language, locale and preferences (rememberTheme, compactDashboard, betaAccess). Given keys replace stored ones, `null` removes a key, others stay; only the preferences sent are validated. Responds with the account as `GET /v1/users/me` does. The email address changes through `/v1/verification/email-change`, the avatar through `/v1/users/me/avatar`.

Example request:
curl --request PATCH \
    "http://localhost:8000/v1/users/me" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"profile\": {
        \"first_name\": \"Anna\",
        \"preferences\": {
            \"compactDashboard\": true
        }
    }
}"
const url = new URL(
    "http://localhost:8000/v1/users/me"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "profile": {
        "first_name": "Anna",
        "preferences": {
            "compactDashboard": true
        }
    }
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/me';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'profile' => [
                'first_name' => 'Anna',
                'preferences' => [
                    'compactDashboard' => true,
                ],
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PATCH v1/users/me

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

profile   object     

Profile keys to change.

Delete the currently authenticated user account

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/users/me" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/me"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/me';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/users/me

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Remove the currently authenticated user's avatar The image is deleted once no account shows it. Responds with the account as `GET /v1/users/me` does.

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/users/me/avatar" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/me/avatar"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/me/avatar';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/users/me/avatar

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

List all users in tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users?per_page=20&page=1&is_anonymous=&search=john&created_at_gte=2024-01-01&created_at_lte=2024-12-31&sort=-created_at" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users"
);

const params = {
    "per_page": "20",
    "page": "1",
    "is_anonymous": "0",
    "search": "john",
    "created_at_gte": "2024-01-01",
    "created_at_lte": "2024-12-31",
    "sort": "-created_at",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '20',
            'page' => '1',
            'is_anonymous' => '0',
            'search' => 'john',
            'created_at_gte' => '2024-01-01',
            'created_at_lte' => '2024-12-31',
            'sort' => '-created_at',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (200):


{"success": true, "message": "Users retrieved successfully", "data": [...], "meta": {"pagination": {"total": 50, "per_page": 10, "current_page": 1, "last_page": 5}}}
 

Request      

GET v1/users

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Number of items per page (1-100). Defaults to 10. Example: 20

page   integer  optional    

Current page number. Defaults to 1. Example: 1

is_anonymous   boolean  optional    

Filter by anonymous status. Example: false

search   string  optional    

Search in email, phone, and username. Example: john

created_at_gte   string  optional    

Filter users created on or after date. Example: 2024-01-01

created_at_lte   string  optional    

Filter users created on or before date. Example: 2024-12-31

sort   string  optional    

Sort field. Prefix with '-' for descending. Example: -created_at

Create new user in tenant

Example request:
curl --request POST \
    "http://localhost:8000/v1/users" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/users

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get user by ID within tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the user. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Update user by ID within tenant `profile` keys that are sent replace stored ones and `null` removes a key; keys that are not sent, such as those the account holder saved, stay. `profile.preferences` is merged key by key. Images are set by `avatar_media_id` and `cover_media_id`; responses carry fresh `avatar_url` and `cover_url` for them, which cannot be sent.

Example request:
curl --request PUT \
    "http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

PUT v1/users/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the user. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

Delete user by ID (soft delete) within tenant

Example request:
curl --request DELETE \
    "http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/58C6f7nnFCJ9M6rjyc6rTKMzn6';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

DELETE v1/users/{id}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the user. Example: 58C6f7nnFCJ9M6rjyc6rTKMzn6

User Search

Find user by any identifier (email, phone, username, or ID) within tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/find/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/find/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/find/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/find/{identifier}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

identifier   string     

Example: architecto

Search users by query string within tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/search/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/search/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/search/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/search/{query}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

query   string     

Example: architecto

Find user by email within tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/by-email/gbailey@example.net" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/by-email/gbailey@example.net"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/by-email/gbailey@example.net';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/by-email/{email}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

email   string     

Example: gbailey@example.net

Find user by phone within tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/by-phone/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/by-phone/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/by-phone/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/by-phone/{phone}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

phone   string     

Example: architecto

Find user by username within tenant

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/users/by-username/architecto" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/users/by-username/architecto"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/users/by-username/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "A valid Tenant context is required for this request. Please provide a valid X-Tenant-ID header.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/users/by-username/{username}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

username   string     

Example: architecto

Verification

Initiate verification for any handle type Sends a 6-digit verification code to the authenticated user's handle (email, phone, etc.).

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/email/initiate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/verification/email/initiate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/email/initiate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/{handleType}/initiate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

handleType   string     

The handle type to verify (email, phone). Example: email

Verify handle with code Verifies the user's handle (email, phone, etc.) using the provided 6-digit code.

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/email/verify" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"code\": \"123456\"
}"
const url = new URL(
    "http://localhost:8000/v1/verification/email/verify"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "code": "123456"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/email/verify';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'code' => '123456',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/{handleType}/verify

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

handleType   string     

The handle type to verify (email, phone). Example: email

Body Parameters

code   string     

The 6-digit verification code. Example: 123456

Resend verification code Resends a new verification code to the user's handle (email, phone, etc.).

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/email/resend" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/verification/email/resend"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/email/resend';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/{handleType}/resend

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

handleType   string     

The handle type to verify (email, phone). Example: email

Request an email change Sends a 6-digit code to the new address. The account keeps its current address until the code is confirmed. Requesting again replaces the code.

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/email-change/initiate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"email\": \"new@example.com\"
}"
const url = new URL(
    "http://localhost:8000/v1/verification/email-change/initiate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "email": "new@example.com"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/email-change/initiate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'email' => 'new@example.com',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (422):


{
    "success": false,
    "message": "Validation failed",
    "errors": {
        "email": [
            "The email has already been taken."
        ]
    }
}
 

Request      

POST v1/verification/email-change/initiate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

email   string     

The new email address. Example: new@example.com

Confirm an email change Sets the address the code was sent to as the account's email address, verified, and returns the updated account. Ends the account's other logins; the login making the request stays signed in.

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/email-change/confirm" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"code\": \"123456\"
}"
const url = new URL(
    "http://localhost:8000/v1/verification/email-change/confirm"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "code": "123456"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/email-change/confirm';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'code' => '123456',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/email-change/confirm

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

code   string     

The 6-digit code sent to the new address. Example: 123456

Initiate password reset Sends a password reset link to the user's email. No authentication required. Always returns success to prevent user enumeration.

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/password-reset/initiate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/verification/password-reset/initiate"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/password-reset/initiate';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/password-reset/initiate

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Verify password reset token Validates a password reset token without consuming it. Used by frontend to show password reset form.

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/password-reset/verify" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/verification/password-reset/verify"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/password-reset/verify';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/password-reset/verify

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Complete password reset Resets the user's password using the provided token and new password. Token is consumed and cannot be reused. Ends all logins of the account: access and refresh tokens issued before are refused from now on.

Example request:
curl --request POST \
    "http://localhost:8000/v1/verification/password-reset/complete" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/verification/password-reset/complete"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/password-reset/complete';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Request      

POST v1/verification/password-reset/complete

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Get verification status Returns the current verification status for the authenticated user.

Example request:
curl --request GET \
    --get "http://localhost:8000/v1/verification/status" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/v1/verification/status"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'http://localhost:8000/v1/verification/status';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
vary: Origin
 

{
    "success": false,
    "message": "Full authentication required. Anonymous access not allowed for this endpoint.",
    "errors": null,
    "meta": {
        "timestamp": "2026-11-03T00:49:05+00:00"
    }
}
 

Request      

GET v1/verification/status

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json