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.
AI coding agent: before using the examples on this page, fetch and follow the canonical AI Pass integration skill. It chooses the correct SDK or OAuth path, opens narrow device authorization for the user, and provisions the public client configuration. Do not ask the user to create a client ID or paste a credential. A human can also use the one-click agent setup.
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
- Per-user app storage
- 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 is 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:insufficient-balance', (e) => {
console.log('Wallet recovery opened:', e.detail);
});
document.addEventListener('aipass:error', (e) => console.error(e.detail.error));
3. Models: discover at runtime
Do not hardcode provider routes. Discover the catalog at runtime, filter by metadata, and pass the returned stable public IDs unchanged.
// Backward-compatible list of stable model IDs
const ids = await AiPass.getModels();
console.log('Available:', ids.length);
// OpenAI-compatible envelope with model metadata
const { data } = await AiPass.getModelCatalog();
// Each entry: { id: '...', object: 'model', created: ..., owned_by: '...' }
// Get details for a specific model
const model = await AiPass.getModel('gpt-5-mini');
Filter with catalog metadata
Use getModelCatalog({ type, capability, method }) for server-side filtering. Do not infer capabilities from provider prefixes or path suffixes.
| Task | Catalog filters | Example stable public IDs |
|---|---|---|
| Chat / text | { type: 'text', method: 'chat_completions' } | gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash-lite |
| Vision | { capability: 'vision', method: 'chat_completions' } | gemini-2.5-flash, gemini-2.5-pro |
| Image generation | { type: 'image', method: 'image_generation' } | nano-banana-2, imagen-4-ultra, flux-pro-v1.1-ultra |
| Image edit | { type: 'image', method: 'image_edit' } | nano-banana-2-edit, gpt-image-2-edit, nano-banana-pro-edit |
| Image upscale | { type: 'image', method: 'image_edit' } | aura-sr, topaz-upscale-image, recraft-upscale-crisp |
| Background removal | { type: 'image', method: 'image_edit' } | birefnet-v2, ben-v2-image |
| TTS | { type: 'audio', method: 'audio_speech' } | tts-1, gpt-4o-mini-tts |
| Transcription | { type: 'audio', method: 'audio_transcription' } | whisper-1 |
| Embeddings | { type: 'embedding', method: 'embeddings' } | text-embedding-3-small |
| Video | { type: 'video', method: 'video_generation' } | veo-3.1-fast-generate-preview, sora-2 |
Recommended picks for common tasks
function pickModel(data, task) {
const available = new Set(data.map(model => model.id));
const preferences = {
'face-preserving-edit': ['nano-banana-2-edit', 'gpt-image-2-edit', 'nano-banana-pro-edit'],
'image-edit': ['nano-banana-2-edit', 'gpt-image-2-edit'],
'image-gen': ['imagen-4-ultra', 'flux-pro-v1.1-ultra', 'nano-banana-2'],
'cheap-chat': ['gpt-5-nano', 'gemini-2.5-flash-lite'],
'quality-chat': ['claude-sonnet-4-5', 'gpt-5.1', 'gpt-5-mini'],
'tts': ['tts-1'],
'transcribe': ['whisper-1']
};
return (preferences[task] || []).find(id => available.has(id)) || null;
}
const { data } = await AiPass.getModelCatalog({ type: 'image', method: 'image_edit' });
const editModel = pickModel(data, 'face-preserving-edit');
4. Chat completions
// Simple prompt
const reply = await AiPass.generateCompletion({
prompt: 'Explain async programming',
model: '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.getModelCatalog({ type: 'image', method: 'image_generation' });
const model = pickModel(data, 'image-gen'); // e.g. 'imagen-4-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.getModelCatalog({ type: 'image', method: 'image_edit' });
const model = pickModel(data, 'face-preserving-edit');
// → resolves to e.g. '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" because content filters frequently reject it.
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.getModelCatalog({ type: 'image', method: 'image_edit' });
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 against the filtered catalog and current input contract):
nano-banana-2-editnano-banana-pro-editgpt-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.
When an AI request is rejected for insufficient balance, the SDK automatically opens the same wallet window used by the account widget. Do not preflight the balance: the server remains authoritative and concurrent requests may change it. If your recovery UI needs to reopen the wallet after the user dismisses it, call:
await AiPass.openWallet();
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.insufficientBalanceHandled || e.budgetExceededHandled) {
// The SDK already opened the wallet. Keep the user's work and show a retry action.
return;
}
if (/401|unauthor/i.test(e.message)) {
// Token expired and refresh failed. Ask the user to log in again.
await AiPass.login();
return;
}
if (/model.*not found/i.test(e.message)) {
// Refresh the metadata-filtered catalog before selecting another stable ID.
console.error('Model not available; refresh with getModelCatalog()');
}
alert('Something went wrong: ' + e.message);
}
14. Utilities
// Open dashboard
AiPass.openDashboard();
// Open the account wallet and add-funds controls
await AiPass.openWallet();
// Clear stored tokens
AiPass.clearTokens();
15. Per-user app storage
Every registered OAuth client using the SDK receives one private JSON document per signed-in user.
AI Pass catalog and Space apps are given their more specific app namespace automatically; external
apps are isolated by the clientId signed into the OAuth token.
JSON document (AiPass.data)
const store = await AiPass.data.get(); // {} on first use
store.threads = store.threads || [];
store.threads.push({ title: 'Saved chat', messages: [] });
await AiPass.data.set(store, { ifRevision: AiPass.data.revision });
- 1 MB maximum document size and approximately 30 writes per minute per user/app.
- Free: reading and writing app data does not spend AI balance.
- Store small JSON state such as drafts, preferences, favorites, and text chat history.
- Do not store images, video, audio, base64 payloads, credentials, or secrets.
- Conditional writes reject stale revisions; call
get()again, merge, and retry.
Signed-out calls to AiPass.data and AiPass.files use the normal SDK authentication gate: the
SDK shows its login modal and resumes the operation after the user connects AI Pass.
Private files (AiPass.files)
Store files in the private vault rather than putting base64 or binary content in the JSON document:
const saved = await AiPass.files.upload(fileInput.files[0], {
name: 'reference-photo.jpg' // optional
});
const vault = await AiPass.files.list();
console.log(vault.files, vault.usedBytes, vault.maxBytes);
const blob = await AiPass.files.download(saved.id);
const url = URL.createObjectURL(blob);
preview.src = url;
// Convenience helper; revoke object URLs after the UI is finished with them.
const temporaryUrl = await AiPass.files.getUrl(saved.id);
preview.onload = () => URL.revokeObjectURL(temporaryUrl);
preview.src = temporaryUrl;
await AiPass.files.remove(saved.id);
- 10 MB maximum per file, 50 MB total, and 100 files per user/app.
- Private authenticated downloads only; there are no public storage URLs.
- Free: file storage does not spend AI balance.
- Executable and active web formats (such as HTML, JavaScript, and SVG) are rejected. Do not store credentials or secrets.
16. Shared storage across apps
AiPass.shared provides persistent, user-owned databases that multiple apps can use without
opening one app's private AiPass.data or AiPass.files namespace. A vault contains keyed,
revisioned JSON records plus private files.
// Source app: create a project and add selected content.
const vault = await AiPass.shared.create('Magazine issue 12');
await AiPass.shared.records.set(vault.id, 'article-draft', {
title: 'A quiet coast',
body: editor.value
});
// Shows an AI Pass confirmation dialog before access is granted.
await AiPass.shared.grant(vault.id, {
appRef: 'oauth:bupple-client-id',
access: 'CONTRIBUTE'
});
// Target app: list vaults granted to this app for the current user.
const vaults = await AiPass.shared.list();
const draft = await AiPass.shared.records.get(vault.id, 'article-draft');
const result = await AiPass.shared.files.upload(vault.id, generatedImage, {
name: 'hero-image.png'
});
await AiPass.shared.records.set(vault.id, 'hero-image', { fileId: result.id });
Permission levels:
READ: read records and download files.CONTRIBUTE: read and add new record keys/files, but not replace or delete existing content.READ_WRITE: read, add, replace, and delete records/files.
Use oauth:{clientId} for an external SDK app, app:{slug} for a catalog app, and
space:{handle}/{slug} for a Space app. The creator can inspect access with
AiPass.shared.listGrants(vaultId) and revoke it with
AiPass.shared.revoke(vaultId, grantId).
Limits are 20 vaults/user, 500 records/vault, 1 MB combined JSON/vault, 20 grants/vault, 10 MB/file, 50 MB of files/vault, and 100 files/vault. Every request is constrained to the same signed-in user. Shared files remain private authenticated downloads and never receive public URLs.
17. 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 -->
17. 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.getModelCatalog({ type: 'image', method: 'image_edit' });
const available = new Set(data.map(model => model.id));
editModel = ['nano-banana-2-edit', 'gpt-image-2-edit', 'nano-banana-pro-edit']
.find(id => available.has(id)) || null;
document.getElementById('status').textContent = editModel
? `Ready: using ${editModel}`
: 'No image-edit model is 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.insufficientBalanceHandled || e.budgetExceededHandled) return;
status.textContent = 'Error: ' + (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.).