# FastAPI (/guides/fastapi)

## 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. Install the FastAPI extra in your Python environment and set the merchant key. The SDK uses `https://api.recuut.com` by default, so the usual path needs only `RECUUT_API_KEY`.

```bash
pip install "recuut[fastapi]>=0.11.0"
```

```bash
export RECUUT_API_KEY="recuut_test_..."
```

For a local or staging deployment, also set `RECUUT_API_URL` to that deployment's API origin. Use the same origin when you pull contracts and inspect transaction receipts. Leave it unset when using `https://api.recuut.com`.

[Pull contracts](/guides/pull-contracts/) into a `recuut_contracts` package in your application. The `-v` output prints the exact import path and Python class names for each published revision. To see a complete generated app before changing your own, [try an example](/guides/example/).

## Protect an existing route

This route has no payment protection:

```python title="Before"
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class GenerateRequest(BaseModel):
    prompt: str


class GenerateResponse(BaseModel):
    text: str


@app.post("/generate")
def generate(payload: GenerateRequest) -> GenerateResponse:
    text = payload.prompt.upper()
    return GenerateResponse(text=text)
```

This is the same route protected by a text-generation revision:

```python title="After"
from typing import Self

from fastapi import FastAPI
from pydantic import BaseModel, Field, model_validator
from recuut.fastapi import recuut
from recuut_contracts import (
    TextGenerationV1,
    TextGenerationV1Payment,
)

app = FastAPI()


class GenerateRequest(BaseModel):
    prompt: str
    input_tokens: int = Field(
        default=0,
        exclude=True,
        ge=0,
        json_schema_extra={"readOnly": True},
    )

    @model_validator(mode="after")
    def derive_input_tokens(self) -> Self:
        self.input_tokens = len(self.prompt.split())
        return self


class GenerateResponse(BaseModel):
    text: str


# Replace this sample handler with your application logic.
@app.post("/generate")
@recuut(TextGenerationV1)
def generate(
    payload: GenerateRequest,
    payment: TextGenerationV1Payment,
) -> GenerateResponse:
    text = payload.prompt.upper()
    return payment.complete(
        GenerateResponse(text=text),
        output_tokens=len(text.split()),
    )
```

Use the import names printed by `recuut pull-contracts -v`, and adapt the route and measurements to your application. The revision reads `input_tokens` from the request, so `GenerateRequest` derives it on the server in an excluded field; the decorator binds the field by name, and the validator overwrites any value a caller sends. `payment.complete(...)` validates and prepares the route response, completes payment, and returns your typed result. The decorator releases the prepared response with its receipt, preserving FastAPI response-model filtering, status and headers. Async handlers use `await payment.acomplete(...)`. Report what your application measured, in the unit the revision declares.

Use a raw revision when you cannot commit generated files. Set its `rvk_...` key, then adapt the same route. The values dictionary is checked at runtime rather than by your Python type checker. Never trust a client-supplied usage count.

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

```python title="app.py"
import os
from typing import Self

from fastapi import FastAPI
from pydantic import BaseModel, Field, model_validator
from recuut import RawPayment, RawRevision
from recuut.fastapi import recuut

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


class GenerateRequest(BaseModel):
    prompt: str
    input_tokens: int = Field(
        default=0,
        exclude=True,
        ge=0,
        json_schema_extra={"readOnly": True},
    )

    @model_validator(mode="after")
    def derive_input_tokens(self) -> Self:
        self.input_tokens = len(self.prompt.split())
        return self


class GenerateResponse(BaseModel):
    text: str


@app.post("/generate")
@recuut(revision)
def generate(
    payload: GenerateRequest,
    payment: RawPayment,
) -> GenerateResponse:
    text = payload.prompt.upper()
    return payment.complete(
        GenerateResponse(text=text),
        values={"output_tokens": len(text.split())},
    )
```

## Test your endpoint

Start your application as usual and call the protected route without payment headers:

```bash
curl --include \
  --request POST \
  --url http://127.0.0.1:8000/generate \
  --header 'Content-Type: application/json' \
  --data '{"prompt":"hello world"}'
```

For a paid revision, expect HTTP `402` with `PAYMENT-REQUIRED` and no handler result. If it differs, check the published revision, active Price, **Require payment** setting, and test key.

Then [test a paid request](/guides/test-paid-request/) against your protected URL.

## Integration behavior

For revisions without input variables, the integration checks access before FastAPI validates the request body, so a request without `PAYMENT-SIGNATURE` can receive `402` without a valid body. Revisions with input variables first use FastAPI's validation and capture every required input before checking access. Missing or invalid inputs return `422`; inputs above the Price maximum are rejected before the handler runs. A successful paid request completes payment before returning the handler result with `PAYMENT-RESPONSE`.

Use `input_map` when the parsed field name differs from the revision's input variable. Derive usage on the server; a mapping alone does not make a caller's count trustworthy. Configuration problems warn at startup by default, and `strict=True` makes them errors. While settlement is pending, the client receives HTTP `202` without your result or `PAYMENT-RESPONSE`. A returned `4xx` or `5xx` fails the transaction and reports only status, duration, media type, and byte size to recuut, never the response body or headers.
