# REST API

Implement OAuth2 + PKCE and call AI Pass endpoints from any stack — Flutter, iOS, Android, desktop, CLI, server. No SDK required.

> Building a browser/web app instead? See the [Web SDK](/docs/sdk) — it handles PKCE, token storage, and the auth button for you.
>
> Managing OAuth clients, payments, gift cards, or apps? See [Management API](/docs/rest/endpoints).
>
> Building with an AI agent (Claude Code, Cursor, etc.)? `npx skills add aipass-one/skill --skill aipass-oauth-app` installs the canonical agent skill.

## TL;DR flow

1. Create an OAuth2 client →
2. Generate PKCE + state →
3. Send user to `/oauth2/authorize` →
4. Exchange code at `/oauth2/token` →
5. Store `access_token` + `refresh_token` →
6. Call `/oauth2/v1/*` with Bearer token

> CORS is already open on `/oauth2/token`, so mobile/web apps can exchange codes directly without a custom backend.

---

## 1) Get a client ID

Register your app and obtain `client_id` (and optional `client_secret`) from the [Developer Dashboard](https://aipass.one/panel/developer) or via REST.

```http
POST /api/v1/oauth2/clients
Content-Type: application/json

{
  "clientName": "My Flutter App",
  "redirectUri": "myapp://auth/callback",
  "requestedScopes": ["api:access", "profile:read"]
}
```

> Save the client secret if you generate one — it's only shown once. Public apps (Flutter, web) typically use PKCE without a client secret.

## 2) Generate PKCE + state

Generate these per login:

- **`code_verifier`**: random 43-128 chars
- **`code_challenge`**: `BASE64URL(SHA256(code_verifier))`
- **`state`**: random string to prevent CSRF

### JavaScript

```javascript
const verifier  = generateRandom(64);
const challenge = await sha256Base64Url(verifier);
const state     = generateRandom(24);
```

### Dart (Flutter)

```dart
final verifier = generateRandomString(64);
final challenge = base64UrlEncode(
  sha256.convert(utf8.encode(verifier)).bytes
).replaceAll('=', '');
final state = generateRandomString(24);
```

### Python

```python
import secrets, hashlib, base64
code_verifier = secrets.token_urlsafe(64)[:64]
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)
```

> Keep `code_verifier` and `state` until the redirect returns so you can validate and exchange the code.

## 3) Launch the OAuth screen

Open the user's browser (or `url_launcher` in Flutter) to:

```
GET https://aipass.one/oauth2/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=YOUR_REDIRECT_URI
  &scope=api:access profile:read
  &state=STATE_VALUE
  &code_challenge=PKCE_CHALLENGE
  &code_challenge_method=S256
```

Use custom schemes like `myapp://auth/callback` on mobile. **Validate the returned `state`** before exchanging the `code`.

## 4) Exchange code & refresh

### Exchange the authorization code

```http
POST https://aipass.one/oauth2/token
Content-Type: application/json

{
  "grantType": "authorization_code",
  "code": "CODE_FROM_REDIRECT",
  "codeVerifier": "ORIGINAL_CODE_VERIFIER",
  "clientId": "YOUR_CLIENT_ID",
  "redirectUri": "YOUR_REDIRECT_URI"
}
```

Response fields: `access_token`, `refresh_token`, `expires_in`, `token_type` (Bearer), `scope`.

### Refresh tokens

```http
POST https://aipass.one/oauth2/token
Content-Type: application/json

{
  "grantType": "refresh_token",
  "refreshToken": "YOUR_REFRESH_TOKEN",
  "clientId": "YOUR_CLIENT_ID"
}
```

> On `401` responses from the API, refresh once, then restart OAuth if the refresh fails too.

## 5) Discover models — don't hardcode

Models change. Always list at runtime:

```bash
curl https://aipass.one/oauth2/v1/models \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  | jq '.data[] | select(.id | endswith("/edit")) | .id'
# Returns the available image-edit model IDs.
```

Filter by ID convention:

| Capability | Pattern |
|---|---|
| Image edit | ends in `/edit` (e.g. `fal-ai/nano-banana-2/edit`) |
| Image gen | image-provider prefix, no `/edit` |
| Chat / text | `gpt-*`, `claude-*`, `gemini/*` |
| TTS | starts with `tts-` |
| Transcription | contains `whisper` |
| Embeddings | starts with `text-embedding-` |

## 6) Make AI calls

Send the Bearer token in every request. The proxy mirrors OpenAI-style payloads.

### Chat completion

```bash
curl -X POST https://aipass.one/oauth2/v1/chat/completions \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "gpt-5-mini",
        "messages": [
          {"role":"system","content":"You answer briefly."},
          {"role":"user","content":"One tip for better focus?"}
        ],
        "max_tokens": 200,
        "stream": false
      }'
```

For streaming, set `"stream": true` and consume Server-Sent Events. Budget errors arrive as JSON; surface them to the user.

### Image generation

```bash
curl -X POST https://aipass.one/oauth2/v1/images/generations \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "imagen4/preview/ultra",
    "prompt": "A futuristic city at sunset",
    "size": "1024x1024",
    "n": 1,
    "response_format": "url"
  }'

# Always check both `url` and `b64_json` — different models return different shapes.
```

### Image editing — single image

```bash
curl -X POST https://aipass.one/oauth2/v1/images/edits \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "image=@selfie.jpg" \
  -F "prompt=Change the hairstyle to a sleek bob cut. Preserve the face and lighting." \
  -F "model=fal-ai/nano-banana-2/edit" \
  -F "size=1024x1024" \
  -F "response_format=url"
```

```python
import requests
with open("selfie.jpg", "rb") as f:
    r = requests.post(
        "https://aipass.one/oauth2/v1/images/edits",
        headers={"Authorization": f"Bearer {access_token}"},
        files={"image": f},
        data={
            "model": "fal-ai/nano-banana-2/edit",
            "prompt": "Change the hairstyle to a sleek bob cut. Preserve the face and lighting.",
            "size": "1024x1024",
            "response_format": "url",
        },
    )
url = r.json()["data"][0].get("url") or f"data:image/png;base64,{r.json()['data'][0]['b64_json']}"
```

### Image editing — multi-image

Pass multiple `-F image=@…` for models that support multi-image input (`fal-ai/nano-banana-2/edit`, `openai/gpt-image-2/edit`, `fal-ai/nano-banana-pro/edit`):

```bash
curl -X POST https://aipass.one/oauth2/v1/images/edits \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "image=@target.jpg" \
  -F "image=@reference.jpg" \
  -F "prompt=Apply the hairstyle from the second image to the person in the first." \
  -F "model=fal-ai/nano-banana-2/edit"
```

The server treats repeated `image` form fields as an array.

## 7) Vision (multimodal)

Same `/oauth2/v1/chat/completions` endpoint, but `content` is an array:

```json
{
  "model": "gpt-5-mini",
  "messages": [{
    "role": "user",
    "content": [
      {"type": "text", "text": "What's in this image?"},
      {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}}
    ]
  }],
  "max_tokens": 2000
}
```

> Compress images to ~800KB before encoding to base64. Vision models support: JPEG, PNG, GIF, WebP.

## 8) Audio + embeddings

```bash
# TTS
curl -X POST https://aipass.one/oauth2/v1/audio/speech \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"Hello world","voice":"nova","response_format":"mp3"}' \
  --output speech.mp3

# Transcribe
curl -X POST https://aipass.one/oauth2/v1/audio/transcriptions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "file=@audio.mp3" \
  -F "model=whisper-1" \
  -F "language=en"

# Embeddings
curl -X POST https://aipass.one/oauth2/v1/embeddings \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"text-embedding-3-small","input":["First text","Second text"]}'
```

## 9) Usage & balance tracking

Show users their remaining budget:

```http
GET https://aipass.one/api/v1/usage/me/summary
Authorization: Bearer ACCESS_TOKEN

// Response:
{
  "success": true,
  "data": {
    "totalCost": 2.45,
    "maxBudget": 10.00,
    "remainingBudget": 7.55
  }
}
```

> Cache balance data and refresh periodically (every 5-10 minutes) to reduce API calls.

## 10) All allowed endpoints

All require `Authorization: Bearer ACCESS_TOKEN` and `api:access` scope. Base URL: `https://aipass.one/oauth2/v1`

| Category | Endpoints |
|---|---|
| Models | `GET /models`, `GET /models/{id}` |
| Chat | `POST /chat/completions` |
| Embeddings | `POST /embeddings` |
| Images | `POST /images/generations`, `POST /images/edits`, `POST /images/variations` |
| Audio | `POST /audio/speech`, `POST /audio/transcriptions` |
| Video | `POST /videos`, `POST /videos/{id}/remix`, `GET /videos/{id}`, `GET /videos/{id}/content` |
| User profile | `GET /oauth2/userinfo` (needs `profile:read`) |
| Usage & Balance | `GET /api/v1/usage/me/summary` |
| Revoke | `POST /oauth2/revoke?token=ACCESS_TOKEN` |

> Only allowlisted endpoints above are proxied. All calls require `api:access` scope.

For management endpoints (OAuth2 clients, payments, gift cards, spaces, apps), see the [Endpoints Reference](/docs/rest/endpoints).
