Get startedIntegrate

Protect a route in your existing FastAPI application with a published recuut revision.

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

pip install "recuut[fastapi]>=0.11.0"
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 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.

Protect an existing route

This route has no payment protection:

Before
from fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class GenerateRequest(BaseModel):    prompt: strclass 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:

After
from typing import Selffrom fastapi import FastAPIfrom pydantic import BaseModel, Field, model_validatorfrom recuut.fastapi import recuutfrom 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 selfclass 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.

export RECUUT_REVISION_KEY="rvk_..."
app.py
import osfrom typing import Selffrom fastapi import FastAPIfrom pydantic import BaseModel, Field, model_validatorfrom recuut import RawPayment, RawRevisionfrom recuut.fastapi import recuutapp = 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 selfclass 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:

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

On this page