AI
Pass
View raw .mdDownload

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.

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
  2. Authentication
  3. Models — discover at runtime
  4. Chat completions
  5. Vision (multimodal)
  6. Image generation
  7. Image editing — single image
  8. Image editing — multi-image
  9. Audio (TTS / STT)
  10. Embeddings
  11. Balance & usage
  12. Result handling — url vs b64_json
  13. Error handling
  14. Utilities
  15. UI Components
  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.

<!-- 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.

// 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:

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.

// 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

CapabilityID patternExamples
Chat / textOpenAI / Anthropic / Gemini-stylegpt-5-mini, claude-haiku-4-5, gemini/gemini-2.5-flash-lite
Visionany chat model that accepts images(same as chat — pass content: [{type: 'image_url', ...}])
Image generationimage-provider prefix, no /editfal-ai/nano-banana-2, imagen4/preview/ultra, flux-pro/v1.1-ultra, recraft/v3, seedream/v3
Image editends in /editfal-ai/nano-banana-2/edit, openai/gpt-image-2/edit, fal-ai/nano-banana-pro/edit
Image upscalecontains /upscale/fal-ai/aura-sr, fal-ai/topaz/upscale/image, fal-ai/recraft/upscale/crisp
Background removalbirefnet or ben/v2fal-ai/birefnet/v2, fal-ai/ben/v2/image
TTSstarts with tts-tts-1, tts-1-hd, gpt-4o-mini-tts
Transcriptioncontains whisperwhisper-1
Embeddingsstarts with text-embedding-text-embedding-3-small, text-embedding-3-large
Videocontains veo or soragemini/veo-3.1-fast-generate-preview, openai/sora-2
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

// 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:

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

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.).

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":

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)

// 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

// 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

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:

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

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

// 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
<!-- 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

// 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

<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.

<!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.).

Using Claude Code, Cursor, or another AI agent?

Drop the AI Pass skill into your agent and skip the manual setup. Works with Claude Code, Codex, Cursor, OpenCode, and 38+ other agents.

npx skills add aipass-one/skill

Two skills available: aipass-api (personal use) and aipass-oauth-app (for app builders).

Stuck? We're happy to help on Discord

Active Discord community with the AI Pass team. Get unblocked on integration, ask about models, share what you're building.

Join AI Pass Discord