A partner API for school data.
Read schools, staff, invoices and attendance from SekolahOS through versioned, signed REST.
Domains open to partners
Every API key is scoped per domain and per school. A scope you did not request is never readable.
Roster & staff
Foundation, schools and active staff with NIP and employment status. NIK only for keys with roster.pii.
Finance
Invoices with total, paid and balance due. Money is always a 2dp string with its currency.
Attendance
Daily record per student with six fixed statuses. Gate photos are never exported.
Event
Signed webhooks or polling for new students, settled payments and key rotation. Applies to every key.
Authentication you can rotate
A key-id and secret pair per foundation, HMAC-SHA256 signed per request. Rotate with no downtime.
Safe to retry
Every mutating endpoint accepts an Idempotency-Key. Replay the same request and you get the same response, not a double effect.
Webhooks, with a polling net
Signed webhooks for domain events with five retries — and an events endpoint for when your receiver was down.
Connect an AI assistant to your school data.
Claude Desktop, Claude Code, Cursor and other MCP clients can read your school data through the Partner API. Read-only, limited to the key's scopes, and every call is logged.
Issue key
The foundation admin issues a key in the console, under Partners & API keys. Start with an ak_test_ key with read scopes only.
Install the bridge
Download one Python file with no dependencies. MCP clients cannot make an HMAC signature, so this bridge signs every request on your computer. The secret is never sent.
Add it to your client
Paste the configuration for your client, restart it, then ask: which schools can you read? The full guide is in the MCP section of the documentation.
ak_test_ always hit synthetic data and never send notifications to parents. Use it for all development.
Authentication
Every request carries a key-id header and an HMAC-SHA256 signature over timestamp + method + path + body. The secret is never sent. Requests with a timestamp older than 300 seconds are rejected.
X-EduCore-Key-Id: ak_live_7Qp2R9 X-EduCore-Signature: t=1789632650,v1=9f... Idempotency-Key: 4f0a1b2c-...
| Rule | Terms |
|---|---|
| Scope | Set by the foundation, per domain and per school. Cannot be widened by the partner. |
| Rotation | At most two active keys for 30 days. The old key becomes read-only on day 23. |
| IP allow-list | Optional per key. Requests from an address outside the list are refused with IP_NOT_ALLOWED. |
Versioning & deprecation
The version lives in the path, not a header. Breaking changes always become a new /api/v2/; adding an optional field is not a breaking change. An old version lives for 12 months after its replacement ships, and sends a Sunset header from day one.
Conventions
| Rule | Shape |
|---|---|
| Pagination | ?cursor=&limit= → {results, next_cursor} |
| Money | {"amount": "1500000.00", "currency": "IDR"} |
| Time | ISO 8601 UTC — 2026-09-17T02:14:00Z |
| Idempotency | Idempotency-Key — kept for 7 days |
Money is a string with the currency at the same object level. Never sum across currencies: aggregate endpoints return an array of totals per currency, not a single number.
Error
All errors are RFC 9457 application/problem+json. Branch your logic on code — a stable SCREAMING_SNAKE string — not on the detail prose.
{
"type": "/errors/scope-denied",
"title": "Scope not granted",
"status": 403,
"detail": "Key lacks roster.pii",
"instance": "/api/v1/partner/_",
"code": "SCOPE_DENIED"
}
| Status | Code |
|---|---|
| 401 | SIGNATURE_INVALID |
| 403 | SCOPE_DENIED |
| 400 | VALIDATION_ERROR |
| 403 | IP_NOT_ALLOWED |
| 404 | NOT_FOUND |
| 409 | IDEMPOTENCY_MISMATCH |
| 429 | RATE_LIMITED |
Webhooks & polling
Webhooks are the primary path. Register one HTTPS endpoint per environment, accept a signed POST, and answer 2xx within 5 seconds. Failures retry five times up to 6 hours; after that the event remains available by polling.
| Event | Triggered when |
|---|---|
roster.student.enrolled | A new active student at a school in the key's scope |
finance.payment.settled | A virtual-account or QRIS payment settled and was allocated |
integration.key.rotated | A key is rotated — 30 days before the old one dies |
Endpoint
| Method & path | Description |
|---|---|
GET /partner/foundationsroster.read | Foundations that granted you access |
GET /partner/schoolsroster.read | Schools within the key's scope |
GET /partner/staff?school_idroster.read | Active staff with NIP and status, cursor-paginated; NIK only with roster.pii |
GET /partner/invoices?statusfinance.read | Invoices and outstanding balance |
GET /partner/attendance/daily?dateattendance.read | Daily attendance per student |
GET /partner/events?since=cursor— | Event stream — a webhook alternative |
POST /partner/mcpper tool | Read-only MCP server for AI assistants; see the MCP section |
POST /partner/webhooks— | Register a webhook receiver |
MCP
The POST /partner/mcp endpoint is a Model Context Protocol server (HTTP, sessionless JSON mode) behind the same authentication and rate limit as the other endpoints. It holds read-only tools; a key only sees the tools its scopes allow, and the rows returned are exactly those of REST: school limit, foundation limit, NIK only with roster.pii.
Before you start
- Python 3.8 or newer on the computer that runs the AI client (
python3 --version; on Windows usepython). - Download educore_mcp_bridge.py and keep it in a permanent folder, for example
~/sekolahos/. - Check the file before running it:
shasum -a 256 educore_mcp_bridge.pymust print177f51df1cb11dea603f32a03809fca400a4f0e5e5dfbabf378f50d3f3a0c080. - Ask the foundation admin to issue a key. Note the key-id and secret; the secret is shown only once.
- Keep the secret in a password manager or the operating system keychain, not in a synced file or in git. Ask for a key with only the scopes you need and restrict it with an IP list.
| Variables | Text |
|---|---|
EDUCORE_API_BASE | https://schoolday.live/api/v1/ |
EDUCORE_KEY_ID | The key-id, for example ak_test_7Qp2R9 |
EDUCORE_KEY_SECRET | The secret shown when the key was issued |
Guide by client
Claude Desktop
- Open Settings → Developer → Edit Config; the
claude_desktop_config.jsonfile opens. - Add the following server, replacing the path and credentials:
{
"mcpServers": {
"sekolahos": {
"command": "python3",
"args": ["/Users/anda/sekolahos/educore_mcp_bridge.py"],
"env": {
"EDUCORE_API_BASE": "https://schoolday.live/api/v1/",
"EDUCORE_KEY_ID": "ak_test_...",
"EDUCORE_KEY_SECRET": "..."
}
}
}
}
- Quit Claude Desktop completely and open it again. A tools icon appears in the chat box with the sekolahos server.
Claude Code
One command in the terminal:
claude mcp add sekolahos \ --env EDUCORE_API_BASE=https://schoolday.live/api/v1/ \ --env EDUCORE_KEY_ID=ak_test_... \ --env EDUCORE_KEY_SECRET=... \ -- python3 ~/sekolahos/educore_mcp_bridge.py
Check with claude mcp list, or type /mcp inside a session.
Cursor
Create ~/.cursor/mcp.json (or .cursor/mcp.json in the project) with the same content as Claude Desktop above, then enable the server under Settings → MCP.
Other MCP clients
Any client that supports local (stdio) MCP servers only needs the command python3 /path/educore_mcp_bridge.py and the three environment variables above. To test without an AI client, use MCP Inspector:
EDUCORE_API_BASE=https://schoolday.live/api/v1/ \ EDUCORE_KEY_ID=ak_test_... EDUCORE_KEY_SECRET=... \ npx @modelcontextprotocol/inspector python3 educore_mcp_bridge.py
Quick test from the terminal
Send one JSON-RPC message to the bridge; the reply lists the tools your key may use:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 educore_mcp_bridge.py
Available tools
| Tool | Scope | Purpose |
|---|---|---|
list_schools | roster.read | Schools within the key's scope |
list_classes | roster.read | Class groups with grade level and academic year. Filter: school_id |
list_students | roster.read roster.pii | Students with NIS, status and current class; NISN only with roster.pii; NIK, date of birth, address and guardian contact are never sent. Filter: school_id, class_group_id, status, q (part of the name) |
list_staff | roster.read roster.pii | Active staff with NIP and status; NIK only with roster.pii. Filter: school_id |
list_invoices | finance.read | Invoices and balance due. Filter: status, school_id, student_id, period, due_from, due_to |
get_daily_attendance | attendance.read | Student attendance for one day. Required: date (YYYY-MM-DD) |
get_attendance_range | attendance.read | Student attendance over a date range (at most 31 days). Required: date_from, date_to. Filter: school_id, student_id, status |
Every tool accepts limit (at most 200) and before_id. The answer carries next_before_id; pass its value to get the next page. There are no write tools; webhook registration stays REST-only. One key may call tools at most 120 times per minute (within the 600 request limit).
If it does not work
| Symptom | Cause and fix |
|---|---|
| The server does not appear in the client | The file path is wrong or Python is not on PATH. Run the command from a terminal to see its error message. |
SIGNATURE_INVALID | Wrong secret, the key was revoked or has expired, or the computer clock is off by more than 300 seconds. |
IP_NOT_ALLOWED | The key uses an IP allow-list and your computer's address is not on it. |
SCOPE_DENIED | The key has no scope for that tool. Ask the foundation admin to issue a new key with the right scopes. |
RATE_LIMITED | The limit of 600 requests per minute per key is shared by REST and MCP. |
For resellers and integrators
Every API key works for exactly one foundation; no key reaches several foundations. If you integrate many schools, ask each foundation's admin to issue you a key in the console, under Partners & API keys, with the narrowest scope possible. Keep each secret in your own vault, one per foundation.
To help with configuration inside the console, the foundation admin creates an ordinary staff login with only the roles needed. A reseller gets no cross-foundation access.
Rate limits
| Limit | Value |
|---|---|
| Requests per key | 600 / minute |
| Max rows per cursor page | 200 |
| Fastest allowed polling interval | 30 seconds |
Going over a limit returns 429 RATE_LIMITED with a Retry-After header.