API
Overview¶
SpeedPy comes with Django REST Framework (DRF) and drf-spectacular pre-installed and ready to use. No API endpoints are defined by default — this gives you a clean starting point.
Installed Packages¶
From pyproject.toml:
Getting Started¶
Create a Serializer¶
# mainapp/serializers.py
from rest_framework import serializers
from mainapp.models import Team
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ['id', 'name', 'slug', 'plan', 'created_at']
Create a ViewSet¶
# mainapp/api_views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from mainapp.models import Team
from mainapp.serializers import TeamSerializer
class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Team.objects.filter(
teammembership__user=self.request.user
)
Register URLs¶
# project/urls.py or mainapp/urls.py
from rest_framework.routers import DefaultRouter
from mainapp.api_views import TeamViewSet
router = DefaultRouter()
router.register('teams', TeamViewSet, basename='team')
urlpatterns += [
path('api/', include(router.urls)),
]
API Schema with drf-spectacular¶
drf-spectacular generates OpenAPI 3.0 schemas for your API. Add the schema views:
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns += [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
Then visit /api/docs/ for interactive Swagger documentation.
JWT Token Issuance Gates¶
The JWT token endpoint (POST /api/auth/token/) enforces security gates before issuing tokens:
Verified email¶
Users must have a verified primary email address. Unverified users receive a 401 with "Email address is not verified.".
MFA (TOTP)¶
When a user has TOTP two-factor authentication enabled, they must include an mfa_code field in the token request:
If the code is missing or invalid, the request is rejected with 401. Users without TOTP enrolled can obtain tokens without mfa_code.
Only TOTP codes are accepted — backup/recovery codes are not valid for JWT issuance. Users who lose their TOTP device can log into the web UI with a backup code and create a PAT instead.
Configuration¶
All gates are on by default (conservative). Disable via settings or environment variables:
| Setting | Default | Effect |
|---|---|---|
SPEEDPY_API_TOKEN_REQUIRE_VERIFIED_EMAIL |
True |
Require verified email for JWT and PAT issuance |
SPEEDPY_JWT_REQUIRE_MFA |
True |
Require TOTP code for JWT when MFA is enrolled |
SPEEDPY_PAT_REQUIRE_RECENT_REAUTH |
True |
Require recent reauthentication for PAT creation |
Rate Limiting¶
All API endpoints are rate-limited using DRF throttle classes. Limits differ by authentication status:
| Bucket | Default rate | Applied when |
|---|---|---|
| Anonymous | 100 requests / hour | No credentials supplied |
| Authenticated | 1,000 requests / hour | Any valid auth (PAT, OAuth2, JWT, session) |
All authenticated methods share the user throttle bucket — there are no per-token or per-scope quotas at this time.
Response headers¶
Every API response includes rate-limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the current window |
X-RateLimit-Remaining |
Requests remaining before throttling kicks in |
X-RateLimit-Reset |
Seconds until the rate-limit window resets |
Throttled responses (429)¶
When the limit is exceeded the API returns HTTP 429 Too Many Requests with a Retry-After header (seconds). The response body follows DRF's standard format:
Recommended client behavior¶
- Read
X-RateLimit-Remainingand slow down before hitting zero. - On a
429, wait at least the number of seconds inRetry-Afterbefore retrying. - Use exponential backoff with jitter for retries — do not retry in a tight loop.
- Never ignore
Retry-After; aggressive retry loops may extend the throttle window.
Idempotency Keys¶
Selected POST endpoints support the Idempotency-Key header for safe retries. When provided, the server stores the response and replays it on duplicate requests with the same key, preventing duplicate side effects.
Supported endpoints¶
| Endpoint | Method |
|---|---|
/api/v1/teams/{team_id}/invitations/ |
POST |
Header format¶
The key must be 1–128 characters, alphanumeric, hyphens, or underscores. A UUID is recommended:
Behavior¶
| Scenario | Response |
|---|---|
| First request with key | Normal response; result is stored |
| Same key + same body | Stored response replayed with Idempotency-Replay: true header |
| Same key + different body | 409 Conflict |
| No key | Normal processing (no idempotency) |
| Invalid key format | 400 Bad Request |
Notes¶
- Keys are scoped per user, HTTP method, and path — different users can reuse the same key string.
- Stored results expire after 24 hours (configurable via
SPEEDPY_IDEMPOTENCY_TTL_HOURS). - Only responses with status 2xx–4xx are stored; 5xx errors are not cached so the client can retry.
Request Correlation IDs¶
Every API response includes an X-Request-ID header for request tracing and debugging. The same ID appears in server logs, making it easy to correlate a client request with its server-side processing.
Client-supplied IDs¶
Clients may send their own X-Request-ID header. If the value is valid (1–128 alphanumeric, hyphen, or underscore characters), it is accepted and echoed. Invalid or oversized values are silently replaced with a server-generated UUID.
Response header¶
This header is present on every API response — success, error, 404, and 429 alike.
Usage for debugging¶
When reporting an issue or investigating a failed API call, include the X-Request-ID from the response. Support and operators can use it to locate the exact request in server logs.
Implementation details¶
- IDs are generated by django-structlog's
RequestMiddlewareand bound into the structured logging context. - All log lines for a given request automatically include
request_id. - The
X-Correlation-IDheader is also accepted by the logging middleware for distributed tracing across services.
Pagination¶
SpeedPy provides two pagination strategies. Choose based on the endpoint's data characteristics.
Page-number pagination (default)¶
All list endpoints use page-number pagination by default:
Response shape:
{
"count": 142,
"next": "https://example.com/api/v1/products/?page=3",
"previous": "https://example.com/api/v1/products/?page=1",
"results": [...]
}
| Parameter | Default | Max |
|---|---|---|
page |
1 | — |
| Page size | 50 | 50 (fixed) |
When to use: Most endpoints. Works well when total count is useful and the dataset is reasonably stable between requests.
Limitations: On large or frequently-changing datasets, items can be skipped or duplicated across pages because new inserts shift offsets.
Cursor pagination (for high-volume endpoints)¶
For endpoints where data changes frequently or the dataset is very large, SpeedPy provides a cursor pagination helper:
from speedpycom.api.pagination import SpeedPyCursorPagination
class MyHighVolumeListView(ListAPIView):
pagination_class = SpeedPyCursorPagination
# Ordering MUST be deterministic — use a unique tiebreaker
ordering = ["-created_at", "id"]
Response shape:
{
"next": "https://example.com/api/v1/events/?cursor=cD0yMDI2LTA2LTIx",
"previous": "https://example.com/api/v1/events/?cursor=cD0yMDI2LTA2LTIw",
"results": [...]
}
| Parameter | Default | Max |
|---|---|---|
cursor |
(start) | — |
page_size |
50 | 200 |
When to use: Audit logs, activity feeds, webhook delivery history, or any endpoint where the dataset grows quickly and clients iterate sequentially.
Key differences from page-number pagination:
- No
countfield (counting large tables is expensive). - Stable iteration — inserts don't cause items to be skipped or duplicated.
- Opaque cursor tokens — clients must not parse or construct them.
Ordering requirements for cursor pagination¶
Cursor pagination requires a deterministic ordering — the sort must fully order all rows with no ties. For models inheriting from BaseModel, ["-created_at", "id"] is a safe default because id (UUID) breaks ties when multiple rows share the same created_at.
Avoid ordering by non-unique columns alone (e.g. just -created_at), as identical timestamps will produce undefined page boundaries.
Guidance for fork owners¶
- Keep existing endpoints on page-number pagination unless you have a measured performance or correctness reason to switch.
- If adding a new high-volume list endpoint, prefer cursor pagination from the start.
- Do not mix pagination styles on the same endpoint — pick one and document it.
- If you add per-endpoint throttle overrides (stricter limits on expensive list queries), document those in the endpoint's OpenAPI description.