Skip to content

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:

djangorestframework==3.16.1
drf-spectacular==0.29.0

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:

{
  "email": "user@example.com",
  "password": "...",
  "mfa_code": "123456"
}

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:

{
  "detail": "Request was throttled. Expected available in 42 seconds."
}
  1. Read X-RateLimit-Remaining and slow down before hitting zero.
  2. On a 429, wait at least the number of seconds in Retry-After before retrying.
  3. Use exponential backoff with jitter for retries — do not retry in a tight loop.
  4. 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

Idempotency-Key: <string>

The key must be 1–128 characters, alphanumeric, hyphens, or underscores. A UUID is recommended:

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

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

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

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 RequestMiddleware and bound into the structured logging context.
  • All log lines for a given request automatically include request_id.
  • The X-Correlation-ID header 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:

GET /api/v1/products/?page=2

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"]
GET /api/v1/events/?cursor=cD0yMDI2LTA2LTIx

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 count field (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.