{"info":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","description":"<html><head></head><body><h2 id=\"ekool-public-api-v3\">eKool public API (v3)</h2>\n<p>RESTful read API for authorized third-party platforms to access school data on behalf of an authenticated eKool user.</p>\n<h3 id=\"hosts\">Hosts</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th></th>\n<th>login (auth)</th>\n<th>API</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Staging</strong></td>\n<td><code>https://login.ekool.dev</code></td>\n<td><code>https://api-v2.ekool.dev</code></td>\n</tr>\n<tr>\n<td><strong>Production</strong></td>\n<td><code>https://login.ekool.eu</code></td>\n<td><code>https://api-v2.ekool.eu</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>Set the collection variables <code>login_url</code> and <code>api_url</code> for the environment you target.</p>\n<h3 id=\"authorization-model\">Authorization model</h3>\n<ul>\n<li><strong>OAuth 2.0 Authorization Code grant with PKCE</strong> is required for every client. eKool does not issue client secrets — PKCE alone authenticates the token exchange.</li>\n<li>The bearer access token is sent in the <code>Authorization</code> header for every API call: <code>Authorization: Bearer &lt;access_token&gt;</code>.</li>\n<li>Tokens carry the partner's <code>client_id</code> and the scopes granted by the user during consent. eKool also tracks per-school <strong>agreements</strong> between the partner and each school.</li>\n</ul>\n<h3 id=\"final-access\">Final access</h3>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>Final access = (Client scopes ∩ User-approved scopes) + Active school agreements\n</code></pre><ul>\n<li>Scopes control <strong>which endpoints</strong> the partner can call.</li>\n<li>School agreements control <strong>which schools</strong> are reachable. Single-school endpoints reject schools without an agreement (<code>403 no_school_agreement</code>); the schools list endpoint exposes a per-item <code>has_agreement</code> flag.</li>\n</ul>\n<h3 id=\"available-scopes\">Available scopes</h3>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Scope</th>\n<th>Grants</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>user:read</code></td>\n<td><code>GET /api/v3/users/me</code></td>\n</tr>\n<tr>\n<td><code>schools:read</code></td>\n<td><code>GET /api/v3/users/me/schools</code>, <code>GET /api/v3/schools/{id}/roles</code></td>\n</tr>\n<tr>\n<td><code>journals:read</code></td>\n<td><code>GET /api/v3/schools/{id}/journals</code></td>\n</tr>\n<tr>\n<td><code>journal.students:read</code></td>\n<td><code>GET /api/v3/schools/{id}/journals/{journal_id}/students</code></td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"response-envelope\">Response envelope</h3>\n<p><strong>Success:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"status\": true,\n  \"meta\": { \"total\": 42, \"last_page\": 3, \"current_page\": 1, \"per_page\": 20 },\n  \"data\": [ ... ]\n}\n</code></pre>\n<p><strong>Error:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"status\": false,\n  \"errors\": [ { \"code\": 403, \"message\": \"not_allowed_action\" } ]\n}\n</code></pre>\n<ul>\n<li><code>code</code> is the numeric HTTP status code.</li>\n<li><code>message</code> is a short machine-readable identifier (<code>bad_request</code>, <code>not_allowed_action</code>, <code>no_school_agreement</code>, <code>data_not_found</code>, <code>invalid_context_role</code>, <code>journal_does_not_belong_to_school</code>) or a specific human-readable message.</li>\n<li><code>errors</code> is always an array; it may contain one or more entries.</li>\n</ul>\n<h3 id=\"pagination\">Pagination</h3>\n<p>List endpoints accept <code>page</code> (default <code>1</code>) and <code>per_page</code> (default <code>20</code>).</p>\n<h3 id=\"onboarding\">Onboarding</h3>\n<p>To create a new client, send an email to <code>help@ekool.eu</code> with:</p>\n<ul>\n<li>the institution / company name;</li>\n<li>the responsible person's name, e-mail and phone;</li>\n<li>the integration use case;</li>\n<li>one or more <code>redirect_uri</code> values (e.g. <code>https://my.application.com/oauth2/callback</code>).</li>\n</ul>\n<p>You will receive a <code>client_id</code> and the list of granted scopes. eKool does not issue <code>client_secret</code> — every client (mobile, SPA, native, server-side) authenticates the token exchange with PKCE.</p>\n<p>Keep your <code>client_id</code> private and never embed it in user-visible source for higher-trust integrations. For partner-agreement onboarding (per-school access) eKool sets up the agreements on its side; partners do not manage them.</p>\n</body></html>","schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json","toc":[],"owner":"17171853","collectionId":"d500d161-614f-4c83-9f85-63f9af5ab021","publishedId":"2sBXqMHeSR","public":true,"customColor":{"top-bar":"FFFFFF","right-sidebar":"303030","highlight":"FF6C37"},"publishDate":"2026-10-27T13:05:17.000Z"},"item":[{"name":"Authorization (OAuth 2.0 + PKCE)","item":[{"name":"1. Authorization request (opens in browser)","id":"61977162-873a-45f0-9873-2d677dad6842","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"auth":{"type":"noauth","isInherited":false},"method":"GET","header":[],"url":"https://login.ekool.dev/oauth/authorize?client_id=&redirect_uri=https://my.application.com/oauth2/callback&response_type=code&scope=user:read schools:read journals:read journal.students:read&code_challenge=&code_challenge_method=S256&state=xyz","description":"<p><strong>Open this URL in a browser</strong> — it cannot be completed via Postman's <em>Send</em> button because the user must log in and consent.</p>\n<p><code>code_challenge</code> and <code>code_verifier</code> are generated client-side by the partner application; this collection's folder-level pre-request script fills them automatically for Postman testing (see the folder description).</p>\n<p>After consent, eKool redirects to:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>{redirect_uri}?code=&lt;auth_code&gt;&amp;state=xyz\n</code></pre><p>Copy <code>&lt;auth_code&gt;</code> into the <code>code</code> collection variable, then proceed to step 2.</p>\n","urlObject":{"path":["oauth","authorize"],"host":["https://login.ekool.dev"],"query":[{"description":{"content":"<p>Client identifier issued by eKool</p>\n","type":"text/plain"},"key":"client_id","value":""},{"description":{"content":"<p>Must match one of the redirect URIs registered for the client</p>\n","type":"text/plain"},"key":"redirect_uri","value":"https://my.application.com/oauth2/callback"},{"description":{"content":"<p>Always <code>code</code></p>\n","type":"text/plain"},"key":"response_type","value":"code"},{"description":{"content":"<p>Space-separated scopes; user is asked to consent to these</p>\n","type":"text/plain"},"key":"scope","value":"user:read schools:read journals:read journal.students:read"},{"description":{"content":"<p>BASE64URL(SHA256(code_verifier)) — generated locally by the client app (see folder description, step 0)</p>\n","type":"text/plain"},"key":"code_challenge","value":""},{"description":{"content":"<p>Always <code>S256</code> — eKool does not accept the <code>plain</code> method</p>\n","type":"text/plain"},"key":"code_challenge_method","value":"S256"},{"description":{"content":"<p>Opaque value echoed back on redirect; verify it on your callback to mitigate CSRF</p>\n","type":"text/plain"},"key":"state","value":"xyz"}],"variable":[]}},"response":[{"id":"ef2f0537-7562-4b24-96be-f6becf1d0209","name":"Success — 302 redirect","originalRequest":{"method":"GET","header":[],"url":"https://login.ekool.dev/oauth/authorize"},"status":"Found","code":302,"_postman_previewlanguage":"Text","header":[],"cookie":[],"responseTime":null,"body":"{redirect_uri}?code=<auth_code>&state=xyz"},{"id":"d610f932-7908-42df-a927-ae2fc3030491","name":"Error — 302 redirect with error params","originalRequest":{"method":"GET","header":[],"url":"https://login.ekool.dev/oauth/authorize"},"status":"Found","code":302,"_postman_previewlanguage":"Text","header":[],"cookie":[],"responseTime":null,"body":"{redirect_uri}?error=<error name>&error_description=<error description>&hint=<error hint>&state=xyz"}],"_postman_id":"61977162-873a-45f0-9873-2d677dad6842"},{"name":"2. Token exchange (code -> access_token)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["// On success, store the tokens for the API requests.","if (pm.response.code === 200) {","    const json = pm.response.json();","    if (json.access_token) {","        pm.collectionVariables.set('access_token', json.access_token);","    }","    if (json.refresh_token) {","        pm.collectionVariables.set('refresh_token', json.refresh_token);","    }","}"],"id":"f31ea2e3-1cbe-4251-a93b-5e6359ba2003"}}],"id":"aded723d-578c-4e4e-b695-4b39cce90282","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"auth":{"type":"noauth","isInherited":false},"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"authorization_code","type":"text","description":"<p>Always <code>authorization_code</code></p>\n"},{"key":"client_id","value":"","type":"text","description":"<p>OAuth client identifier</p>\n"},{"key":"redirect_uri","value":"https://my.application.com/oauth2/callback","type":"text","description":"<p>Must match the <code>redirect_uri</code> used in step 1</p>\n"},{"key":"code","value":"","type":"text","description":"<p>Authorization code obtained from the redirect in step 1</p>\n"},{"key":"code_verifier","value":"","type":"text","description":"<p>Plain <code>code_verifier</code> generated client-side (step 0); eKool re-derives the challenge and compares it with the value sent in step 1</p>\n"}]},"url":"https://login.ekool.dev/oauth/token","description":"<p>Exchanges the authorization <code>code</code> for an access token. PKCE is enforced: the <code>code_verifier</code> sent here must match the <code>code_challenge</code> sent in step 1.</p>\n<p>A <code>test</code> script copies <code>access_token</code> and <code>refresh_token</code> from the response into collection variables so the API requests in the next folder authenticate automatically.</p>\n","urlObject":{"path":["oauth","token"],"host":["https://login.ekool.dev"],"query":[],"variable":[]}},"response":[{"id":"b0b155ce-7c4b-48ee-99ce-067b678213e1","name":"200 OK — Tokens issued","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"authorization_code","type":"text"},{"key":"client_id","value":"","type":"text"},{"key":"redirect_uri","value":"https://my.application.com/oauth2/callback","type":"text"},{"key":"code","value":"","type":"text"},{"key":"code_verifier","value":"","type":"text"}]},"url":"https://login.ekool.dev/oauth/token"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n    \"token_type\": \"Bearer\",\n    \"expires_in\": 604800,\n    \"access_token\": \"eyJ0eXiJS1A...UzIi7_A\",\n    \"refresh_token\": \"def5020c40f...d20b6a62\"\n}"},{"id":"d8bdf5e0-9959-4b6f-bd93-e560e6dbf2f9","name":"400 Bad Request — invalid `code_verifier` (PKCE mismatch)","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"authorization_code","type":"text"},{"key":"client_id","value":"","type":"text"},{"key":"redirect_uri","value":"https://my.application.com/oauth2/callback","type":"text"},{"key":"code","value":"","type":"text"},{"key":"code_verifier","value":"wrong-verifier","type":"text"}]},"url":"https://login.ekool.dev/oauth/token"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n    \"error\": \"invalid_grant\",\n    \"error_description\": \"Failed to verify code challenge.\",\n    \"hint\": \"...\",\n    \"message\": \"...\"\n}"},{"id":"aec936f5-5ca8-4aa5-95d2-a11b8f8b92ca","name":"401 Unauthorized — invalid `code` or `client_id`","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"authorization_code","type":"text"},{"key":"client_id","value":"","type":"text"},{"key":"redirect_uri","value":"https://my.application.com/oauth2/callback","type":"text"},{"key":"code","value":"","type":"text"},{"key":"code_verifier","value":"","type":"text"}]},"url":"https://login.ekool.dev/oauth/token"},"status":"Unauthorized","code":401,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n    \"error\": \"...\",\n    \"error_description\": \"...\",\n    \"hint\": \"...\",\n    \"message\": \"...\"\n}"}],"_postman_id":"aded723d-578c-4e4e-b695-4b39cce90282"},{"name":"3. Refresh token","event":[{"listen":"test","script":{"type":"text/javascript","exec":["if (pm.response.code === 200) {","    const json = pm.response.json();","    if (json.access_token) {","        pm.collectionVariables.set('access_token', json.access_token);","    }","    if (json.refresh_token) {","        pm.collectionVariables.set('refresh_token', json.refresh_token);","    }","}"],"id":"3f7daca2-b051-4f8d-a9dd-e8331654ac21"}}],"id":"ea655d97-358c-434e-897f-86f3eb1e78aa","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"auth":{"type":"noauth","isInherited":false},"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"refresh_token","type":"text","description":"<p>Always <code>refresh_token</code></p>\n"},{"key":"client_id","value":"","type":"text"},{"key":"refresh_token","value":"","type":"text","description":"<p>The <code>refresh_token</code> obtained in step 2</p>\n"}]},"url":"https://login.ekool.dev/oauth/token","description":"<p>Exchanges a <code>refresh_token</code> for a new access token (and usually a new refresh token). No PKCE is needed here — refresh is bound to the original client.</p>\n<p>The <code>test</code> script writes the new tokens back into the <code>access_token</code> / <code>refresh_token</code> collection variables.</p>\n","urlObject":{"path":["oauth","token"],"host":["https://login.ekool.dev"],"query":[],"variable":[]}},"response":[{"id":"85076906-14bc-485d-9260-cbd09cfaabc1","name":"200 OK — Tokens refreshed","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"refresh_token","type":"text"},{"key":"client_id","value":"","type":"text"},{"key":"refresh_token","value":"","type":"text"}]},"url":"https://login.ekool.dev/oauth/token"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n    \"token_type\": \"Bearer\",\n    \"expires_in\": 604800,\n    \"access_token\": \"eyJ0eXiJS1A...UzIi7_A\",\n    \"refresh_token\": \"def5020c40f...d20b6a62\"\n}"},{"id":"ba0be064-d6cb-4c61-94ba-27b055dfecf6","name":"401 Unauthorized — invalid or revoked refresh_token","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/x-www-form-urlencoded"}],"body":{"mode":"urlencoded","urlencoded":[{"key":"grant_type","value":"refresh_token","type":"text"},{"key":"client_id","value":"","type":"text"},{"key":"refresh_token","value":"","type":"text"}]},"url":"https://login.ekool.dev/oauth/token"},"status":"Unauthorized","code":401,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n    \"error\": \"...\",\n    \"error_description\": \"...\",\n    \"hint\": \"...\",\n    \"message\": \"...\"\n}"}],"_postman_id":"ea655d97-358c-434e-897f-86f3eb1e78aa"}],"id":"ca110eb6-b381-434e-973e-c278fa4f03b0","description":"<p>Authorization Code grant with PKCE (RFC 7636). PKCE is required for every client. eKool does not issue client secrets — only <code>client_id</code> plus the PKCE pair authenticate the token exchange.</p>\n<h3 id=\"step-0--generate-code_verifier-and-code_challenge-no-ekool-request\">Step 0 — generate <code>code_verifier</code> and <code>code_challenge</code> (no eKool request)</h3>\n<p>The partner application generates the PKCE pair <strong>locally, in its own code</strong>, before starting the authorization flow. There is no eKool endpoint that returns these values — they are entirely client-side.</p>\n<ul>\n<li><code>code_verifier</code>: a cryptographically random string, 43–128 characters from the unreserved set <code>[A-Z a-z 0-9 - . _ ~]</code>. Generate with a CSPRNG (e.g. 32 random bytes, base64url-encoded without padding).</li>\n<li><code>code_challenge</code>: <code>BASE64URL(SHA256(code_verifier))</code>, sent in the authorization request with <code>code_challenge_method=S256</code> (eKool does not accept the <code>plain</code> method).</li>\n<li>The same <code>code_verifier</code> is later sent during the token exchange. eKool re-derives the challenge from it and rejects the exchange if it does not match.</li>\n</ul>\n<p>Reference (JavaScript):</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-js\">const toBase64Url = (buf) =&gt; Buffer.from(buf).toString('base64')\n    .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n\nconst verifier = toBase64Url(crypto.randomBytes(32));\nconst challenge = toBase64Url(crypto.createHash('sha256').update(verifier).digest());\n</code></pre>\n<blockquote>\n<p><strong>Postman testing convenience:</strong> this collection's folder-level pre-request script auto-fills <code></code> and <code></code> if they are empty, so partners trying out the flow do not have to compute them manually. Real applications must implement this step in their own code.</p>\n</blockquote>\n<h3 id=\"steps-13--actual-oauth-flow\">Steps 1–3 — actual OAuth flow</h3>\n<ol>\n<li><strong>Authorization request</strong> — open the URL in a browser (Postman's <em>Send</em> cannot complete interactive login). The user logs in to eKool and consents. eKool redirects to <code>redirect_uri?code=&lt;auth_code&gt;&amp;state=...</code>. Copy the <code>code</code> value into the <code>code</code> collection variable.</li>\n<li><strong>Token exchange</strong> — send the request to swap <code>code</code> + <code>code_verifier</code> for <code>access_token</code> and <code>refresh_token</code>. The test script stores them in <code>access_token</code> / <code>refresh_token</code>.</li>\n<li><strong>Refresh token</strong> — when the access token expires, send this request to get a new one without prompting the user.</li>\n</ol>\n<p><strong>Important:</strong> keep the same <code>code_verifier</code> between the authorization request (which sends its <code>code_challenge</code>) and the token exchange (which sends the verifier itself). A new PKCE pair must be generated for each new authorization, but never between authorize and token-exchange of the same flow.</p>\n","event":[{"listen":"prerequest","script":{"type":"text/javascript","exec":["// Postman testing convenience: auto-fill the PKCE pair if it has not been set yet.","// Real partner applications generate code_verifier and code_challenge in their own code,","// not via any HTTP request to eKool.","","const verifier = pm.collectionVariables.get('code_verifier');","if (!verifier) {","    const randomBytes = CryptoJS.lib.WordArray.random(32);","    const v = CryptoJS.enc.Base64.stringify(randomBytes)","        .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');","    const c = CryptoJS.enc.Base64.stringify(CryptoJS.SHA256(v))","        .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');","    pm.collectionVariables.set('code_verifier', v);","    pm.collectionVariables.set('code_challenge', c);","    console.log('PKCE pair auto-generated for Postman testing');","    console.log('  code_verifier  =', v);","    console.log('  code_challenge =', c);","    console.log('Clear both collection variables to force regeneration.');","}"],"id":"bc4cb4f6-97af-4ea1-8213-ed68b7bb191f"}}],"_postman_id":"ca110eb6-b381-434e-973e-c278fa4f03b0","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}}},{"name":"API methods (v3)","item":[{"name":"GET /api/v3/users/me","id":"e9baed4e-8293-4ea5-b460-ea76b09b4bbc","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/users/me","description":"<p>Returns the authenticated user's basic profile.</p>\n<p><strong>Required scope:</strong> <code>user:read</code></p>\n<p>No path or query parameters. Partner-agreement middleware is <strong>not</strong> applied on this endpoint.</p>\n","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}},"urlObject":{"path":["api","v3","users","me"],"host":["https://api-v2.ekool.dev"],"query":[],"variable":[]}},"response":[{"id":"9f505521-fe4d-4444-b1c2-1ab4e326b1c6","name":"200 OK — Authenticated user","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/users/me"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": true,\n  \"meta\": [],\n  \"data\": {\n    \"id\": 12345,\n    \"first_name\": \"Mari\",\n    \"last_name\": \"Tamm\",\n    \"email\": \"mari.tamm@example.edu\",\n    \"profile_img_fn\": \"12345_avatar.jpg\",\n    \"lang_code\": \"et\"\n  }\n}"},{"id":"142dcdf3-8e9a-406d-91e6-197fcc5542dd","name":"401 Unauthorized — missing/invalid token","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/users/me"},"status":"Unauthorized","code":401,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 401, \"message\": \"invalid_token\" }\n  ]\n}"},{"id":"db3a1218-80c0-4289-b0c4-edfbbca85b80","name":"403 Forbidden — token scope does not allow this endpoint","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/users/me"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"Not allowed to access this url.\" }\n  ]\n}"}],"_postman_id":"e9baed4e-8293-4ea5-b460-ea76b09b4bbc"},{"name":"GET /api/v3/users/me/schools","id":"93faea22-7a16-45fc-99f2-7d2d441178eb","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/users/me/schools?page=1&per_page=20","description":"<p>Lists every school the user has an active role in.</p>\n<p><strong>Required scope:</strong> <code>schools:read</code></p>\n<p>This endpoint is <strong>outside</strong> the partner-agreement middleware (no <code>school_id</code> in the URL); it is <strong>not filtered</strong> by agreement.</p>\n<p>Each item carries a <code>has_agreement</code> boolean:</p>\n<ul>\n<li><strong>Partner tokens</strong>: <code>true</code> when the school has an active agreement (an <code>institution_school_package</code> whose package has a <code>school_package_feature</code> row with a matching <code>client_id</code>), <code>false</code> otherwise.</li>\n<li><strong>First-party tokens</strong> (empty scopes): always <code>true</code>.</li>\n</ul>\n<p>Clients should use <code>has_agreement</code> to decide whether to call the agreement-gated single-school endpoints; calling those for a school with <code>has_agreement = false</code> returns <code>403 no_school_agreement</code>.</p>\n","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}},"urlObject":{"path":["api","v3","users","me","schools"],"host":["https://api-v2.ekool.dev"],"query":[{"key":"page","value":"1"},{"key":"per_page","value":"20"}],"variable":[]}},"response":[{"id":"1baa00fb-a792-4d6d-9456-fda6ca018609","name":"200 OK — Schools with per-item `has_agreement`","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":{"raw":"https://api-v2.ekool.dev/api/v3/users/me/schools?page=1&per_page=20","host":["https://api-v2.ekool.dev"],"path":["api","v3","users","me","schools"],"query":[{"key":"page","value":"1"},{"key":"per_page","value":"20"}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": true,\n  \"meta\": {\n    \"total\": 2,\n    \"last_page\": 1,\n    \"current_page\": 1,\n    \"per_page\": 20\n  },\n  \"data\": [\n    {\n      \"id\": 1001,\n      \"name\": \"Tallinna Reaalkool\",\n      \"ehis_id\": 123,\n      \"logo_img_fn\": \"1001_logo.png\",\n      \"has_agreement\": true\n    },\n    {\n      \"id\": 1002,\n      \"name\": \"Gustav Adolfi Gümnaasium\",\n      \"ehis_id\": 124,\n      \"logo_img_fn\": null,\n      \"has_agreement\": false\n    }\n  ]\n}"},{"id":"74ddadbc-931c-402b-9af6-f0b504af61a6","name":"403 Forbidden — missing `schools:read` scope","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/users/me/schools"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"Not allowed to access this url.\" }\n  ]\n}"}],"_postman_id":"93faea22-7a16-45fc-99f2-7d2d441178eb"},{"name":"GET /api/v3/schools/{school_id}/roles","id":"58dfdb2e-9f24-42ec-9ea6-04c5a66f3ee8","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools//roles?page=1&per_page=20","description":"<p>Lists the authenticated user's active roles inside one school. <code>class</code> is <code>null</code> for roles without a class context (e.g. teacher roles).</p>\n<p><strong>Required scope:</strong> <code>schools:read</code></p>\n<p><strong>Partner-agreement middleware applied</strong> — the partner must have an active agreement for <code>{school_id}</code>.</p>\n","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}},"urlObject":{"path":["api","v3","schools","","roles"],"host":["https://api-v2.ekool.dev"],"query":[{"key":"page","value":"1"},{"key":"per_page","value":"20"}],"variable":[]}},"response":[{"id":"86dd07d4-0de4-46cc-8e25-4c7bc0d51aa9","name":"200 OK — Roles in the school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/roles"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": true,\n  \"meta\": {\n    \"total\": 2,\n    \"last_page\": 1,\n    \"current_page\": 1,\n    \"per_page\": 20\n  },\n  \"data\": [\n    {\n      \"id\": 7001,\n      \"type\": \"Teacher\",\n      \"class\": null\n    },\n    {\n      \"id\": 7002,\n      \"type\": \"Formmaster\",\n      \"class\": {\n        \"class_level_id\": 5,\n        \"parallel_identifier_id\": 2,\n        \"name\": \"5B\"\n      }\n    }\n  ]\n}"},{"id":"3ff8b767-98e5-4f84-bd0d-1785da06a63d","name":"403 Forbidden — no partner agreement for this school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/roles"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"no_school_agreement\" }\n  ]\n}"}],"_postman_id":"58dfdb2e-9f24-42ec-9ea6-04c5a66f3ee8"},{"name":"GET /api/v3/schools/{school_id}/journals","id":"7098de99-ef47-406d-9ad6-2d286c7d396c","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools//journals?context_role_id=&page=1&per_page=20","description":"<p>Lists the journals visible for <code>context_role_id</code> inside the specified school.</p>\n<ul>\n<li>Teacher roles: journals the teacher is actively teaching in the current study year.</li>\n<li>Student roles: journals the student is actively enrolled in (or was transferred into/out of).</li>\n<li>Any other role type: empty list with <code>meta.total = 0</code>.</li>\n</ul>\n<p><strong>Required scope:</strong> <code>journals:read</code></p>\n<p><strong>Partner-agreement middleware applied.</strong></p>\n<p><strong>Context role validation:</strong> the <code>context_role_id</code> must belong to the authenticated user <strong>and</strong> to the school in the URL. Mismatches respond with <code>403 invalid_context_role</code>.</p>\n","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}},"urlObject":{"path":["api","v3","schools","","journals"],"host":["https://api-v2.ekool.dev"],"query":[{"description":{"content":"<p><strong>Required.</strong> Role ID from <code>/schools/{school_id}/roles</code> to evaluate the listing in. Must belong to the authenticated user and to <code>{school_id}</code>.</p>\n","type":"text/plain"},"key":"context_role_id","value":""},{"key":"page","value":"1"},{"key":"per_page","value":"20"}],"variable":[]}},"response":[{"id":"0a0984fe-f3a5-45ac-91e4-f685d3c9fb1c","name":"200 OK — Journals for a teacher","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": true,\n  \"meta\": {\n    \"total\": 2,\n    \"last_page\": 1,\n    \"current_page\": 1,\n    \"per_page\": 20\n  },\n  \"data\": [\n    {\n      \"id\": 5001,\n      \"name\": \"5B matemaatika\",\n      \"study_year_id\": 2025,\n      \"course\": {\n        \"id\": 901,\n        \"name\": \"Matemaatika 5. klass\",\n        \"subject_name\": \"Matemaatika\",\n        \"class_level\": \"5\"\n      }\n    },\n    {\n      \"id\": 5002,\n      \"name\": \"6A eesti keel\",\n      \"study_year_id\": 2025,\n      \"course\": {\n        \"id\": 902,\n        \"name\": \"Eesti keel 6. klass\",\n        \"subject_name\": \"Eesti keel\",\n        \"class_level\": \"6\"\n      }\n    }\n  ]\n}"},{"id":"e7ef8135-42cd-4aae-b608-cff1fa7bed16","name":"400 Bad Request — missing `context_role_id`","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 400, \"message\": \"context_role_id is required\" }\n  ]\n}"},{"id":"eb957d8d-94b6-4f01-b90d-5a45a56fd0f4","name":"403 Forbidden — `context_role_id` not owned by user or not in this school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"invalid_context_role\" }\n  ]\n}"},{"id":"7e103876-3061-4e8b-97bb-c1bf0fe44f6c","name":"403 Forbidden — no partner agreement for this school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"no_school_agreement\" }\n  ]\n}"}],"_postman_id":"7098de99-ef47-406d-9ad6-2d286c7d396c"},{"name":"GET /api/v3/schools/{school_id}/journals/{journal_id}/students","id":"8d2a1577-369e-4269-941a-872d63afd7b2","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools//journals//students?context_role_id=&page=1&per_page=20","description":"<p>Lists all students currently associated with the given journal.</p>\n<p><strong>Required scope:</strong> <code>journal.students:read</code></p>\n<p><strong>Partner-agreement middleware applied</strong> (via the parent <code>/schools/{school_id}/</code> prefix).</p>\n<p><strong>Context role validation:</strong> the supplied <code>context_role_id</code> must belong to the authenticated user and to the school in the URL. Mismatches respond with <code>403 invalid_context_role</code>.</p>\n<p><strong>Journal validation:</strong> <code>{journal_id}</code> must belong to <code>{school_id}</code>. Otherwise the API responds with <code>400 journal_does_not_belong_to_school</code>.</p>\n","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}},"urlObject":{"path":["api","v3","schools","","journals","","students"],"host":["https://api-v2.ekool.dev"],"query":[{"description":{"content":"<p><strong>Required.</strong> Role ID the call is made in the context of.</p>\n","type":"text/plain"},"key":"context_role_id","value":""},{"key":"page","value":"1"},{"key":"per_page","value":"20"}],"variable":[]}},"response":[{"id":"b84309d3-250e-451f-bea1-971aa742dd10","name":"200 OK — Students of the journal","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals/5001/students"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": true,\n  \"meta\": {\n    \"total\": 3,\n    \"last_page\": 1,\n    \"current_page\": 1,\n    \"per_page\": 20\n  },\n  \"data\": [\n    {\n      \"id\": 8001,\n      \"first_name\": \"Jaan\",\n      \"last_name\": \"Saar\",\n      \"class\": {\n        \"class_level_id\": 5,\n        \"parallel_identifier_id\": 2,\n        \"name\": \"5B\"\n      }\n    },\n    {\n      \"id\": 8002,\n      \"first_name\": \"Kadri\",\n      \"last_name\": \"Lepp\",\n      \"class\": {\n        \"class_level_id\": 5,\n        \"parallel_identifier_id\": 2,\n        \"name\": \"5B\"\n      }\n    },\n    {\n      \"id\": 8003,\n      \"first_name\": \"Mart\",\n      \"last_name\": \"Kask\",\n      \"class\": {\n        \"class_level_id\": 5,\n        \"parallel_identifier_id\": null,\n        \"name\": \"5\"\n      }\n    }\n  ]\n}"},{"id":"c4eb2e77-4810-41fe-9243-dd32cffd84f0","name":"400 Bad Request — missing `context_role_id`","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals/5001/students"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 400, \"message\": \"context_role_id is required\" }\n  ]\n}"},{"id":"c896ddb8-ac3f-45d7-81db-0705d354d9c6","name":"400 Bad Request — journal does not belong to the requested school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals/5099/students"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 400, \"message\": \"journal_does_not_belong_to_school\" }\n  ]\n}"},{"id":"39081a78-71d9-4887-ba2f-ac843a076cd8","name":"403 Forbidden — `context_role_id` not owned by user or not in this school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals/5001/students"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"invalid_context_role\" }\n  ]\n}"},{"id":"e7795b62-937e-429b-a9be-9a5bed5fb38a","name":"403 Forbidden — no partner agreement for this school","originalRequest":{"method":"GET","header":[{"key":"Accept","value":"application/json"}],"url":"https://api-v2.ekool.dev/api/v3/schools/1001/journals/5001/students"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"status\": false,\n  \"errors\": [\n    { \"code\": 403, \"message\": \"no_school_agreement\" }\n  ]\n}"}],"_postman_id":"8d2a1577-369e-4269-941a-872d63afd7b2"}],"id":"82291c10-c79e-4c2b-92c2-587c6f667c0d","description":"<p>All v3 endpoints require a valid bearer token (<code>Authorization: Bearer </code>) and are authorized by:</p>\n<ul>\n<li><p><strong>scope.resolve</strong> — token's scopes must include the scope listed for each endpoint.</p>\n</li>\n<li><p><strong>partner-agreement</strong> — single-school endpoints (<code>/schools/{school_id}/...</code>) require an active agreement between the partner and the school. The agreement check is bypassed for first-party (internal) tokens with empty scopes.</p>\n</li>\n</ul>\n<p>For the full set of error variants per endpoint, see the dedicated <code>v3_api.postman_collection.json</code> collection.</p>\n","_postman_id":"82291c10-c79e-4c2b-92c2-587c6f667c0d","auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]},"isInherited":true,"source":{"_postman_id":"d500d161-614f-4c83-9f85-63f9af5ab021","id":"d500d161-614f-4c83-9f85-63f9af5ab021","name":"eKool public API (v3)","type":"collection"}}}],"auth":{"type":"bearer","bearer":{"basicConfig":[{"key":"token","value":""}]}},"variable":[{"key":"login_url","value":"https://login.ekool.dev","description":"OAuth host (staging: https://login.ekool.dev, production: https://login.ekool.eu)"},{"key":"api_url","value":"https://api-v2.ekool.dev","description":"API host (staging: https://api-v2.ekool.dev, production: https://api-v2.ekool.eu)"},{"key":"client_id","value":"","description":"OAuth client identifier issued by eKool"},{"key":"redirect_uri","value":"https://my.application.com/oauth2/callback","description":"One of the redirect URIs registered for the client"},{"key":"scope","value":"user:read schools:read journals:read journal.students:read","description":"Space-separated list of scopes to request"},{"key":"code_verifier","value":"","description":"Auto-populated by the 'Generate PKCE pair' request"},{"key":"code_challenge","value":"","description":"Auto-populated by the 'Generate PKCE pair' request (S256 of code_verifier)"},{"key":"code","value":"","description":"Authorization code returned to the redirect_uri after user consent"},{"key":"access_token","value":"","description":"Bearer token used in the Authorization header for every API call"},{"key":"refresh_token","value":"","description":"Refresh token used to obtain a new access token without re-prompting the user"},{"key":"school_id","value":"","description":"Institution ID"},{"key":"journal_id","value":"","description":"Journal ID inside the school"},{"key":"context_role_id","value":"","description":"Role ID the call is made in the context of (Teacher or Student)"},{"key":"page","value":"1"},{"key":"per_page","value":"20"}]}