API Development Guide
Throttling
Bitcaster uses a sliding window algorithm for API rate limiting, implemented in SlidingWindowThrottle. This mechanism ensures a smoother rate limit compared to fixed window counters and is backed by Redis for atomicity.
Customising Thresholds
You can customise the rate limit (number of requests) and the window (duration in seconds) at different levels. The system resolves these values with the following priority:
1. Per-Action (Directly on Method)
This is the most granular way to define limits, especially useful for methods decorated with @action.
@action(detail=True, methods=["post"])
def trigger(self, request, pk=None):
...
trigger.throttle_rate = 5
trigger.throttle_window = 60
2. Per-Action (On ViewSet Class)
You can also define action-specific limits as class attributes using the throttle_rate_{action_name} and throttle_window_{action_name} naming convention.
class EventViewSet(viewsets.ViewSet):
throttle_rate_list = 100
throttle_window_list = 3600
3. View-Level Default
If no action-specific override is found, the ViewSet will fall back to its own defaults.
class EventViewSet(viewsets.ViewSet):
throttle_rate = 50
throttle_window = 60
4. Global Default
If none of the above are defined, the defaults from the SlidingWindowThrottle class are used (default: 30 requests per 60 seconds).
Implementation Details
The implementation can be found in src/bitcaster/api/throttling.py. It uses a LUA script to perform the "check-and-add" operation atomically in Redis.
An in-memory fallback mechanism is provided in case the Redis connection fails, ensuring the system remains operational though with slightly less precise rate limiting across multiple application instances.
Adding a New Endpoint
All public endpoints live in src/bitcaster/api/ and are wired in src/bitcaster/api/urls.py.
1. Create the View
Write a view class in src/bitcaster/api/<resource>.py. Every endpoint must:
- extend
SecurityMixin(src/bitcaster/api/base.py), which provides the authentication classes (ApiKeyAuthentication,BasicAuthentication,SessionAuthentication), the permission class (ApiApplicationPermission) and theSlidingWindowThrottlethrottle; - declare
required_grantswith the grants the caller needs (seesrc/bitcaster/auth/constants.py, e.g.Grant.ORGANIZATION_READ,Grant.EVENT_TRIGGER,Grant.MANAGE_APPLICATION_USERS); - set
serializer_class(serializers live insrc/bitcaster/api/serializers.py); - decorate every handler with
@extend_schema(...)so the OpenAPI schema stays complete.
from bitcaster.api.base import SecurityMixin
from bitcaster.auth.constants import Grant
class MyResourceView(SecurityMixin, RetrieveAPIView):
serializer_class = MyResourceSerializer
required_grants = [Grant.ORGANIZATION_READ]
lookup_url_kwarg = "slug"
lookup_field = "slug"
def get_queryset(self):
return MyResource.objects.filter(organization__slug=self.kwargs["org"])
@extend_schema(responses={200: MyResourceSerializer})
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
2. Register the URL
Add the path in src/bitcaster/api/urls.py. Routing conventions:
o/<slug:org>/for organization-scoped resources;o/<slug:org>/p/<slug:prj>/for project-scoped resources;o/<slug:org>/p/<slug:prj>/a/<slug:app>/for application-scoped resources;me/...for the authenticated user profile.
path(
"o/<slug:org>/my-resource/<slug:slug>/",
MyResourceView.as_view({"get": "retrieve"}),
name="my-resource-detail",
),
3. Scope and Grant Enforcement
ApiBasePermission (src/bitcaster/api/permissions.py) validates that the API key's organization/project/application scope matches the URL kwargs and that the key's grants intersect the view's required_grants. A key with FULL_ACCESS bypasses the grant check. Out-of-scope keys or missing grants fail with 403 and an InvalidGrantError message.
4. Document the Endpoint
Add the endpoint to the API reference under docs/api/ (one page per resource, e.g. docs/api/users.md) with the method, path, parameters, request/response examples and required grants.
5. Test It
Add integration tests under tests/api/test_api_<resource>.py following the existing patterns:
- fixtures with a valid API key (
tests/api/conftest.py); - happy path, missing grant (
403), wrong scope, invalid payload (4xx) and idempotency cases; - add the new URL to the public-URL list checked by
tests/api/test_api_smoke.py; - the schema tests (
tests/api/test_api_schema.py) assert the OpenAPI schema and the Swagger/Redoc render — a missing@extend_schemabreaks them.