Django
Protect Django REST Framework APIs with typed payments and ordinary serializers.
Configure your endpoint
Before you start, complete Set up your product: publish a revision with a Price and copy a merchant test key.
Use Python 3.11 through 3.14. Create and activate a virtual environment:
python -m venv .venv. .venv/bin/activateInstall the Django extra and Django REST Framework, then create a project with an api application:
pip install "recuut[django]>=0.11.0" djangorestframeworkdjango-admin startproject my_app .python manage.py startapp apiSet the merchant test key in your server environment. The SDK reads RECUUT_API_KEY unless you pass a client or api_key to the decorator.
export RECUUT_API_URL="https://api.recuut.com"export RECUUT_API_KEY="recuut_test_..."Generate contracts into your application using this pyproject.toml configuration:
[tool.recuut]contracts-output-dir = "recuut_contracts"recuut pull-contracts -vUse the import names printed by recuut pull-contracts -v. Generated payment methods require each reported variable as a keyword, so type checkers catch missing or misspelled names.
from dataclasses import dataclassfrom recuut.django import recuutfrom recuut_contracts import TextGenerationV1, TextGenerationV1Paymentfrom rest_framework import serializersfrom rest_framework.decorators import api_viewfrom rest_framework.response import Response@dataclassclass GenerateResult: text: str output_tokens: intclass GenerateSerializer(serializers.Serializer): prompt = serializers.CharField(max_length=10_000, write_only=True) text = serializers.CharField(read_only=True) def create(self, validated_data) -> GenerateResult: # Replace this sample with your logic and measured usage. text = validated_data["prompt"].upper() return GenerateResult(text=text, output_tokens=len(text.split()))@api_view(["POST"])@recuut(TextGenerationV1)def generate(request, payment: TextGenerationV1Payment) -> Response: serializer = GenerateSerializer(data=request.data) serializer.is_valid(raise_exception=True) refusal = payment.access( input_tokens=len(serializer.validated_data["prompt"].split()) ) if refusal is not None: return refusal result = serializer.save() return payment.complete( Response(serializer.data), output_tokens=result.output_tokens )DRF parses request.data and handles validation errors. Use your existing serializer. Call payment.access(...) after validation and before serializer.save(). Return any refusal immediately. If the revision has no input variables, call payment.access() with no arguments.
serializer.save() runs your application logic. Replace the sample serializer's create() with that logic and real usage measurements. payment.complete(Response(serializer.data), ...) finalizes and renders the response, completes payment, and attaches its receipt. If saving or response preparation raises after access succeeds, the decorator reports failure instead of completing payment.
Choose your view style
Both function views and APIView methods use the same payment operations.
Use the function above and register it in api/urls.py:
from django.urls import pathfrom .views import generateurlpatterns = [path("generate", generate)]Preserve your application's DRF authentication, permissions, and CSRF policy. Session-authenticated requests require a valid CSRF token. Payment headers do not replace your application's user authorization.
Include the application routes from the project URL configuration:
from django.urls import include, pathurlpatterns = [path("", include("api.urls"))]Test your endpoint
Start Django from the directory containing manage.py:
python manage.py runserverCheck the unpaid path:
curl --include \ --request POST \ --url http://127.0.0.1:8000/generate \ --header 'Content-Type: application/json' \ --data '{"prompt":"paid testnet response"}'Expect HTTP 402, a PAYMENT-REQUIRED header, and no result. A refused request does not run serializer.save(). If this differs, check the published revision, active Price, Require payment setting, and test key.
If your view returns 4xx or 5xx, the decorator reports failure with the status, duration, media type, and response size. It never sends the body or headers to recuut. While settlement is pending, the client receives 202 without your result. Streaming responses are not supported.
Then test a paid request against the same URL. A completed paid request returns your result with a PAYMENT-RESPONSE receipt header.
Sync and async
These DRF examples use synchronous views and payment.access() / payment.complete(). Plain Django async views use await payment.aaccess() / await payment.acomplete(). For plain Django function and class views, parse and validate the request yourself, then call the same payment methods.