> ## Documentation Index
> Fetch the complete documentation index at: https://connect-docs.supertab.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

The Supertab Connect Python SDK lets publishers implement Really Simple Licensing (RSL) and the
Crawler Authentication Protocol (CAP) in Python applications.

The SDK handles license token verification, bot detection, enforcement decisions, analytics recording,
and customer-side license token acquisition.

**Requirements:** Python 3.12+.

## Installation

Install the SDK from PyPI:

```bash theme={null}
pip install supertab-connect-sdk
```

## Initializing the Client

The merchant client is async and uses `httpx.Request` for request handling.

```python theme={null}
from supertab_connect import (
    EnforcementMode,
    SupertabConnect,
    SupertabConnectConfig,
    default_bot_detector,
)

client = SupertabConnect(
    SupertabConnectConfig(
        api_key="stc_live_your_api_key",  # read from environment variables or secrets management
        enforcement=EnforcementMode.SOFT,
        bot_detector=default_bot_detector,
        debug=False,
    )
)
```

### Configuration Options

| Property       | Type                  | Required | Default | Description                                                               |
| -------------- | --------------------- | -------- | ------- | ------------------------------------------------------------------------- |
| `api_key`      | `str`                 | Yes      | —       | Your Supertab Merchant API Key                                            |
| `enforcement`  | `EnforcementMode`     | No       | `SOFT`  | How to handle bots without a valid token                                  |
| `bot_detector` | `BotDetector \| None` | No       | `None`  | Function that receives an `httpx.Request` and returns whether it is a bot |
| `debug`        | `bool`                | No       | `False` | Enables verbose SDK logging through Python logging                        |

The SDK enforces a singleton pattern per API key. Creating another client with the same key returns the existing instance.
Creating one with a different key raises an error unless you call `SupertabConnect.reset_instance()` or pass `reset=True`.

## Common Workflows

### Handle a Protected Request

Use `handle_request()` when you want the SDK to manage the full lifecycle:

1. Extract a token from the `Authorization: License <token>` header.
2. Verify the token against the Supertab JWKS.
3. Record an analytics event.
4. If no token is present, run bot detection and apply the enforcement mode.

```python theme={null}
import httpx

from supertab_connect import (
    EnforcementMode,
    HandlerAction,
    SupertabConnect,
    SupertabConnectConfig,
    default_bot_detector,
)

client = SupertabConnect(
    SupertabConnectConfig(
        api_key="stc_live_your_api_key",
        enforcement=EnforcementMode.STRICT,
        bot_detector=default_bot_detector,
    )
)

request = httpx.Request(
    "GET",
    "https://example.com/premium/article",
    headers={
        "Authorization": "License your.jwt.token",
        "User-Agent": "Mozilla/5.0",
        "Accept": "text/html",
        "Accept-Language": "en-US",
        "Sec-CH-UA": '"Chromium";v="123"',
    },
)

# Inside an async function
async with client:
    result = await client.handle_request(request)

if result["action"] is HandlerAction.BLOCK:
    status = result["status"]
    headers = result["headers"]
    body = result["body"]
    # Return this response from your framework
else:
    headers = result.get("headers", {})
    # Apply returned headers, then serve content
```

### Framework Integration

Create an `httpx.Request` from your framework request object, then translate the `HandlerResult` back to a framework response.
For example, in FastAPI:

```python theme={null}
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request, Response
import httpx

from supertab_connect import (
    EnforcementMode,
    HandlerAction,
    SupertabConnect,
    SupertabConnectConfig,
    default_bot_detector,
)

connect = SupertabConnect(
    SupertabConnectConfig(
        api_key="stc_live_your_api_key",
        enforcement=EnforcementMode.SOFT,
        bot_detector=default_bot_detector,
    )
)


@asynccontextmanager
async def lifespan(_app: FastAPI):
    try:
        yield
    finally:
        await connect.aclose()


app = FastAPI(lifespan=lifespan)


@app.get("/premium/article")
async def premium_article(request: Request):
    sdk_request = httpx.Request(
        request.method,
        str(request.url),
        headers=dict(request.headers),
    )

    result = await connect.handle_request(sdk_request)

    if result["action"] is HandlerAction.BLOCK:
        return Response(
            content=result["body"],
            status_code=result["status"],
            headers=result["headers"],
        )

    return Response(
        content="Premium content",
        headers=result.get("headers", {}),
    )
```

### Verify a Token and Record Usage

Use `verify_and_record()` when you need custom routing or response handling but still want analytics and billing events.

```python theme={null}
from supertab_connect import EnforcementMode, SupertabConnect, SupertabConnectConfig

client = SupertabConnect(
    SupertabConnectConfig(
        api_key="stc_live_your_api_key",
        enforcement=EnforcementMode.SOFT,
    )
)

# Inside an async function
async with client:
    result = await client.verify_and_record(
        token="your.jwt.token",
        resource_url="https://example.com/premium/article",
        user_agent="Mozilla/5.0",
        request_headers={
            "Accept": "text/html",
            "Accept-Language": "en-US",
        },
    )

if not result.valid:
    # Return 401 or another response appropriate for your application
    print(result.error)
```

### Verify Without Recording

Use the static `verify()` method when you only need to check token validity and do not want analytics side effects.

