Skip to main content

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:

  1. Sign into your GreekManage instance in another tab to obtain an access_token cookie
  2. Open the endpoint page here
  3. Fill in path / query parameters
  4. 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.

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.

Auth flows in detail

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"]
}
}
StatusMeaning
400Validation error — see errors for field-specific messages
401Not authenticated
403Authenticated but not authorized (wrong role / out of scope)
404Not found — sometimes returned instead of 403 to avoid revealing existence
409Conflict (duplicate, optimistic lock fail)
422Unprocessable entity — request was valid JSON but semantically wrong
429Throttled — see Retry-After header
500Server 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

ScopeAnonymousAuthenticated
Default2000/day2000/day
auth (login, OAuth callback)60/minute
token_refresh120/minute
sso (SSO initiate, code exchange)20/minute
sso_discovery (SSO/OAuth discovery)120/minute
account_request30/hour
account_request_email2/hour (per email)
password_reset20/hour
password_reset_email3/hour (per email)
admin_password_reset_target10/hour (per target)
passkey_begin60/minute60/minute
passkey_complete30/minute30/minute
pnm_submit_ip5/hour
pnm_submit_email1/minute
pnm_verify_email5/hour
public_reference_letter5/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, profile
  • auth — login, logout, SSO
  • organizations, chapters, regions, memberships
  • compliance — requirements, submissions, alerts
  • elections — elections, ballots, results
  • finances — invoices, payments, dues settings
  • forums — engage / community
  • learning — courses, enrollments, certificates
  • foundation — campaigns, donations, donors
  • alumni — directory, mentors, career board
  • members — profiles, skills, recognitions
  • messaging — conversations, messages
  • notifications — list, prefs
  • ai-services — chat, config, embeddings
  • platform — 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.