# Web SDK

Drop two `<script>` tags into your HTML, call `AiPass.initialize()`, and you have OAuth2 + PKCE auth, AI APIs, balance tracking, and a ready-to-use auth button. The whole integration is on this page.

> Building for mobile, CLI, or server-side instead? See [REST API](/docs/rest).
>
> Building with an AI agent (Claude Code, Cursor, etc.)? `npx skills add aipass-one/skill --skill aipass-oauth-app` installs the canonical agent skill.

## Contents

1. [Setup](#1-setup)
2. [Authentication](#2-authentication)
3. [Models — discover at runtime](#3-models--discover-at-runtime)
4. [Chat completions](#4-chat-completions)
5. [Vision (multimodal)](#5-vision-multimodal)
6. [Image generation](#6-image-generation)
7. [Image editing — single image](#7-image-editing--single-image)
8. [Image editing — multi-image](#8-image-editing--multi-image)
9. [Audio (TTS / STT)](#9-audio-tts--stt)
10. [Embeddings](#10-embeddings)
11. [Balance & usage](#11-balance--usage)
12. [Result handling — `url` vs `b64_json`](#12-result-handling--url-vs-b64_json)
13. [Error handling](#13-error-handling)
14. [Utilities](#14-utilities)
15. [UI Components](#15-ui-components)
16. [Worked example: hair-style try-on app](#16-worked-example-hair-style-try-on-app)

---

## 1. Setup

Add the SDK to your page and initialize it once on load. **`clientId` is required.** The SDK automatically handles the OAuth redirect on the same page — no extra callback code needed.

```html
<!-- Include the SDK -->
<script src="https://aipass.one/aipass-sdk.js"></script>

<script>
  // REQUIRED: provide your OAuth2 clientId
  AiPass.initialize({
    clientId: 'your_client_id',
    // Optional:
    // requireLogin: true,         // gate the whole page behind login
    // redirectUri: '...',         // defaults to current page (same-page flow)
    // baseUrl: 'https://aipass.one',
    // scopes: ['api:access', 'profile:read']
  });
</script>
```

> Use HTTPS in production. HTTP is allowed only on **localhost** for development.

## 2. Authentication

Start OAuth2 + PKCE in a popup, then call SDK methods with the stored token. The SDK handles refresh automatically.

```javascript
// Trigger login from a user action (e.g. button click)
async function login() {
  try {
    await AiPass.login();
    console.log('Signed in');
  } catch (e) {
    console.error('Login failed:', e);
  }
}

// Check status
if (AiPass.isAuthenticated()) {
  console.log('Ready to call APIs');
}

// Get token if needed (for custom calls)
const token = AiPass.getAccessToken();

// Helpers
async function logout()       { await AiPass.logout(); }
async function refreshToken() { await AiPass.refreshAccessToken(); }
```

Listen to auth events to wire up your UI:

```javascript
document.addEventListener('aipass:login',  () => { /* enable features */ });
document.addEventListener('aipass:logout', () => { /* disable features */ });
document.addEventListener('aipass:balance', (e) => console.log('Balance:', e.detail));
document.addEventListener('aipass:error',   (e) => console.error(e.detail.error));
```

## 3. Models — discover at runtime

**Do not hardcode model strings.** Models change. Always list them at runtime and pick by ID convention.

```javascript
// List models
const { data } = await AiPass.getModels();
console.log('Available:', data.length);

// Each entry: { id: '...', object: 'model', created: ..., owned_by: '...' }

// Get details for a specific model
const model = await AiPass.getModel('gpt-5-mini');
```

### Filter by ID convention

| Capability | ID pattern | Examples |
|---|---|---|
| **Chat / text** | OpenAI / Anthropic / Gemini-style | `gpt-5-mini`, `claude-haiku-4-5`, `gemini/gemini-2.5-flash-lite` |
| **Vision** | any chat model that accepts images | (same as chat — pass `content: [{type: 'image_url', ...}]`) |
| **Image generation** | image-provider prefix, no `/edit` | `fal-ai/nano-banana-2`, `imagen4/preview/ultra`, `flux-pro/v1.1-ultra`, `recraft/v3`, `seedream/v3` |
| **Image edit** | ends in `/edit` | `fal-ai/nano-banana-2/edit`, `openai/gpt-image-2/edit`, `fal-ai/nano-banana-pro/edit` |
| **Image upscale** | contains `/upscale/` | `fal-ai/aura-sr`, `fal-ai/topaz/upscale/image`, `fal-ai/recraft/upscale/crisp` |
| **Background removal** | `birefnet` or `ben/v2` | `fal-ai/birefnet/v2`, `fal-ai/ben/v2/image` |
| **TTS** | starts with `tts-` | `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts` |
| **Transcription** | contains `whisper` | `whisper-1` |
| **Embeddings** | starts with `text-embedding-` | `text-embedding-3-small`, `text-embedding-3-large` |
| **Video** | contains `veo` or `sora` | `gemini/veo-3.1-fast-generate-preview`, `openai/sora-2` |

### Recommended picks for common tasks

```javascript
function pickModel(data, task) {
  const ids = data.map(m => m.id);
  const has = (s) => ids.find(id => id.includes(s));

  switch (task) {
    case 'face-preserving-edit':
      return has('nano-banana-2/edit')
          || has('gpt-image-2/edit')
          || has('nano-banana-pro/edit');
    case 'image-edit':
      return has('nano-banana-2/edit') || has('gpt-image-2/edit');
    case 'image-gen':
      return has('imagen4/preview/ultra') || has('flux-pro/v1.1-ultra') || has('nano-banana-2');
    case 'cheap-chat':
      return has('gpt-5-nano') || has('gemini-2.5-flash-lite');
    case 'quality-chat':
      return has('claude-sonnet-4-5') || has('gpt-5.1') || has('gpt-5-mini');
    case 'tts':
      return has('tts-1');
    case 'transcribe':
      return has('whisper-1');
  }
}

const { data } = await AiPass.getModels();
const editModel = pickModel(data, 'face-preserving-edit');
```

## 4. Chat completions

```javascript
// Simple prompt
const reply = await AiPass.generateCompletion({
  prompt: 'Explain async programming',
  model: 'gemini/gemini-2.5-flash-lite',
  temperature: 0.7,
  maxTokens: 500
});
console.log(reply.choices[0].message.content);

// Messages format (recommended for multi-turn)
const chat = await AiPass.generateCompletion({
  messages: [
    { role: 'system', content: 'You are helpful.' },
    { role: 'user', content: 'What are closures?' }
  ]
});
```

> Streaming is currently disabled on the backend — use non-streaming calls.

## 5. Vision (multimodal)

Send an image alongside text in a chat completion:

```javascript
const fileToDataUrl = (file) => new Promise((resolve, reject) => {
  const r = new FileReader();
  r.onload = () => resolve(r.result);
  r.onerror = reject;
  r.readAsDataURL(file);
});

const dataUrl = await fileToDataUrl(fileInput.files[0]);

const result = await AiPass.generateCompletion({
  messages: [{
    role: 'user',
    content: [
      { type: 'text', text: 'What hairstyle is this person wearing?' },
      { type: 'image_url', image_url: { url: dataUrl } }
    ]
  }],
  model: 'gpt-5-mini'
});
```

Compress images to ~800KB before encoding to keep latency down.

## 6. Image generation

```javascript
const { data } = await AiPass.getModels();
const model = pickModel(data, 'image-gen');     // e.g. 'imagen4/preview/ultra'

const result = await AiPass.generateImage({
  prompt: 'A futuristic city at sunset, photorealistic',
  model,
  n: 1,
  size: '1024x1024',
  quality: 'high',
  responseFormat: 'url'      // or 'b64_json'
});

// Always handle BOTH response shapes:
const payload = result.data[0];
const imageUrl = payload.url || `data:image/png;base64,${payload.b64_json}`;
```

## 7. Image editing — single image

The bread-and-butter call for any "transform a photo" app (hair styles, fashion try-on, restyle, etc.).

```javascript
const file = document.getElementById('photo').files[0];

const { data } = await AiPass.getModels();
const model = pickModel(data, 'face-preserving-edit');
//   → resolves to e.g. 'fal-ai/nano-banana-2/edit'
//   (Google identity-preservation, best-in-class for faces)

const result = await AiPass.editImage({
  image: file,
  prompt: 'Change the hairstyle to a sleek bob cut. Preserve the face, lighting, and clothing exactly.',
  model,
  n: 1,
  size: '1024x1024',
  responseFormat: 'url'
});

const payload = result.data[0];
const url = payload.url || `data:image/png;base64,${payload.b64_json}`;
```

### Prompt tips for "swap one attribute"

- Lead with what to change (`"Change the hairstyle to ..."`).
- Explicitly call out what to preserve (`"Preserve the face, lighting, and clothing exactly."`). Without this, models drift.
- Avoid "make it look like X celebrity" — frequently rejected by content filters.

## 8. Image editing — multi-image

`editImage` accepts an **array of files** for models that support multi-image input. Useful for "use this as reference":

```javascript
const target    = document.getElementById('your-photo').files[0];
const reference = document.getElementById('reference').files[0];

const { data } = await AiPass.getModels();
const model = pickModel(data, 'image-edit');

const result = await AiPass.editImage({
  image: [target, reference],         // <-- ARRAY, not a single file
  prompt: 'Apply the hairstyle from the second image to the person in the first image. Preserve the first person\'s face, lighting, and clothing.',
  model,
  size: '1024x1024'
});
```

**Multi-image-capable models** (verify at runtime via `getModels()`):

- `fal-ai/nano-banana-2/edit`
- `fal-ai/nano-banana-pro/edit`
- `openai/gpt-image-2/edit`

## 9. Audio (TTS / STT)

```javascript
// Text → speech (returns Blob)
const audioBlob = await AiPass.generateSpeech({
  text: 'Hello world',
  model: 'tts-1',
  voice: 'nova',                // alloy | echo | fable | onyx | nova | shimmer
  responseFormat: 'mp3',
  speed: 1.0
});
new Audio(URL.createObjectURL(audioBlob)).play();

// Speech → text
const out = await AiPass.transcribeAudio({
  audioFile: fileInput.files[0],
  model: 'whisper-1',
  language: 'en'
});
console.log(out.text);
```

## 10. Embeddings

```javascript
// Single text
const result = await AiPass.generateEmbeddings({
  input: 'Hello world',
  model: 'text-embedding-3-small'
});
console.log(result.data[0].embedding);

// Batch
const batch = await AiPass.generateEmbeddings({
  input: ['First text', 'Second text', 'Third text']
});
```

## 11. Balance & usage

```javascript
const summary = await AiPass.getUserBalance();
console.log('Remaining $', summary.data.remainingBudget);
console.log('Used $',      summary.data.totalCost);
console.log('Limit $',     summary.data.maxBudget);
```

The `<div data-aipass-button>` widget already shows balance automatically.

## 12. Result handling — `url` vs `b64_json`

Different models return image responses in different shapes. **Always handle both** or your app will silently break when a model is swapped:

```javascript
function extractImageUrl(payload) {
  if (payload.url) return payload.url;
  if (payload.b64_json) return `data:image/png;base64,${payload.b64_json}`;
  throw new Error('No image in response');
}

const result = await AiPass.editImage({ /* … */ });
const url = extractImageUrl(result.data[0]);
```

## 13. Error handling

```javascript
try {
  const result = await AiPass.editImage({ /* … */ });
} catch (e) {
  if (e.budgetExceededHandled) {
    // SDK already showed the budget UI; you can just bail.
    return;
  }
  if (/401|unauthor/i.test(e.message)) {
    // Token expired and refresh failed — kick user to re-login.
    await AiPass.login();
    return;
  }
  if (/model.*not found/i.test(e.message)) {
    // You hardcoded a model. Use getModels() instead.
    console.error('Model not available; falling back via getModels()');
  }
  alert('Something went wrong: ' + e.message);
}
```

## 14. Utilities

```javascript
// Open dashboard
AiPass.openDashboard();

// Clear stored tokens
AiPass.clearTokens();
```

## 15. UI Components

AI Pass provides a ready-to-use authentication button and balance widget.

### `<div data-aipass-button>`

A beautiful, animated button that handles authentication and displays user balance. Two states:

- **Disconnected**: dark background with "CONNECT" text
- **Connected**: white background, ripple animation, balance displayed

```html
<!-- 1. Include the UI stylesheet -->
<link rel="stylesheet" href="https://aipass.one/aipass-ui.css">

<!-- 2. Add the button -->
<div data-aipass-button></div>

<!-- 3. Include the SDK and initialize -->
<script src="https://aipass.one/aipass-sdk.js"></script>
<script>
  AiPass.initialize({ clientId: 'your_client_id' });
</script>
```

That's it — the button automatically:

- Handles login/logout on click
- Switches between states
- Fetches and displays balance
- Shows ripple animation when connected
- Is keyboard accessible (Tab, Enter, Space)

### Public API

```javascript
// Refresh balance manually
AiPassUI.refreshBalance();

// Or for a specific button
const button = document.querySelector('[data-aipass-button]');
AiPassUI.refreshBalance(button);

// Re-initialize all buttons (for dynamic content)
AiPassUI.reinit();

// Check if button is connected
const isConnected = AiPassUI.isConnected(button);
```

### Size variants

```html
<div class="logo-container dark">...</div>           <!-- default -->
<div class="logo-container small dark">...</div>     <!-- small -->
<div class="logo-container large dark">...</div>     <!-- large -->
```

---

## 16. Worked example: hair-style try-on app

Drop this in a `.html` file, replace `client_id`, open in a browser. Fully functional ~150-line app: discovers models, accepts a selfie, applies a chosen hair style via image-edit, renders the result.

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hair Studio — AI Pass demo</title>
  <link rel="stylesheet" href="https://aipass.one/aipass-ui.css">
  <style>
    body { font-family: system-ui; max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
    .row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
    .col { flex: 1; min-width: 280px; }
    .styles { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; margin-top: 1rem; }
    .style { padding: .75rem; border: 2px solid #ddd; border-radius: 8px; cursor: pointer; text-align: center; font-size: .9rem; }
    .style.selected { border-color: #6366f1; background: #eef2ff; }
    button { padding: .75rem 1.5rem; background: #6366f1; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; }
    button:disabled { opacity: .5; cursor: not-allowed; }
    img { max-width: 100%; border-radius: 8px; }
    #status { color: #666; font-size: .9rem; margin-top: .5rem; }
  </style>
</head>
<body>
  <header style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
    <h1>💇 Hair Studio</h1>
    <div data-aipass-button></div>
  </header>

  <div class="row">
    <div class="col">
      <h3>1. Upload your photo</h3>
      <input id="photo" type="file" accept="image/*">
      <img id="preview" style="margin-top:.5rem;display:none">

      <h3>2. Pick a style</h3>
      <div class="styles" id="styles"></div>

      <button id="go" disabled style="margin-top:1rem">Try this style</button>
      <div id="status"></div>
    </div>

    <div class="col">
      <h3>Result</h3>
      <img id="result" style="display:none">
    </div>
  </div>

  <script src="https://aipass.one/aipass-sdk.js"></script>
  <script>
    AiPass.initialize({
      clientId: 'YOUR_CLIENT_ID_HERE',
      requireLogin: true
    });

    const STYLES = [
      'sleek bob cut', 'long flowing waves', 'short buzz cut',
      'curly afro', 'straight bangs', 'high ponytail',
      'man bun', 'pixie cut', 'mullet',
      'mohawk', 'dreadlocks', 'shoulder-length wavy'
    ];

    let editModel = null;
    let selectedStyle = null;

    const stylesEl = document.getElementById('styles');
    STYLES.forEach((style) => {
      const div = document.createElement('div');
      div.className = 'style';
      div.textContent = style;
      div.onclick = () => {
        document.querySelectorAll('.style').forEach(s => s.classList.remove('selected'));
        div.classList.add('selected');
        selectedStyle = style;
        updateButton();
      };
      stylesEl.appendChild(div);
    });

    const photoInput = document.getElementById('photo');
    const previewImg = document.getElementById('preview');
    photoInput.onchange = () => {
      if (!photoInput.files[0]) return;
      previewImg.src = URL.createObjectURL(photoInput.files[0]);
      previewImg.style.display = 'block';
      updateButton();
    };

    function updateButton() {
      document.getElementById('go').disabled =
        !photoInput.files[0] || !selectedStyle || !AiPass.isAuthenticated();
    }

    document.addEventListener('aipass:login', async () => {
      const { data } = await AiPass.getModels();
      const ids = data.map(m => m.id);
      editModel = ids.find(id => id.includes('nano-banana-2/edit'))
               || ids.find(id => id.includes('gpt-image-2/edit'))
               || ids.find(id => id.endsWith('/edit'));
      document.getElementById('status').textContent = editModel
        ? `Ready — using ${editModel}`
        : '⚠️ No image-edit model available in your account.';
      updateButton();
    });

    document.addEventListener('aipass:logout', () => updateButton());

    document.getElementById('go').onclick = async () => {
      const status = document.getElementById('status');
      const goBtn = document.getElementById('go');
      goBtn.disabled = true;
      status.textContent = 'Generating…';

      try {
        const result = await AiPass.editImage({
          image: photoInput.files[0],
          prompt: `Change the hairstyle to ${selectedStyle}. Preserve the face, lighting, skin tone, and clothing exactly. Keep all other features identical.`,
          model: editModel,
          n: 1,
          size: '1024x1024',
          responseFormat: 'url'
        });

        const payload = result.data[0];
        const url = payload.url || `data:image/png;base64,${payload.b64_json}`;

        const resultImg = document.getElementById('result');
        resultImg.src = url;
        resultImg.style.display = 'block';
        status.textContent = '✅ Done';
      } catch (e) {
        if (e.budgetExceededHandled) return;
        status.textContent = '❌ ' + (e.message || 'Edit failed');
      } finally {
        goBtn.disabled = false;
      }
    };
  </script>
</body>
</html>
```

This is a complete, drop-in app. Adapt the prompt and gallery for any "swap an attribute on a photo" use case (fashion, makeup, eye color, age, room redecoration, etc.).
