API reference
Every endpoint, request schema, response schema, and parameter — auto-generated from the running Django backend via drf-spectacular.
The sidebar on the left organizes endpoints by tag (which usually maps to a Django app — compliance, elections, members, etc.).
Try it out
The endpoint pages include an interactive try-it-out panel. To use it:
- Sign into your GreekManage instance in another tab to obtain an
access_tokencookie - Open the endpoint page here
- Fill in path / query parameters
- Click Send API Request
Responses, headers, and timing show inline.
Authentication
All endpoints (except a small set listed under auth and public) require a JWT.
Web (cookie)
Cookie: access_token=<jwt>
The cookie is set automatically by POST /api/auth/login/. It's HttpOnly, Secure, SameSite=Lax.
Mobile / scripts (header)
Authorization: Bearer <jwt>
Use this when calling from outside a browser or from the iOS / Android app.
Tenant scoping
Every authenticated request is automatically scoped to the user's organization via OrganizationContextMiddleware. You don't need to pass the org ID on most endpoints — it's derived from your token.
Exceptions:
- Platform admins acting on behalf of a tenant: pass
X-Organization-Id: <uuid> - Public endpoints (PNM apply, public donate): explicitly take an org slug or campaign slug in the URL
Error format
{
"detail": "Human-readable error",
"code": "machine_readable_code",
"errors": {
"field_name": ["Field-specific error"]
}
}
| Status | Meaning |
|---|---|
400 | Validation error — see errors for field-specific messages |
401 | Not authenticated |
403 | Authenticated but not authorized (wrong role / out of scope) |
404 | Not found — sometimes returned instead of 403 to avoid revealing existence |
409 | Conflict (duplicate, optimistic lock fail) |
422 | Unprocessable entity — request was valid JSON but semantically wrong |
429 | Throttled — see Retry-After header |
500 | Server error — please report |
Pagination
List endpoints use page-number pagination:
GET /api/chapters/?page=2&page_size=50
{
"count": 142,
"next": "...?page=3",
"previous": "...?page=1",
"results": [ ... ]
}
Default page_size: 25. Maximum: 100. Asking for more than the maximum returns
the maximum rather than an error.
Rows are returned in a deterministic order: where an endpoint's ordering is not already unique, the primary key is appended as a tiebreaker, so a row never repeats on one page while disappearing from another.
Endpoints that return a bare array
A few endpoints deliberately return a plain JSON array with no envelope. These are bounded per-organization configuration lists that clients consume whole — category pickers, select options, ordering UIs — where paging would silently truncate the list:
GET /api/service-hours/organizations/{org_id}/categories/GET /api/organizations/{org_pk}/recognition-categories/GET /api/organizations/{org_pk}/custom-fields/GET /api/organizations/{org_pk}/regions/GET /api/auth/sso/organizations/{org_pk}/identity-providers/GET /api/organizations/{org_id}/document-categories/
Search and typeahead endpoints (member, admin, user and mention search) also return bare arrays, as do all public/unauthenticated endpoints — ICS feeds, public campaign and donate pages, PNM apply, public reference letters, the public org/chapter directory and certificate verification.
Each endpoint page in this reference shows its exact response shape; when in doubt, trust that over this summary.
Filtering + ordering
Where supported, filter and order via query params:
GET /api/members/?status=undergraduate&ordering=-joined_at
The endpoint detail pages list which params each endpoint accepts.
Versioning
The API version is exposed in the OpenAPI schema (info.version). Breaking changes bump the major version with at least 90 days of notice and an overlap period when both versions run.
The schema's info.version tracks the platform release, and the API reference above is regenerated from that schema on every docs build, so it always reflects the current release. Treat it as a release marker rather than an independently-versioned contract — there is no formal API-contract versioning policy yet, and breaking changes are still rare and announced in CHANGELOG.md.
Throttling
| Scope | Anonymous | Authenticated |
|---|---|---|
| Default | 2000/day | 2000/day |
auth (login, OAuth callback) | 60/minute | – |
token_refresh | 120/minute | – |
sso (SSO initiate, code exchange) | 20/minute | – |
sso_discovery (SSO/OAuth discovery) | 120/minute | – |
account_request | 30/hour | – |
account_request_email | 2/hour (per email) | – |
password_reset | 20/hour | – |
password_reset_email | 3/hour (per email) | – |
admin_password_reset_target | – | 10/hour (per target) |
passkey_begin | 60/minute | 60/minute |
passkey_complete | 30/minute | 30/minute |
pnm_submit_ip | 5/hour | – |
pnm_submit_email | 1/minute | – |
pnm_verify_email | 5/hour | – |
public_reference_letter | 5/hour | – |
Hit 429 and back off per Retry-After. Authoritative source: REST_FRAMEWORK.DEFAULT_THROTTLE_RATES in backend/greekmanage/settings.py; the shipped production defaults live in backend/apps/common/throttle_rates.py.
What an IP-scoped bucket actually is
Per-IP buckets are keyed on the caller's real address, derived by counting in from the right of X-Forwarded-For (NUM_PROXIES) — the leftmost entry is supplied by the caller and cannot be trusted. See backend/apps/common/client_ip.py.
That means one bucket is one shared network, not one person: a chapter house behind a single NAT, campus wifi, or mobile carrier CGNAT all share it. The rates above were raised in v0.71.13 for exactly this reason, and it is why account_request and password_reset are additionally capped per email address — a loose network bucket protects the shared NAT, and a tight per-target bucket protects the individual. Both throttles apply, so a request must satisfy each.
Webhook events (out of scope today)
Outbound webhooks for events (member added, dues paid, election closed, etc.) are on the roadmap. For now, integrations poll relevant endpoints.
Generating the schema yourself
docker compose exec backend python manage.py spectacular \
--file ../docs-site/openapi/greekmanage.yaml \
--format openapi
Then regenerate the API MDX in this site:
cd docs-site
npm run docusaurus gen-api-docs all
The new endpoints appear in the sidebar after a rebuild.
Reading the endpoint pages
Each endpoint page in the sidebar has:
- Path + method at the top
- Description pulled from the docstring on the view
- Path / query parameters with type, required flag, description
- Request body schema with example
- Response schemas by status code
- Try it out panel
- Code samples in curl, Python, Node.js, TypeScript
Tags
Endpoints are grouped by tag in the sidebar. Click a tag to expand the list. Tags map roughly to Django apps:
accounts— user, password, profileauth— login, logout, SSOorganizations,chapters,regions,membershipscompliance— requirements, submissions, alertselections— elections, ballots, resultsfinances— invoices, payments, dues settingsforums— engage / communitylearning— courses, enrollments, certificatesfoundation— campaigns, donations, donorsalumni— directory, mentors, career boardmembers— profiles, skills, recognitionsmessaging— conversations, messagesnotifications— list, prefsai-services— chat, config, embeddingsplatform— tenant management (platform admins only)backups— full DB / org export
Help
- Endpoint missing? Most likely an authentication / permission issue — check the request comes with a valid JWT and the user has scope for the resource.
- Field looks wrong? The schema is generated from serializers; if a field renders with the wrong type, it's a serializer issue — open an issue.
- Want a new endpoint? Open a feature request issue in the repo.