# FastAPI Quickstart

Complete the raw-HTTP MonetizeKit activation journey in a FastAPI service.

<!-- docs-source -->
Source: https://learning.monetizekit.app/docs/quickstarts/fastapi
<!-- /docs-source -->

FastAPI uses the Raw HTTP path in Python. Complete every step with an isolated non-production workspace before deploying.

## Prerequisites

- Python 3.12+
- pip
- A scoped non-production API key

## Environment setup

```bash
MONETIZEKIT_BASE_URL=https://app.monetizekit.app/api/v1
MONETIZEKIT_API_KEY=${MONETIZEKIT_API_KEY}
MONETIZEKIT_PLAN_ID=${MONETIZEKIT_PLAN_ID}
MONETIZEKIT_FEATURE_KEY=${MONETIZEKIT_FEATURE_KEY}
MONETIZEKIT_DENIED_FEATURE_KEY=${MONETIZEKIT_DENIED_FEATURE_KEY}
MONETIZEKIT_METER_ID=${MONETIZEKIT_METER_ID}
MONETIZEKIT_RUN_ID=replace-with-a-unique-run-id
```

## Install commands

```bash
pip install -r examples/docs-quickstarts/fastapi/requirements.txt
```

## Happy path

### 1. Create a customer

Create a unique test customer. Raw HTTP variants send a deterministic idempotency key; the current Node SDK does not expose one for customer creation.

```python
def create_customer(config: dict[str, str], idempotency_key: str) -> dict[str, Any]:
    return request_json(
        config,
        "POST",
        "/customers",
        201,
        headers={"Idempotency-Key": idempotency_key},
        body={
            "name": f"Docs quickstart {config['run_id']}",
            "email": f"docs-quickstart+{config['run_id']}@example.com",
        },
    )
```

Expected HTTP status: 201.

### 2. Select the isolated published plan

List published plans and select the exact MONETIZEKIT_PLAN_ID provisioned for this run.

```python
def select_plan(config: dict[str, str]) -> dict[str, Any]:
    plans = request_json(config, "GET", "/plans", 200)
    plan = next(
        (candidate for candidate in plans.get("data", []) if candidate["id"] == config["plan_id"]),
        None,
    )
    if plan is None:
        raise RuntimeError(
            f"Isolated plan {config['plan_id']} is not published in this workspace"
        )
    return plan
```

Expected HTTP status: 200.

### 3. Attach the plan

Create the customer's subscription. Raw HTTP variants reuse a deterministic idempotency key.

```python
def attach_plan(
    config: dict[str, str],
    customer_id: str,
    plan_id: str,
    idempotency_key: str,
) -> dict[str, Any]:
    return request_json(
        config,
        "POST",
        "/subscriptions",
        201,
        headers={"Idempotency-Key": idempotency_key},
        body={"customerId": customer_id, "planId": plan_id, "status": "active"},
    )
```

Expected HTTP status: 201.

### 4. Check and enforce an entitlement

Request the feature decision and stop protected work unless `allowed` is true.

```python
def require_entitlement(
    config: dict[str, str], customer_id: str
) -> dict[str, Any]:
    decision = request_json(
        config,
        "GET",
        f"/entitlements/{quote(customer_id, safe='')}/{quote(config['feature_key'], safe='')}",
        200,
    )
    if not decision["allowed"]:
        raise RuntimeError(
            f"Entitlement denied ({decision['reasonCode']}): {decision['reason']}"
        )
    return decision
```

Expected HTTP status: 200.

### 5. Submit usage

Record one metered event with a deterministic idempotency key that remains stable across retries.

```python
def submit_usage(
    config: dict[str, str], customer_id: str, idempotency_key: str
) -> dict[str, Any]:
    return request_json(
        config,
        "POST",
        "/usage/events",
        201,
        headers={"Idempotency-Key": idempotency_key},
        body={"customerId": customer_id, "meterId": config["meter_id"], "value": 1},
    )
```

Expected HTTP status: 201.

### 6. Verify the observed usage

Read the meter after submission and include the observed result in the smoke output.

```python
def validate_usage(
    config: dict[str, str], customer_id: str
) -> dict[str, Any]:
    return request_json(
        config,
        "GET",
        f"/usage/{quote(customer_id, safe='')}/{quote(config['meter_id'], safe='')}",
        200,
    )
```

Expected HTTP status: 200.

## Troubleshooting

### 401 missing_api_key / invalid_api_key

Confirm the API key is present, active, and uses the non-production prefix for the environment you intend to test.

[Read the full guide](/docs/troubleshooting/auth-issues)

### 403 missing scope

Issue a key with customers:create, plans:read, subscriptions:write, entitlements:read, usage:write, usage:read, and cleanup permissions.

[Read the full guide](/docs/troubleshooting/auth-issues)

### Entitlement denied

Confirm MONETIZEKIT_PLAN_ID is the isolated plan that grants MONETIZEKIT_FEATURE_KEY and does not grant MONETIZEKIT_DENIED_FEATURE_KEY.

[Read the full guide](/docs/guides/entitlement-patterns)

### A retry records duplicate usage

Keep MONETIZEKIT_RUN_ID stable for retries of the same logical smoke run so the generated Idempotency-Key is reused.

[Read the full guide](/docs/troubleshooting/metering-issues)

## Deploy + smoke test

Platform: Docker Compose.

1. Install requirements and inject the seven quickstart environment variables.
2. Build and run the API image before invoking its smoke entry point.

```bash
python3 examples/docs-quickstarts/fastapi/quickstart.py --smoke
```

Expected: One JSON object with outcome "passed", allowed and denied entitlement decisions, and observed usage data, followed by successful subscription/customer cleanup.

## CLI links

Use the CLI to authenticate and inspect the same workspace after the smoke test.

```bash
monetizekit auth login
```

```bash
monetizekit customers list
```