> This page is for Alfa (Preview).

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.alfa.boosted.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.alfa.boosted.ai/_mcp/server.

# Authorization

---

## Table of Contents

* [Overview](#overview)
* [What You Need to Provide Us](#what-you-need-to-provide-us)
* [What We Will Provide You](#what-we-will-provide-you)
* [What You Need to Implement](#what-you-need-to-implement)

---

## Overview

Token Exchange enables your backend to obtain Boosted access tokens for your users without browser redirects or user interaction. This method is designed for backend-to-backend integrations where you embed Boosted features directly in your application.

**Authentication Flow:**

1. User is authenticated in your application
2. User requests access to Boosted features
   1. If it is their first time, perform token exchange with `sub` set to your client ID to obtain a service account token
   2. Use the management token to create the account hierarchy (Organization → Workspace → User)
3. Your backend creates a signed JWT asserting the user's identity
4. Your backend sends JWT to Boosted's token endpoint
5. Boosted validates JWT signature and returns access tokens
6. Your backend uses access tokens to call Boosted APIs on user's behalf

**No user redirects, login screens, or consent flow.**

**Standards:**

* [RFC 8693 - OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693.html)
* [RFC 7523 - JWT Profile for OAuth 2.0](https://www.rfc-editor.org/rfc/rfc7523.html)
* [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)

---

## What You Need to Provide Us

### 1. Public Signing Key URL (`jwks_uri`)

Provide the public URL where your JWKS (JSON Web Key Set) is hosted. We will fetch your public keys from this endpoint to verify signatures.

**How to generate and host:**

1. **Generate Keys:**
   ```bash
   # Generate private key (keep this secret)
   openssl genrsa -out private_key.pem 2048

   # Extract public key
   openssl rsa -in private_key.pem -pubout -out public_key.pem
   ```
2. **Convert & Host:**
   * Convert the public key to JWKS format (JSON).
   * Upload the JSON file to a publicly accessible location (e.g., AWS S3, a static site, or a dedicated API endpoint).

**Example URL:**
`https://your-domain.com/.well-known/jwks.json`

**Expected Response (at the URL above):**

```json
{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "your-key-id-1",
      "n": "oO2Re_MzrCqR-1rLFcmuZqf2kYmqjWQax1gYo-cWIGEDX_UIvQ4FtDRx53fPfbl...",
      "e": "AQAB"
    }
  ]
}
```

---

## What We Will Provide You

### 1. Client ID

```
your_client_id_abc123
```

Used for:

* Token exchange request body
* JWT `iss` claim (issuer)
* JWT `sub` claim (for token exchange to use the [account-management API](/alfa-preview/account-management-api/reference/get/organization))

### 2. Client Secret

```
your_client_secret_xyz789
```

Used for:

* Token exchange request body

### 3. Token Endpoint

```
https://sandbox.api.boosted.ai/oauth2/token
```

Send your token exchange requests here.

### 4. Account Management API Endpoints

```
https://sandbox.api.boosted.ai/v2/account-management/org
https://sandbox.api.boosted.ai/v2/account-management/workspace
https://sandbox.api.boosted.ai/v2/account-management/user
```

Create the organizational hierarchy via these endpoints before performing token exchange for users.

### 5. Audience Value

```
https://sandbox.api.boosted.ai
```

Include this in the `aud` claim of your JWT.

### 6. JWT Requirements

Your JWT must include these standard claims:

```json
{
  "iss": "your_client_id_abc123",                    // Issuer: Your client ID (who signed this)
  "sub": "user_id_from_api",                         // Subject: Client ID or user identifier (we returned this when you created the user)
  "aud": "https://sandbox.api.boosted.ai",           // Audience: Boosted's server
  "exp": 1234567890,                                 // Expiration: Time issued + 300 seconds (5 min max)
  "jti": "unique-id-12345"                           // JWT ID: Unique for replay prevention
}
```

### 7. Available Scopes

You can request specific scopes in your token exchange request. Your partner account will be configured with allowed scopes.

```
account-management    - Access to management APIs (org, workspace, user creation)
thread                - Access to threads API
analytics             - Access to analytics API
```

---

## What You Need to Implement

### 1. Account Setup Flow

The Threads API requires an access token for a user with the `thread` scope. Before you can generate these tokens, you must set up your accounts using the Account Management API.

**Account Hierarchy**

The Account Management API uses a three-tier hierarchy:

* **Organization**: Your top-level entity
* **Workspace**: Groups of users within an organization
* **User**: Individual accounts that can access the Threads API

**Choosing Your Structure**

For most use cases, we recommend starting with a simple structure:

* 1 Organization
* 1 Workspace
* All users in that single workspace

> **Note**
>
> This approach works well when you don't need to segment users into different groups. If you have more complex requirements (e.g., separating internal production use from end user production use, or isolating different end user groups), contact us for guidance on structuring your hierarchy.

#### Step 1: Get Service Account Token

Perform token exchange with your client ID as the subject to obtain a management token:

**`get_service_token.py`**

```python title="get_service_token.py"
import time
import uuid

import jwt
import requests
from cryptography.hazmat.primitives import serialization

KID: str = "your-key-id-1"
CLIENT_ID: str = "your_client_id_abc123"
CLIENT_SECRET: str = "your_client_secret_xyz789"
PRIVATE_KEY: str = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"

BASE_URL: str = "https://sandbox.api.boosted.ai"

key_bytes: bytes = PRIVATE_KEY.encode("utf-8")
private_key = serialization.load_pem_private_key(key_bytes, password=None)

payload: dict = {
    "iss": CLIENT_ID,
    "sub": CLIENT_ID,  # Use client_id for service account
    "aud": "https://sandbox.api.boosted.ai",
    "exp": int(time.time()) + 300,
    "jti": str(uuid.uuid4()),
}

subject_token: str = jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": KID})

token_resp: requests.Response = requests.post(
    f"{BASE_URL}/oauth2/token",
    data={
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "subject_token": subject_token,
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
        "scope": "account-management",
    },
)

service_token: str = token_resp.json().get("access_token")
```

**`get_service_token.sh`**

```bash title="get_service_token.sh"
#!/bin/bash
KID="your-key-id-1"
CLIENT_ID="your_client_id_abc123"
CLIENT_SECRET="your_client_secret_xyz789"
PRIVATE_KEY=$'-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'

BASE_URL="https://sandbox.api.boosted.ai"

base64url_encode() {
    openssl base64 -e -A | tr '+/' '-_' | tr -d '='
}

create_jwt() {
    local sub="$1"
    local now=$(date +%s)
    local exp=$((now + 300))
    local jti=$(uuidgen | tr '[:upper:]' '[:lower:]')

    local header=$(echo -n "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"kid\":\"${KID}\"}" | base64url_encode)
    local payload=$(echo -n "{\"iss\":\"${CLIENT_ID}\",\"sub\":\"${sub}\",\"aud\":\"https://sandbox.api.boosted.ai\",\"exp\":${exp},\"jti\":\"${jti}\"}" | base64url_encode)
    local signature=$(echo -n "${header}.${payload}" | openssl dgst -sha256 -sign <(echo "$PRIVATE_KEY") | base64url_encode)

    echo "${header}.${payload}.${signature}"
}

SERVICE_JWT=$(create_jwt "$CLIENT_ID")

SERVICE_TOKEN=$(curl -s -X POST "${BASE_URL}/oauth2/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=${CLIENT_ID}" \
    -d "client_secret=${CLIENT_SECRET}" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    -d "subject_token=${SERVICE_JWT}" \
    -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
    -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
    -d "scope=account-management" | jq -r '.access_token')
```

> **Tip**
>
> The bash snippets use process substitution `<(...)` which requires bash. Add `#!/bin/bash` at the top of your script or run with `bash script.sh`.

#### Step 2: Create Organization

**`create_org.py`**

```python title="create_org.py"
org_payload: dict = {"name": "Test Org"}
org_resp: dict = make_request("PUT", "/v2/account-management/org", service_token, org_payload)
org_id: str = org_resp["org"]["org_id"]
```

**`create_org.sh`**

```bash title="create_org.sh"
ORG_RESPONSE=$(curl -s -X PUT "${BASE_URL}/v2/account-management/org" \
    -H "Authorization: Bearer ${SERVICE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Test Org"
    }')

ORG_ID=$(echo "$ORG_RESPONSE" | jq -r '.org.org_id')
```

#### Step 3: Create Workspace

**`create_workspace.py`**

```python title="create_workspace.py"
workspace_payload: dict = {"org_id": org_id, "name": "Test Workspace"}
ws_resp: dict = make_request("PUT", "/v2/account-management/workspace", service_token, workspace_payload)
workspace_id: str = ws_resp["workspace"]["workspace_id"]
```

**`create_workspace.sh`**

```bash title="create_workspace.sh"
WORKSPACE_RESPONSE=$(curl -s -X PUT "${BASE_URL}/v2/account-management/workspace" \
    -H "Authorization: Bearer ${SERVICE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
        \"org_id\": \"${ORG_ID}\",
        \"name\": \"Test Workspace\"
    }")

WORKSPACE_ID=$(echo "$WORKSPACE_RESPONSE" | jq -r '.workspace.workspace_id')
```

#### Step 4: Create User

**`create_user.py`**

```python title="create_user.py"
user_payload: dict = {"workspace_id": workspace_id, "name": "Test User"}
user_resp: dict = make_request("PUT", "/v2/account-management/user", service_token, user_payload)
user_id: str = user_resp["user"]["user_id"]

# Store this user_id in your database
# It will be used in the 'sub' claim when performing token exchange
```

**`create_user.sh`**

```bash title="create_user.sh"
USER_RESPONSE=$(curl -s -X PUT "${BASE_URL}/v2/account-management/user" \
    -H "Authorization: Bearer ${SERVICE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
        \"workspace_id\": \"${WORKSPACE_ID}\",
        \"name\": \"Test User\"
    }")

USER_ID=$(echo "$USER_RESPONSE" | jq -r '.user.user_id')
```

### 2. User Token Exchange

When a user needs Boosted access, create a JWT with their User ID as the subject and exchange it for an access token:

**`get_user_token.py`**

```python title="get_user_token.py"
payload: dict = {
    "iss": CLIENT_ID,
    "sub": user_id,  # User ID from create user response
    "aud": "https://sandbox.api.boosted.ai",
    "exp": int(time.time()) + 300,
    "jti": str(uuid.uuid4()),
}

subject_token: str = jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": KID})

token_resp: requests.Response = requests.post(
    f"{BASE_URL}/oauth2/token",
    data={
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "subject_token": subject_token,
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
        "scope": "thread",
    },
)

user_token: str = token_resp.json().get("access_token")
```

**`get_user_token.sh`**

```bash title="get_user_token.sh"
USER_JWT=$(create_jwt "$USER_ID")

USER_TOKEN=$(curl -s -X POST "${BASE_URL}/oauth2/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=${CLIENT_ID}" \
    -d "client_secret=${CLIENT_SECRET}" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    -d "subject_token=${USER_JWT}" \
    -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
    -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
    -d "scope=thread" | jq -r '.access_token')
```

**Example Response:**

```json
{
  "access_token": "aeea600e-961d-4d2f-bd50-c2eaf6609552",
  "token_type": "Bearer",
  "expires_in": 3600,
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "scope": "thread"
}
```

### 3. Using the Threads API

Use the access token to call Boosted APIs:

**`threads_example.py`**

```python title="threads_example.py"
# Create a thread
threads_payload: dict = {"thread_name": "Test thread"}
threads_resp: dict = make_request("POST", "/v2/threads", user_token, threads_payload)
thread_id: str = threads_resp["thread_id"]

# List threads
threads_list: dict = make_request("GET", "/v2/threads", user_token)

# Send a message
message_resp: dict = make_request(
    "POST",
    f"/v2/threads/{thread_id}/messages",
    user_token,
    {"message": "Hello from Python client"},
    use_form_urlencoded=True,
)
```

**`threads_example.sh`**

```bash title="threads_example.sh"
# Create a thread
THREAD_RESPONSE=$(curl -s -X POST "${BASE_URL}/v2/threads" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
        "thread_name": "Test Thread"
    }')

THREAD_ID=$(echo "$THREAD_RESPONSE" | jq -r '.thread_id')

# List threads
curl -s -X GET "${BASE_URL}/v2/threads" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -H "Content-Type: application/json" | jq .

# Send a message
curl -s -X POST "${BASE_URL}/v2/threads/${THREAD_ID}/messages" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "message=Hello from bash client" | jq .
```

### 4. Complete Integration Example

**`full_integration.py`**

```python title="full_integration.py"
import time
import uuid

import jwt
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes

# =============================================================================
# CONFIGURATION - Set these values before running
# =============================================================================

KID: str = "SET - KID HERE"
CLIENT_ID: str = "SET - CLIENT ID HERE"
CLIENT_SECRET: str = "SET - CLIENT SECRET HERE"
PRIVATE_KEY: str = "SET - PRIVATE KEY HERE"

BASE_URL: str = "https://sandbox.api.boosted.ai"


# =============================================================================
# API UTILITIES
# =============================================================================


def make_request(
    method: str,
    endpoint: str,
    token: str,
    data: dict | None = None,
    use_form_urlencoded: bool = False,
) -> dict:
    """
    Make an authenticated HTTP request to the Boosted API.

    Args:
        method: HTTP method (GET, POST, PUT, etc.)
        endpoint: API endpoint path
        token: Bearer token for authorization
        data: Request payload
        use_form_urlencoded: If True, send as form data instead of JSON

    Returns:
        Parsed JSON response as a dictionary

    Raises:
        Exception: If the API request fails
    """
    url: str = f"{BASE_URL}{endpoint}"
    headers: dict = {
        "Content-Type": "application/json"
        if not use_form_urlencoded
        else "application/x-www-form-urlencoded",
        "Authorization": f"Bearer {token}",
    }

    print(f"\n[{method}] {endpoint}")
    response: requests.Response
    if use_form_urlencoded:
        response = requests.request(method, url, data=data, headers=headers)
    else:
        response = requests.request(method, url, json=data, headers=headers)

    if response.status_code in [201, 200]:
        print(f"Success ({response.status_code})")
        return response.json()
    else:
        print(f"Failed ({response.status_code})")
        print(response.text)
        raise Exception(f"API Request failed: [{method}] {endpoint}")


def get_auth_token(user: str, scope: str, pkey: PrivateKeyTypes) -> str:
    """
    Exchange a JWT for an access token via OAuth2 token exchange.

    Args:
        user: User ID or client ID to generate token for
        scope: OAuth scope (e.g., "account-management", "thread")
        pkey: RSA private key for signing the JWT

    Returns:
        Access token string
    """
    payload: dict = {
        "iss": CLIENT_ID,
        "sub": user,
        "aud": "https://sandbox.api.boosted.ai",
        "exp": int(time.time()) + 300,
        "jti": str(uuid.uuid4()),
    }

    subject_token: str = jwt.encode(payload, pkey, algorithm="RS256", headers={"kid": KID})

    print("\nExchanging token...")
    token_resp: requests.Response = requests.post(
        f"{BASE_URL}/oauth2/token",
        data={
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "subject_token": subject_token,
            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "scope": scope,
        },
    )

    if token_resp.status_code != 200:
        print(f"Token Exchange Failed: {token_resp.text}")
        exit(1)

    token: str = token_resp.json().get("access_token")
    print(f"Auth Token Acquired: {token}")
    return token


# =============================================================================
# ACCOUNT MANAGEMENT
# =============================================================================


def create_user(token: str) -> str:
    """
    Create a new organization, workspace, and user.

    Args:
        token: Service account token with account-management scope

    Returns:
        The created user's ID
    """
    org_payload: dict = {"name": "Test Org"}
    org_resp: dict = make_request("PUT", "/v2/account-management/org", token, org_payload)
    print(org_resp)
    org_id: str = org_resp["org"]["org_id"]
    print(f"Created Org ID: {org_id}")

    workspace_payload: dict = {"org_id": org_id, "name": "Test Workspace"}
    ws_resp: dict = make_request(
        "PUT", "/v2/account-management/workspace", token, workspace_payload
    )
    print(ws_resp)
    workspace_id: str = ws_resp["workspace"]["workspace_id"]
    print(f"Created Workspace ID: {workspace_id}")

    user_payload: dict = {"workspace_id": workspace_id, "name": "Test User"}
    user_resp: dict = make_request("PUT", "/v2/account-management/user", token, user_payload)
    print(user_resp)
    user_id: str = user_resp["user"]["user_id"]
    print(f"Created User ID: {user_id}")

    return user_id


# =============================================================================
# THREAD OPERATIONS
# =============================================================================


def test_thread_operations(token: str) -> None:
    """
    Test thread API operations: create, list, and send message.

    Args:
        token: User token with thread scope
    """
    threads_payload: dict = {"thread_name": "Test thread"}
    threads_resp: dict = make_request("POST", "/v2/threads", token, threads_payload)
    print(threads_resp)

    threads_list: dict = make_request("GET", "/v2/threads", token)
    print(threads_list)

    thread_id: str = threads_resp["thread_id"]
    message_resp: dict = make_request(
        "POST",
        f"/v2/threads/{thread_id}/messages",
        token,
        {"message": "string"},
        True,
    )
    print(message_resp)


# =============================================================================
# MAIN
# =============================================================================

if __name__ == "__main__":
    key_bytes: bytes = PRIVATE_KEY.encode("utf-8")
    private_key: PrivateKeyTypes = serialization.load_pem_private_key(key_bytes, password=None)

    print("Making Service Token")
    service_token: str = get_auth_token(CLIENT_ID, "account-management", private_key)

    user_id: str = create_user(service_token)

    print("Making User Token")
    user_token: str = get_auth_token(user_id, "thread", private_key)

    test_thread_operations(user_token)
```

**`full_integration.sh`**

```bash title="full_integration.sh"
#!/bin/bash

# =============================================================================
# CONFIGURATION - Set these values before running
# =============================================================================

KID="SET - KID HERE"
CLIENT_ID="SET - CLIENT ID HERE"
CLIENT_SECRET="SET - CLIENT SECRET HERE"
PRIVATE_KEY=$'SET - PRIVATE KEY HERE'


BASE_URL="https://sandbox.api.boosted.ai"

# =============================================================================
# JWT UTILITIES
# =============================================================================

base64url_encode() {
    openssl base64 -e -A | tr '+/' '-_' | tr -d '='
}

create_jwt() {
    local sub="$1"
    local now=$(date +%s)
    local exp=$((now + 300))
    local jti=$(uuidgen | tr '[:upper:]' '[:lower:]')

    local header=$(echo -n "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"kid\":\"${KID}\"}" | base64url_encode)
    local payload=$(echo -n "{\"iss\":\"${CLIENT_ID}\",\"sub\":\"${sub}\",\"aud\":\"https://sandbox.api.boosted.ai\",\"exp\":${exp},\"jti\":\"${jti}\"}" | base64url_encode)
    local signature=$(echo -n "${header}.${payload}" | openssl dgst -sha256 -sign <(echo "$PRIVATE_KEY") | base64url_encode)

    echo "${header}.${payload}.${signature}"
}

# =============================================================================
# 1. GET SERVICE ACCOUNT TOKEN
# =============================================================================

echo "=== Getting Service Account Token ==="

SERVICE_JWT=$(create_jwt "$CLIENT_ID")

SERVICE_TOKEN=$(curl -s -X POST "${BASE_URL}/oauth2/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=${CLIENT_ID}" \
    -d "client_secret=${CLIENT_SECRET}" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    -d "subject_token=${SERVICE_JWT}" \
    -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
    -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
    -d "scope=account-management" | jq -r '.access_token')

echo "Service Token: ${SERVICE_TOKEN}"

# =============================================================================
# 2. CREATE ORGANIZATION
# =============================================================================

echo ""
echo "=== Creating Organization ==="

ORG_RESPONSE=$(curl -s -X PUT "${BASE_URL}/v2/account-management/org" \
    -H "Authorization: Bearer ${SERVICE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Test Org"
    }')

echo "$ORG_RESPONSE" | jq .
ORG_ID=$(echo "$ORG_RESPONSE" | jq -r '.org.org_id')
echo "Org ID: ${ORG_ID}"

# =============================================================================
# 3. CREATE WORKSPACE
# =============================================================================

echo ""
echo "=== Creating Workspace ==="

WORKSPACE_RESPONSE=$(curl -s -X PUT "${BASE_URL}/v2/account-management/workspace" \
    -H "Authorization: Bearer ${SERVICE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
        \"org_id\": \"${ORG_ID}\",
        \"name\": \"Test Workspace\"
    }")

echo "$WORKSPACE_RESPONSE" | jq .
WORKSPACE_ID=$(echo "$WORKSPACE_RESPONSE" | jq -r '.workspace.workspace_id')
echo "Workspace ID: ${WORKSPACE_ID}"

# =============================================================================
# 4. CREATE USER
# =============================================================================

echo ""
echo "=== Creating User ==="

USER_RESPONSE=$(curl -s -X PUT "${BASE_URL}/v2/account-management/user" \
    -H "Authorization: Bearer ${SERVICE_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
        \"workspace_id\": \"${WORKSPACE_ID}\",
        \"name\": \"Test User\"
    }")

echo "$USER_RESPONSE" | jq .
USER_ID=$(echo "$USER_RESPONSE" | jq -r '.user.user_id')
echo "User ID: ${USER_ID}"

# =============================================================================
# 5. GET USER TOKEN
# =============================================================================

echo ""
echo "=== Getting User Token ==="

USER_JWT=$(create_jwt "$USER_ID")

USER_TOKEN=$(curl -s -X POST "${BASE_URL}/oauth2/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=${CLIENT_ID}" \
    -d "client_secret=${CLIENT_SECRET}" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    -d "subject_token=${USER_JWT}" \
    -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
    -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
    -d "scope=thread" | jq -r '.access_token')

echo "User Token: ${USER_TOKEN}"

# =============================================================================
# 6. CREATE THREAD
# =============================================================================

echo ""
echo "=== Creating Thread ==="

THREAD_RESPONSE=$(curl -s -X POST "${BASE_URL}/v2/threads" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
        "thread_name": "Test Thread"
    }')

echo "$THREAD_RESPONSE" | jq .
THREAD_ID=$(echo "$THREAD_RESPONSE" | jq -r '.thread_id')
echo "Thread ID: ${THREAD_ID}"

# =============================================================================
# 7. LIST THREADS
# =============================================================================

echo ""
echo "=== Listing Threads ==="

curl -s -X GET "${BASE_URL}/v2/threads" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -H "Content-Type: application/json" | jq .

# =============================================================================
# 8. SEND MESSAGE
# =============================================================================

echo ""
echo "=== Sending Message ==="

curl -s -X POST "${BASE_URL}/v2/threads/${THREAD_ID}/messages" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "message=Hello from bash client" | jq .

echo ""
echo "=== All tests completed! ==="
```

### 5. Token Expiration Handling

Access tokens expire after 1 hour. When expired, perform another token exchange:

```python
def get_boosted_token_with_cache(user_id: str, scope: str, private_key) -> str:
    """Get token with expiry check

    Args:
        user_id: The User ID from create user API response
        scope: OAuth scope to request
        private_key: RSA private key for signing
    """

    cache_key = f"boosted_token:{user_id}:{scope}"

    # Check cache
    cached = cache.get(cache_key)
    if cached and cached['expires_at'] > time.time():
        return cached['token']

    # Token expired or not cached - perform exchange
    access_token = get_auth_token(user_id, scope, private_key)

    # Cache with 55 min TTL (5 min buffer before actual expiry)
    cache.set(cache_key, {
        'token': access_token,
        'expires_at': time.time() + 3300  # 55 minutes
    })

    return access_token
```

> **Note**
>
> Token Exchange does not return refresh tokens. When the access token expires, create a new JWT and perform another token exchange.