API Documentation & API Keys
Implementation Progress
| Feature | Status |
|---|---|
| OpenAPI spec auto-generated from FastAPI | ✅ Done (at /docs) |
| Typed Pydantic models for all endpoints | 🔲 Partial (some use raw dicts) |
| Organization-level API keys (create, revoke, list) | 🔲 Not yet |
| API key authentication middleware | 🔲 Not yet |
| Public API documentation page (styled, not raw Swagger) | 🔲 Not yet |
| Rate limiting per API key | 🔲 Not yet |
| API key usage tracking (last_used_at, request_count) | 🔲 Not yet |
Overview
Sondely needs a public API so organizations can integrate survey data into their own systems: dashboards, CRMs, grant reports, etc. API keys are scoped to organizations (not individual users), because the data belongs to the org.
Reference implementation: The IdeaPlaces Style Guide has a working API key system with hashed keys, prefix display, and bearer token auth. Study
/home/chipdev/ideaplaces-meta/ideaplaces-styleguide/src/lib/auth/api-key.tsfor the pattern.
API Key System
Key Structure
ip_live_<random_44_chars>
Prefix ip_live_ identifies the key as an Sondely production key. The full key is shown once at creation. Only the prefix + last 4 chars are stored for display (e.g., ip_live_****abcd).
Database Model
# New collection: api_keys
{
"id": "key-cuid",
"organization_id": "org-cuid", # Key is scoped to an org
"created_by_id": "user-cuid", # Which admin created it
"name": "Production Dashboard", # User-friendly name
"key_hash": "<argon2_hash>", # Hashed key (never store plaintext)
"prefix": "ip_live_", # Visible prefix for identification
"last_four": "abcd", # Last 4 chars for display
"last_used_at": null, # Updated on each API call
"request_count": 0, # Total requests made
"expires_at": null, # Optional expiry
"revoked_at": null, # Soft-delete
"created_at": "...",
}
API Key Endpoints
All require OWNER or ADMIN role in the organization.
POST /api/v1/organizations/{org_id}/api-keys # Create key (returns full key once)
GET /api/v1/organizations/{org_id}/api-keys # List keys (prefix + last4 only)
DELETE /api/v1/organizations/{org_id}/api-keys/{key_id} # Revoke key
Create response:
{
"id": "key-cuid",
"name": "Production Dashboard",
"key": "ip_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcd",
"prefix": "ip_live_",
"last_four": "abcd",
"created_at": "2026-04-07T..."
}
The key field is only returned on creation. After that, only prefix + last_four are available.
Authentication Middleware
API calls authenticate via Authorization: Bearer ip_live_... header. The middleware:
- Extracts the bearer token
- Hashes it and looks up
api_keysbykey_hash - Checks
revoked_atis null andexpires_athasn't passed - Updates
last_used_atand incrementsrequest_count - Resolves the
organization_idand attaches it to the request context - All subsequent queries are scoped to that org
If the header contains a JWT instead of an API key (starts with eyJ), fall back to the existing JWT auth. This means both auth methods work on all endpoints.
# backend/app/middleware/api_key.py
async def get_api_key_or_user(request: Request):
"""Resolve auth from API key OR JWT. Returns (org_id, user_id)."""
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer ip_live_"):
# API key auth
key = auth.replace("Bearer ", "")
return await validate_api_key(key)
else:
# JWT auth (existing flow)
return await get_current_active_user(request)
Public API Endpoints (API-key accessible)
These endpoints work with both JWT (logged-in user) and API key (programmatic access).
Surveys
GET /api/v1/surveys # List org surveys
GET /api/v1/surveys/{id} # Get survey details + questions
GET /api/v1/surveys/{id}/responses # Get all responses
GET /api/v1/surveys/{id}/responses?email=x # Filter by respondent
GET /api/v1/surveys/{id}/compliance # Compliance table data
GET /api/v1/surveys/{id}/respondent/{email}/history # Respondent attempt history
GET /api/v1/surveys/{id}/impact # Impact report data
Organizations
GET /api/v1/organizations/{id} # Org details
GET /api/v1/organizations/{id}/members # Member list
Responses (for webhooks / integrations)
POST /api/v1/surveys/{id}/respond # Submit a response programmatically
This allows external systems (Zapier, Make, custom integrations) to submit survey responses on behalf of respondents.
OpenAPI Spec
FastAPI already generates an OpenAPI spec at /api/v1/openapi.json. The current spec works but needs improvements:
Step 1: Add Pydantic response models to all endpoints
Many endpoints currently return raw dicts. Add proper response models:
# Example: compliance endpoint currently returns a raw list of dicts
# Should be:
class ComplianceMember(BaseModel):
member_id: str
email: str
name: str
status: str
attempts: int
last_taken: Optional[str]
next_due: Optional[str]
reminders_sent: int
baseline_score: Optional[float]
latest_score: Optional[float]
delta: Optional[float]
@router.get("/{survey_id}/compliance", response_model=List[ComplianceMember])
async def get_survey_compliance(...):
This makes the OpenAPI spec accurate and enables auto-generated client SDKs.
Step 2: Add endpoint descriptions and examples
@router.get("/{survey_id}/responses",
response_model=List[SurveyResponse],
summary="Get survey responses",
description="Returns all responses for a survey. API key users see all org responses. Respondents see only their own.",
responses={
200: {"description": "List of survey responses"},
403: {"description": "Not authorized to view this survey"},
404: {"description": "Survey not found"},
}
)
Step 3: Styled API docs page
Replace the raw Swagger UI with a styled documentation page that matches the Sondely design. Options:
- Scalar (https://scalar.com) provides a beautiful, branded API reference from the OpenAPI spec
- Stoplight Elements is another option
- Or build a custom page using the OpenAPI JSON
The docs page should be accessible at /api/docs and linked from the marketing site.
Implementation Order
- API key model + CRUD endpoints (backend, 1 PR)
- API key auth middleware (backend, same PR)
- Add response models to all endpoints (backend, 1 PR)
- Frontend: API key management page in org settings (1 PR)
- Styled API docs page (frontend, 1 PR)
- Rate limiting (backend, follow-up)
Frontend: API Key Management
Add a new tab "API Keys" in the organization detail page (visible to OWNER/ADMIN only).
┌──────────────────────────────────────────────────┐
│ CatalyzeUp Nonprofit │
│ │
│ Members Surveys API Keys Pending Invitations │
│ │
│ API Keys [Create API Key] │
│ ┌──────────────────────────────────────────────┐ │
│ │ Production Dashboard │ │
│ │ ip_live_****abcd │ │
│ │ Created Apr 7, 2026 Last used: 2 hours ago │ │
│ │ 1,234 requests [Revoke] │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Zapier Integration │ │
│ │ ip_live_****efgh │ │
│ │ Created Mar 15, 2026 Never used │ │
│ │ 0 requests [Revoke] │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
On creation, show the full key in a modal with a copy button and a warning: "This key will only be shown once. Copy it now."