Getting started #
Call platform4 over HTTPS with a Cognito client-credentials access token. Choose the Core API for Alpha School domain data or a dedicated host for a standards integration.
| Surface | Base URL | When to use |
|---|---|---|
| Core API | https://alpha.platform4.alphaschool.dev |
Alpha School domain APIs under /<module-kebab>/v1/ on one base URL |
| Standards integration | https://<module>.platform4.alphaschool.dev |
Standard-native root paths for OneRoster, Ed-Fi, Caliper, and related APIs |
The alpha hostname is a routing name for the Core API, not a release-quality label.
Core API quickstart #
- Set credential env vars (never paste secrets into docs or tickets).
- Mint a token against
$TOKEN_URL. - Call a Core API route with
Authorization: Bearer $TOKEN.
Production token endpoint (set as TOKEN_URL):
https://platform4-prod-182821611732.auth.us-east-1.amazoncognito.com/oauth2/token
1. Mint a client-credentials token #
Cognito issues tokens for scope platform4/api. Credentials are issued when a tenant is provisioned; until self-serve issuance lands, contact your platform4 administrator to obtain them.
Export before running examples:
export TOKEN_URL="https://platform4-prod-182821611732.auth.us-east-1.amazoncognito.com/oauth2/token"
export CLIENT_ID="…"
export CLIENT_SECRET="…"
curl #
TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-d 'grant_type=client_credentials&scope=platform4/api' \
"$TOKEN_URL" | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
echo "$TOKEN"
TypeScript #
const tokenUrl = process.env.TOKEN_URL!;
const clientId = process.env.CLIENT_ID!;
const clientSecret = process.env.CLIENT_SECRET!;
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
const body = new URLSearchParams({
grant_type: "client_credentials",
scope: "platform4/api",
});
const res = await fetch(tokenUrl, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body,
});
const { access_token } = await res.json();
Python #
import base64, os, urllib.parse, urllib.request, json
token_url = os.environ["TOKEN_URL"]
client_id = os.environ["CLIENT_ID"]
client_secret = os.environ["CLIENT_SECRET"]
basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
data = urllib.parse.urlencode({
"grant_type": "client_credentials",
"scope": "platform4/api",
}).encode()
req = urllib.request.Request(
token_url,
data=data,
headers={
"Authorization": f"Basic {basic}",
"Content-Type": "application/x-www-form-urlencoded",
},
)
with urllib.request.urlopen(req) as resp:
access_token = json.load(resp)["access_token"]
2. Base URLs #
Core API host #
Core API modules share one base URL; the module is a path segment:
https://alpha.platform4.alphaschool.dev/<module-kebab>/v1/…
Example:
https://alpha.platform4.alphaschool.dev/people-and-orgs/v1/person
Core API modules on this host are people-and-orgs, applications, content, curriculum, events, results, and analytics. Per-module hosts for these modules remain available as aliases.
Standards / module hosts #
Each module listens on its own host (HTTPS :443 via the shared ALB):
https://<module>.platform4.alphaschool.dev
Examples:
https://oneroster.platform4.alphaschool.devhttps://edfi.platform4.alphaschool.devhttps://clr.platform4.alphaschool.devhttps://peopleandorgs.platform4.alphaschool.dev(hostname is lowercase)
Paths are standards-native at the root of that host, for example /ims/oneroster/, /ims/clr/v2p0/, /ed-fi/, and /health. Do not add Core API path prefixes to standards hosts.
3. First request #
Health checks are unauthenticated. Data routes need Authorization: Bearer <token>.
curl #
# Unauthenticated health (module host)
curl -fsS https://oneroster.platform4.alphaschool.dev/health
# Authenticated Core API call
curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://alpha.platform4.alphaschool.dev/people-and-orgs/v1/person?limit=1"
# Authenticated list (OneRoster orgs on its standards host)
curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://oneroster.platform4.alphaschool.dev/ims/oneroster/rostering/v1p2/orgs?limit=1"
TypeScript #
const health = await fetch("https://oneroster.platform4.alphaschool.dev/health");
console.log(await health.json());
const people = await fetch(
"https://alpha.platform4.alphaschool.dev/people-and-orgs/v1/person?limit=1",
{ headers: { Authorization: `Bearer ${access_token}` } },
);
console.log(await people.json());
const orgs = await fetch(
"https://oneroster.platform4.alphaschool.dev/ims/oneroster/rostering/v1p2/orgs?limit=1",
{ headers: { Authorization: `Bearer ${access_token}` } },
);
console.log(await orgs.json());
Python #
import urllib.request, json
print(json.load(urllib.request.urlopen(
"https://oneroster.platform4.alphaschool.dev/health"
)))
alpha = urllib.request.Request(
"https://alpha.platform4.alphaschool.dev/people-and-orgs/v1/person?limit=1",
headers={"Authorization": f"Bearer {access_token}"},
)
print(json.load(urllib.request.urlopen(alpha)))
req = urllib.request.Request(
"https://oneroster.platform4.alphaschool.dev/ims/oneroster/rostering/v1p2/orgs?limit=1",
headers={"Authorization": f"Bearer {access_token}"},
)
print(json.load(urllib.request.urlopen(req)))
4. Auth header #
Send the access token on every protected request:
Authorization: Bearer <access_token>
Tokens carry tenant and role claims. Missing, malformed, invalid, or expired tokens return 401 with the error envelope below. A verified token that lacks the operation's required role or permission returns 403. Operation-level gates appear as Requires role on each module's API reference page.
5. Roles and permissions #
Your token's roles determine which operations it can use. The platform-issued credential carries the system role and can read most modules. Content, Curriculum, QTI, CASE, and Caliper require a tenant-scoped credential with module-specific roles.
| API group | Works with the default system token |
|---|---|
| People & Orgs, Results, Events, Applications, Analytics, Platform, OneRoster, Ed-Fi, NWEA MAP, CLR, Commerce | Yes |
| Content, Curriculum, QTI, CASE, Caliper | No. These APIs return 403 without a tenant-scoped role. |
To obtain a role-bearing credential, register the integrating app through Applications and email platform4@alphaschool.dev with the tenant, API, and read or write access required. The operation reference identifies each required role.
6. Pagination #
Standards / OneRoster (limit + offset) #
Standards hosts keep their native collection shape. OneRoster list endpoints accept limit / offset (or other standard-specific paging query params). Prefer small limit values while exploring.
curl #
curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://oneroster.platform4.alphaschool.dev/ims/oneroster/rostering/v1p2/orgs?limit=25&offset=0"
TypeScript #
const url = new URL(
"https://oneroster.platform4.alphaschool.dev/ims/oneroster/rostering/v1p2/orgs",
);
url.searchParams.set("limit", "25");
url.searchParams.set("offset", "0");
const page = await fetch(url, {
headers: { Authorization: `Bearer ${access_token}` },
});
Python #
from urllib.parse import urlencode
qs = urlencode({"limit": 25, "offset": 0})
req = urllib.request.Request(
f"https://oneroster.platform4.alphaschool.dev/ims/oneroster/rostering/v1p2/orgs?{qs}",
headers={"Authorization": f"Bearer {access_token}"},
)
Response bodies follow each standard's collection shape (for example OneRoster orgs arrays with offset / limit / total metadata where declared). See the module API reference for exact schemas.
Core API cursor envelope ({data, nextCursor}) #
Core API list reads return a flat cursor page: {data, nextCursor}. The nextCursor value is a string or null. Pass the previous page's nextCursor as the next request's cursor query parameter and stop when nextCursor is null.
let cursor: string | null = null;
do {
const url = new URL(
"https://alpha.platform4.alphaschool.dev/people-and-orgs/v1/person",
);
url.searchParams.set("limit", "25");
if (cursor) url.searchParams.set("cursor", cursor);
const page = await fetch(url, {
headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json()) as { data: unknown[]; nextCursor: string | null };
// …use page.data…
cursor = page.nextCursor;
} while (cursor);
7. Optimistic concurrency (ETag and If-Match) #
Operations that advertise optimistic concurrency in OpenAPI enforce RFC 9110 validators:
- Successful GET, POST, PUT, and PATCH responses include an ETag. This strong, quoted entity tag identifies the returned resource version.
- Per-resource PUT, PATCH, and DELETE requests require
If-Match. Send the ETag from the latest successful read or write. A missing header returns428 Precondition Required. A stale ETag returns412 Precondition Failed; fetch the current resource before retrying. A successful write returns the new ETag. - Follow the OpenAPI contract. Send
If-Matchonly when the operation declares it as a required parameter.
Applications resources that currently use this behavior include publisher, application, appCredential, authClient, rendererCapability, and webhookEndpoint.
8. Error envelope #
Core API routes return RFC 7807 application/problem+json with {type, title, status, detail} and optional errors[] on validation failures:
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"detail": "Missing Authorization header"
}
Representative Core API cases:
| Status | When | Typical detail |
|---|---|---|
| 401 missing Authorization | No Authorization header |
Missing Authorization header |
| 401 invalid token | Malformed, expired, or unverifiable bearer | Invalid or expired bearer token (or malformed-header detail) |
| 403 role / authz | Principal role lacks the required permission | Forbidden detail naming the denied action/role gate |
| 400 validation | Request body/query failed schema validation | Request validation failed (plus optional errors: [{field, message}]) |
| 409 conflict | Uniqueness / FK / state conflict | Conflict detail (constraint name when available) |
| 412 precondition failed | If-Match was sent but does not match the current resource version |
Precondition Failed detail (refetch ETag and retry) |
| 428 precondition | Write requires If-Match (or required Idempotency-Key) and it was omitted |
Precondition Required detail telling the client which header to send |
Standards-native exception: Some standards hosts, including OneRoster and CASE, project failures to their interoperability envelope. This is commonly a flat imsx_StatusInfo object with imsx_codeMajor, imsx_severity, imsx_description, and imsx_codeMinor, served as application/json instead of application/problem+json. Use the published OpenAPI response content types for the host you call.
Unknown hosts receive a fixed 404 response. Only registered module hosts, the Core API host with a matching /<module-kebab>/v1/ path, and this documentation host are routed.