# SPK School Extension API v1

Canonical human guide: https://spk.max12311.com/developers/api
Canonical machine contract: https://spk.max12311.com/api/v1/openapi.json
API base URL: https://spk.max12311.com/api/v1

This document is intentionally available as Markdown so developers and AI agents can understand the API without parsing the application UI.

## Purpose and safety model

The API lets external systems read a minimized subset of school data without receiving a Supabase service-role key or direct database access.

- Long-lived keys begin with `spk_live_` and are stored server-side as SHA-256 hashes.
- Every client has explicit scopes, optional section restrictions, status, expiration, allowed origins, and a per-minute rate limit.
- Data endpoints return `Cache-Control: private, no-store` and a request ID for audit correlation.
- The API intentionally excludes national IDs, password hashes, guardian contacts, medical notes, encryption keys, and other secrets.
- Treat every successful response containing student data as protected personal data.

## Authentication

An administrator creates an API client at **Admin → Developers and extensions**. The secret is shown once. Call the API from your backend, never from public browser JavaScript or a mobile bundle.

```http
Authorization: Bearer spk_live_<prefix>_<secret>
Accept: application/json
```

CORS origin restrictions are defense-in-depth only; they do not make a long-lived API key safe in a public client.

## Quick start

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $SPK_API_KEY" \
  "https://spk.max12311.com/api/v1/students?limit=50"
```

Successful responses place the result under `data`. Paginated responses also have `meta.next_cursor`:

```json
{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000001",
      "student_code": "40001",
      "status": "studying"
    }
  ],
  "meta": { "next_cursor": "opaque-value-or-null" }
}
```

Pass `meta.next_cursor` unchanged as the next request's `cursor`. Do not decode, edit, or synthesize cursors.

## Scopes

- `students:read` — minimized student identity and current status
- `enrollments:read` — enrollment history
- `conduct:read` — conduct records and points
- `attendance:read` — attendance records
- `sections:read` — sections and current rosters

A key must have both the required scope and access to the relevant section. Out-of-scope student IDs may return `404` to avoid revealing whether the student exists.

## Endpoints

### GET /students

Scope: `students:read`

Query parameters:
- `limit`: 1–100; default 50
- `cursor`: opaque cursor from the previous response
- `section_id`: UUID filter; must be inside the client's section scope
- `status`: `studying | on_leave | graduated | resigned | transferred`
- `updated_since`: ISO 8601 timestamp

### GET /students/{id}

Scope: `students:read`. Returns the minimized student profile.

### GET /students/{id}/status

Scope: `students:read`. Returns current education status and entered year.

### GET /students/{id}/enrollments

Scope: `enrollments:read`. Returns enrollment history restricted to allowed sections.

### GET /students/{id}/conduct

Scope: `conduct:read`.

Query parameters: `limit` (1–100, default 50), `cursor`.

### GET /students/{id}/attendance

Scope: `attendance:read`.

Query parameters:
- `from`, `to`: dates in `YYYY-MM-DD`
- default range: previous 30 days
- maximum range: 366 days
- maximum result set: 1,000 events

Each item has `id` for the attendance record and `event_id` for the attendance event.

### GET /sections

Scope: `sections:read`.

Query parameters: `limit` (1–100, default 50), `cursor`, `academic_year_id`.

### GET /sections/{id}/students

Scope: `sections:read`. Returns section metadata and the active roster.

### POST /extensions/exchange

This endpoint uses a sender-bound one-time launch code rather than an API key. The code arrives in the URL fragment, expires after two minutes, and can be consumed once. Keep the extension exchange secret on the extension backend only.

```bash
curl --fail-with-body -X POST \
  -H "Content-Type: application/json" \
  -d '{"token":"<launch-token>","extension_slug":"library-app","client_secret":"<exchange-secret>"}' \
  "https://spk.max12311.com/api/v1/extensions/exchange"
```

Create a short-lived session in the extension after exchange; do not reuse the launch payload as permanent authorization.

## Error contract

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "ต้องมี scope students:read",
    "request_id": "00000000-0000-4000-8000-000000000002"
  }
}
```

Important status codes:
- `400`: invalid parameter, cursor, date, or request body
- `401`: key is invalid, expired, suspended, or revoked
- `403`: scope, origin, or section is not allowed
- `404`: resource is absent or outside the allowed scope
- `413`: request body is too large
- `429`: per-client rate limit exceeded
- `500`: internal failure
- `503`: API platform or rate limiter temporarily unavailable

Retry only network errors, `429`, and `503`, using exponential backoff with jitter. Do not retry `401` or `403` until credentials or permissions change.

## Signed webhooks

Supported events:
- `student.updated` → `students:read`
- `enrollment.changed` → `enrollments:read`
- `conduct.created` → `conduct:read`
- `attendance.recorded` → `attendance:read`

Headers:
- `X-SPK-Delivery`: stable idempotency key
- `X-SPK-Event`: event name
- `X-SPK-Signature`: `t=<unix-seconds>,v1=<hex-hmac-sha256>`

Verify HMAC-SHA256 over `<timestamp>.<exact raw request body bytes>` with constant-time comparison. Reject timestamps differing by more than five minutes. Store processed delivery IDs and skip duplicates. The sender does not follow redirects, waits at most eight seconds, and retries transient failures.

Webhook payloads contain reference IDs rather than full student records. Fetch authorized fields through the API after receiving an event.

## Versioning

- Breaking changes use a new path such as `/api/v2`.
- Additive optional fields and new endpoints may be added to v1.
- Before v1 sunset, the operator will publish at least 90 days' notice, a sunset date, and a migration guide.
- Clients must ignore unknown JSON fields and must not depend on object key order.

## AI-agent instructions

1. Use the OpenAPI document as the canonical operation/schema contract.
2. Use this Markdown guide for security, retry, webhook, and versioning semantics.
3. Never request, print, log, or embed a real API key in generated code or prompts.
4. Generate examples with synthetic IDs and data only.
5. Prefer backend-to-backend calls and the minimum required scopes.
