# FablePool Public API — Conventions This document complements [`api/openapi.yaml`](../api/openapi.yaml) with the normative conventions every endpoint follows. It is the reference for both API implementers (DRF views in milestone #3) and external consumers. ## 1. Versioning * The API is versioned in the URL path: `/api/v1/...`. * Within `v1`, changes are **additive only**: new optional fields, new endpoints, new enum values on *output* (consumers must tolerate unknown enum values). Breaking changes require `v2`. * Webhook payloads carry `api_version: "v1"` so consumers can route handlers. ## 2. Authentication | Scheme | Header | Use case | Lifetime | | --- | --- | --- | --- | | Bearer JWT | `Authorization: Bearer ` | Browsers / first-party apps | 60 min access, refresh via session | | API key | `X-API-Key: ` | Server-to-server integrations | Until revoked; scoped per key | Anonymous access is permitted for all public-read endpoints (published problems, courses, tags, topics, search, OER export). Anonymous callers get the strictest rate-limit bucket. ## 3. Permission model Each operation in the OpenAPI document carries an `x-permissions` extension: ```yaml x-permissions: authentication: none | optional | required roles: [reviewer, moderator] # any-of; empty list = any authenticated user object: "author or moderator" # object-level rule, enforced server-side ``` Role semantics (cumulative — every account is at least `learner`): | Role | Grants | | --- | --- | | `learner` | Solve, enroll, discuss, bookmark, report | | `contributor` | Create/fork problems & courses, submit versions | | `reviewer` | Claim reviews, comment, decide accept/reject/request-changes | | `moderator` | Resolve reports, lock threads, rollback, soft-delete any content | | `admin` | Role management, tag governance, system configuration | Object-level rules are evaluated **after** role checks and are always audit-logged when they grant elevated access (e.g. a moderator reading another user's draft). ## 4. Pagination Cursor-based, stable under concurrent inserts: * Request: `?cursor=&page_size=20` (`page_size` ∈ [1, 100]). * Response envelope: ```json { "results": [ ... ], "next_cursor": "eyJpZCI6...", "has_more": true, "total_estimate": 1342 } ``` * `next_cursor` is opaque; never construct or parse it. It is signed and expires after 24 h (expired cursors return `400` with type `https://fablepool.org/errors/expired-cursor`). * `total_estimate` is best-effort and may be `null` for expensive queries. ## 5. Errors All errors are RFC 9457 problem documents (`application/problem+json`): ```json { "type": "https://fablepool.org/errors/validation", "title": "Validation failed", "status": 400, "detail": "2 fields failed validation.", "instance": "/api/v1/problems", "errors": { "difficulty": ["Must be between 1 and 5."], "topics": ["At least one topic is required."] } } ``` Stable `type` URIs: `validation`, `not-found`, `forbidden`, `unauthorized`, `conflict`, `rate-limited`, `expired-cursor`, `payload-too-large`. ## 6. Rate limiting Token-bucket per credential (per IP for anonymous), enforced in Redis. | Bucket | Anonymous | Authenticated | Notes | | --- | --- | --- | --- | | read | 60/min | 600/min | All GETs by default | | write | — | 60/min | POST/PATCH/DELETE default | | attempts | — | 30/min | Answer submissions (anti-brute-force) | | search | 20/min | 30/min | Meilisearch-backed `/search` | | oer | 2/hour | 6/hour | Import/export (expensive) | Every response includes: ``` X-RateLimit-Limit: 600 X-RateLimit-Remaining: 587 X-RateLimit-Reset: 1738240800 ``` `429` adds `Retry-After: `. Per-operation overrides are annotated in the spec via `x-rate-limit`. ## 7. Idempotency * `POST /problems/{id}/attempts`, `POST /me/bookmarks`, `POST /courses/{id}/enroll`, and `POST /votes` are **naturally idempotent or toggling**; duplicates do not create duplicate state. * For other creates, clients MAY send `Idempotency-Key: `; the server caches the first response for 24 h and replays it for retries with the same key and body hash. Mismatched bodies return `409`. ## 8. Webhooks * Subscriptions are managed at `/webhooks` (HTTPS endpoints only). * Delivery: `POST` with the `WebhookEnvelope` body, headers: * `X-FablePool-Event: review.completed` * `X-FablePool-Delivery: ` (use for idempotent processing) * `X-FablePool-Signature: t=,v1=` * Consumers MUST verify the signature and reject `t` older than 5 minutes (replay protection). After secret rotation, the previous secret stays valid for 24 h, and deliveries carry both `v1=` signatures during that window. * Retries: exponential backoff (1 m, 5 m, 30 m, 2 h, 6 h, 12 h, 24 h, 24 h — max 8 attempts). A subscription failing continuously for 7 days is disabled with `disabled_reason` set; re-enable via `PATCH { "active": true }`. * Event catalogue: `problem.published`, `course.published`, `review.requested`, `review.completed`, `attempt.graded`, `report.created` (moderators only), `oer.import.completed`. ## 9. Content security boundaries * The public API **never** returns answer keys, grader configuration, numeric tolerances, or hidden test cases. `answer_spec` in responses is the presentation subset only; the full spec is accepted on write and stored server-side. * Widget blocks reference manifests validated against `schemas/v1/widget-manifest.json`; widget code is served from a separate sandboxed origin and embedded via sandboxed iframes — it is never part of a trusted content document. * Solutions are gated server-side (solved / gave up / privileged role), not by client-side hiding. ## 10. Low-bandwidth & i18n * All list endpoints return summaries without content bodies; full `ContentDocument` payloads are fetched per-resource. * `format=manifest` on OER export skips binary assets. * `Accept-Language` is honoured for localized content documents where a translation exists; responses carry `Content-Language`. Untranslated content falls back to the document's source locale.