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-appinstalls the canonical agent skill.
Contents
- Setup
- Authentication
- Models — discover at runtime
- Chat completions
- Vision (multimodal)
- Image generation
- Image editing — single image
- Image editing — multi-image
- Audio (TTS / STT)
- Embeddings
- Balance & usage
- Result handling —
urlvsb64_json - Error handling
- Utilities
- UI Components
- 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
| 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
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/editfal-ai/nano-banana-pro/editopenai/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.).