```python theme={null}
from supertab_connect import SupertabConnect

# Inside an async function
result = await SupertabConnect.verify(
    token="your.jwt.token",
    resource_url="https://example.com/premium/article",
)

if not result.valid:
    print(result.error)
```

### Obtaining a License Token

Use `obtain_license_token()` when you are building a crawler or client that needs to access protected resources.
The SDK fetches the publisher's `license.xml`, finds the best matching content rule, exchanges your client credentials
for a token, and caches tokens in memory until shortly before expiry.

```python theme={null}
from supertab_connect import obtain_license_token

# Inside an async function
token = await obtain_license_token(
    client_id="your_client_id",
    client_secret="your_client_secret",
    resource_url="https://example.com/premium/article",
)

if token is not None:
    headers = {"Authorization": f"License {token}"}
    # Use these headers on the resource request
```

If you pass a `usage` value and the matching RSL content permits that usage without a token server, the function returns `None`
because no token is required.

```python theme={null}
from supertab_connect import UsageType, obtain_license_token

# Inside an async function
token = await obtain_license_token(
    client_id="your_client_id",
    client_secret="your_client_secret",
    resource_url="https://example.com/public-resource",
    usage=UsageType.AI_INPUT,
)
```

## Important Types

### `EnforcementMode`

Enforcement modes determine what happens when a bot is detected without a valid license token.

* `STRICT`: Blocks the request immediately with a `401 Unauthorized` (missing or invalid token) or `403 Forbidden` (token valid but wrong audience).
* `SOFT` (Default): Allows the request but attaches RSL headers (`Link`, `X-RSL-Status`) to signal that a license is required.
* `DISABLED`: No enforcement, signaling, or analytics recording. Requests are allowed without licensing intervention.

Non-bot requests are always allowed regardless of mode. Invalid tokens are always blocked except in `DISABLED` mode.

### `HandlerResult`

Returned by `handle_request()`:

* `{ "action": HandlerAction.ALLOW, "headers": ... }`: The request should proceed. Apply returned headers if present.
* `{ "action": HandlerAction.BLOCK, "status": ..., "body": ..., "headers": ... }`: The request should be rejected with the provided response data.

### `RSLVerificationResult`

Returned by `verify()` and `verify_and_record()`:

| Field   | Type          | Description                                           |
| ------- | ------------- | ----------------------------------------------------- |
| `valid` | `bool`        | Whether the token is valid for the requested resource |
| `error` | `str \| None` | Error message when invalid                            |

## Error Handling

The high-level helpers return framework-friendly result shapes rather than typed invalid-token reason codes:

* `handle_request()` returns a `HandlerResult` with `action`, and when blocked, response `status`, `body`, and `headers`.
* `verify()` and `verify_and_record()` return `RSLVerificationResult(valid=False, error=...)`.

Treat `error` as a message for logs or responses. If your application needs to branch on a machine-readable reason,
call the lower-level `verify_license_token()` function and inspect `InvalidLicenseToken.reason`.

Common invalid-token reasons include:

* `missing_license_token`: No license token was provided.
* `invalid_license_header`: The JWT header is malformed.
* `invalid_license_algorithm`: The token uses an unsupported signing algorithm.
* `invalid_license_payload`: The JWT payload is malformed.
* `invalid_license_issuer`: The token issuer is not recognized.
* `license_signature_verification_failed`: The token signature could not be verified.
* `license_token_expired`: The token has expired.
* `invalid_license_audience`: The token is valid but does not cover the requested URL.
* `server_error`: The SDK could not validate the token because of a platform-side or JWKS fetch error.

## Tips and Pitfalls

**Pass a bot detector for enforcement.** By default, `bot_detector` is `None`, so requests without tokens are treated as non-bot traffic.
Use `default_bot_detector` or provide your own detector if you expect `handle_request()` to signal or block missing-token bots.

**Apply returned headers.** In `SOFT` mode, the SDK signals licensing requirements through response headers. If you drop those headers,
crawlers will not receive the correct RSL signal.

**Use the async context manager.** `async with client:` closes shared HTTP clients used for event recording and JWKS fetching.
For long-lived web apps, create one client at startup and close it during shutdown.

**Cache behavior is in-memory.** `obtain_license_token()` caches `license.xml` by origin and license tokens by client, token server,
and matched URL pattern. Process restarts clear that cache.

## API Reference

### Static and Module Functions

| Method                                                                 | Description                                                     |
| ---------------------------------------------------------------------- | --------------------------------------------------------------- |
| `SupertabConnect.verify(*, token, resource_url, ...)`                  | Verify a token without analytics recording                      |
| `obtain_license_token(*, client_id, client_secret, resource_url, ...)` | Acquire a license token as a crawler client                     |
| `verify_license_token(token, request_url, supertab_base_url, ...)`     | Lower-level token verification with typed valid/invalid results |
| `SupertabConnect.reset_instance()`                                     | Clear the singleton, allowing fresh client initialization       |
| `SupertabConnect.set_base_url(url)`                                    | Override the default Supertab Connect API base URL              |

### Instance Methods

| Method                                                       | Description                                                                                            |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `handle_request(request)`                                    | Handle a request end-to-end: token extraction, verification, bot detection, enforcement, and analytics |
| `verify_and_record(*, token, resource_url, user_agent, ...)` | Verify a token and record a usage event                                                                |
| `aclose()`                                                   | Close SDK-managed async HTTP clients                                                                   |
