# Django (/guides/django)

## Configure your endpoint

Before you start, complete [Set up your product](/guides/setup/): 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:

```bash
python -m venv .venv
. .venv/bin/activate
```

Install the Django extra and Django REST Framework, then create a project with an `api` application:

```bash
pip install "recuut[django]>=0.11.0" djangorestframework
```

```bash
django-admin startproject my_app .
python manage.py startapp api
```

Set 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.

```bash
export RECUUT_API_URL="https://api.recuut.com"
export RECUUT_API_KEY="recuut_test_..."
```

### Generated contract

Generate contracts into your application using this `pyproject.toml` configuration:

```toml title="pyproject.toml"
[tool.recuut]
contracts-output-dir = "recuut_contracts"
```

```bash
recuut pull-contracts -v
```

Use 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.

```python title="api/views.py"
from dataclasses import dataclass

from recuut.django import recuut
from recuut_contracts import TextGenerationV1, TextGenerationV1Payment
from rest_framework import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response


@dataclass
class GenerateResult:
    text: str
    output_tokens: int


class 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
    )
```

### Raw revision

Use a raw revision when your application cannot commit generated contracts. Type checkers cannot check its variable names.

```bash
export RECUUT_REVISION_KEY="rvk_..."
```

```python title="api/views.py"
import os
from dataclasses import dataclass

from recuut import RawPayment, RawRevision
from recuut.django import recuut
from rest_framework import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response

revision = RawRevision(
    revision_key=os.environ["RECUUT_REVISION_KEY"],
    input_keys=("input_tokens",),
)


@dataclass
class GenerateResult:
    text: str
    output_tokens: int


class 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(revision)
def generate(request, payment: RawPayment) -> 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), values={"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.

### Function view

Use the function above and register it in `api/urls.py`:

```python title="api/urls.py"
from django.urls import path

from .views import generate

urlpatterns = [path("generate", generate)]
```

### Class-based view

Keep `GenerateSerializer` in `api/views.py`. For a generated contract, add this class in `api/class_views.py`:

```python title="api/class_views.py"
from recuut.django import recuut
from recuut_contracts import TextGenerationV1, TextGenerationV1Payment
from rest_framework.response import Response
from rest_framework.views import APIView

from .views import GenerateSerializer


class GenerateView(APIView):
    @recuut(TextGenerationV1)
    def post(self, 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
        )
```

For a raw revision, use this class instead:

```python title="api/class_views.py"
from recuut import RawPayment
from recuut.django import recuut
from rest_framework.response import Response
from rest_framework.views import APIView

from .views import GenerateSerializer, revision


class GenerateView(APIView):
    @recuut(revision)
    def post(self, request, payment: RawPayment) -> 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), values={"output_tokens": result.output_tokens}
        )
```

Register the class with `as_view()`:

```python title="api/urls.py"
from django.urls import path

from .class_views import GenerateView

urlpatterns = [path("generate", GenerateView.as_view())]
```

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:

```python title="my_app/urls.py"
from django.urls import include, path

urlpatterns = [path("", include("api.urls"))]
```

## Test your endpoint

Start Django from the directory containing `manage.py`:

```bash
python manage.py runserver
```

Check the unpaid path:

```bash
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](/guides/test-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.
