Capacitor SDK Integration Guide
This guide provides comprehensive instructions for integrating the Passage Capacitor SDK into your Capacitor application and implementing the required backend infrastructure.
The Passage Capacitor SDK supports both iOS and Android platforms, enabling you to build cross-platform applications with secure data capture and authentication capabilities.
Table of Contents
Platform Support
Requirements
- Capacitor 5.0+
- Node.js 16+
iOS Support
- iOS 13.0 and above
- Xcode 14.0+
- Swift 5.0+
Android Support
- Android API Level 21+ (Android 5.0 Lollipop and above)
- Android Studio Arctic Fox or later
- Kotlin 1.5+
Installation: For installation instructions, see the Installation Guide.
SDK Integration
Overview
To integrate the Passage Capacitor SDK, you need to complete these steps:
- Backend Setup → Create intent token endpoint
- Get Token → Call your backend for intent token
- Open Passage → Launch connection flow with callbacks
- Handle Results → Process success/error responses
Quick Reference
Step 1: Get Intent Token
// Call your backend to get intent token
const response = await fetch('https://your-api.com/intent-token');
const { intentToken } = await response.json();
Step 2: Open Passage Flow
import { open } from '@getpassage/capacitor';
await open({
intentToken,
onConnectionComplete: (data) => {
console.log('Success:', data.connectionId);
console.log('Data received:', data.history?.length || 0, 'items');
},
onConnectionError: (error) => {
console.log('Error:', error.error);
},
onExit: (data) => {
console.log('User exited:', data.reason);
}
});
Detailed Implementation
Complete Capacitor Integration
import { open, close } from '@getpassage/capacitor';
import type { PassageSuccessData, PassageErrorData, PassageDataResult } from '@getpassage/capacitor';
// Connect to an integration
export const connectToIntegration = async (integrationId: string) => {
try {
// 1. Fetch intent token from your backend
const intentToken = await fetchIntentToken(integrationId);
// 2. Open Passage with all callback handlers
await open({
intentToken,
onConnectionComplete: (data: PassageSuccessData) => {
console.log('✅ Connection completed!');
console.log('📍 Connection ID:', data.connectionId);
console.log('📦 History items:', data.history?.length || 0);
// Process the captured data
if (data.history) {
data.history.forEach(item => {
if (item.structuredData) {
console.log('📄 Structured data:', item.structuredData);
}
});
}
},
onConnectionError: (error: PassageErrorData) => {
console.error('❌ Connection failed:', error.error);
console.error('📍 Error data:', error.data);
},
onExit: (data) => {
console.log('🚪 User exited:', data.reason);
},
});
} catch (error) {
console.error('❌ Connection failed:', error);
throw error;
}
};
// Helper function to fetch intent token
const fetchIntentToken = async (integrationId: string): Promise<string> => {
const response = await fetch('https://your-api.com/intent-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Publishable your-publishable-key'
},
body: JSON.stringify({ integrationId })
});
if (!response.ok) {
throw new Error(`Failed to fetch intent token: ${response.status}`);
}
const data = await response.json();
return data.intentToken;
};
// Close Passage programmatically
export const closePassage = async () => {
try {
await close();
console.log('✅ Passage closed');
} catch (error) {
console.error('❌ Error closing Passage:', error);
}
};
// Usage example
(async () => {
// Connect to a specific integration
await connectToIntegration('netflix');
})();
Optional Methods
While no setup is required to use the SDK, you can optionally use the configure() method for advanced configuration:
configure() - Optional Configuration
import { configure } from '@getpassage/capacitor';
// Enable debug logging (optional)
await configure({
debug: true, // Enable detailed debug logs
baseUrl: 'custom-url', // Custom API base URL (rarely needed)
socketUrl: 'custom-ws' // Custom WebSocket URL (rarely needed)
});
Note: This method is completely optional. You can call open() directly without any setup.
Backend Implementation
1. Intent Token Generation
Your backend needs to generate intent tokens by calling the Passage API. Here's how to implement this:
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import fetch from "node-fetch";
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001;
// Passage API configuration
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const BASE_URL = process.env.BASE_URL || "https://api.getpassage.ai";
// Validate required environment variables
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error("Error: CLIENT_ID and CLIENT_SECRET environment variables are required");
process.exit(1);
}
app.use(cors());
app.use(express.json());
/**
* Intent Token Endpoint
* Creates an intent token for the Passage SDK to initiate a connection.
*/
app.get("/api/intent-token", async (req, res) => {
try {
const accessToken = await getAccessToken();
const intentTokenUrl = `${BASE_URL}/intent-token`;
const body = {
integrationId: req.query.integrationId || "kindle", // Default integration ID
resources: {
readingHistory: {
read: {}
}
}
};
const apiRes = await fetch(intentTokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify(body)
});
if (!apiRes.ok) {
const error = await apiRes.text();
throw new Error(`Failed to get intent token: ${error}`);
}
const data = await apiRes.json();
res.json(data);
} catch (error) {
console.error("Error creating intent token:", error);
res.status(500).json({ error: error.message });
}
});
/**
* Webhook Endpoint
* Receives webhooks from Passage when connection status changes.
*/
app.post("/api/webhook", express.raw({ type: "application/json" }), (req, res) => {
try {
const webhookData = req.body;
const eventType = webhookData.type || webhookData.event;
// Handle different webhook event types
switch (eventType) {
case "Connection.Created":
console.log("New connection created:", webhookData.data.connectionId);
break;
case "Connection.Updated":
console.log("Connection updated:", webhookData.data.connectionId);
// When data becomes available, fetch the history
if (
webhookData.data.status === "data_available" ||
webhookData.data.status === "data_partially_available"
) {
getHistory(webhookData.data.connectionId);
}
break;
default:
console.log("Unhandled webhook event type:", eventType);
}
res.status(200).json({ status: "success" });
} catch (error) {
console.error("Error processing webhook:", error);
res.status(500).json({ error: "Internal server error" });
}
});
/**
* Get OAuth access token using client credentials
*/
async function getAccessToken() {
const tokenUrl = `${BASE_URL}/oauth/token`;
const body = new URLSearchParams();
body.append("grant_type", "client_credentials");
body.append("client_id", CLIENT_ID);
body.append("client_secret", CLIENT_SECRET);
const res = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Failed to get access token: ${error}`);
}
const data = await res.json();
return data.access_token;
}
/**
* Fetch transaction history for a connection
*/
async function getHistory(connectionId) {
try {
console.log(`Fetching history for connection: ${connectionId}`);
const accessToken = await getAccessToken();
const historyUrl = `${BASE_URL}/connections/${connectionId}/history`;
const res = await fetch(historyUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Failed to get history: ${error}`);
}
const historyData = await res.json();
// TODO: Process the history data
// Examples:
// - Store in database
// - Send to analytics service
// - Trigger business logic
// - Send notifications
return historyData;
} catch (error) {
console.error(`Error fetching history for connection ${connectionId}:`, error);
throw error;
}
}
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Intent token endpoint: /api/intent-token`);
console.log(`Webhook endpoint: /api/webhook`);
});
2. Environment Configuration
Create a .env file:
# Passage API Configuration
CLIENT_ID=your_passage_client_id_here
CLIENT_SECRET=your_passage_client_secret_here
BASE_URL=https://api.getpassage.ai
# Server Configuration
PORT=3001
3. Passage API Endpoints
Your backend will interact with these Passage API endpoints:
- OAuth Token:
POST https://api.getpassage.ai/oauth/token- Get access token using client credentials
- Intent Token:
POST https://api.getpassage.ai/intent-token- Create intent token for SDK initialization
- Connection History:
GET https://api.getpassage.ai/connections/{connectionId}/history- Fetch transaction history when data becomes available
4. Webhook Configuration
To receive webhooks from Passage:
- Set up a public URL for your webhook endpoint (use ngrok for local development)
- Contact Passage support to configure your webhook URL
- Your webhook will receive events for:
Connection.Created- When a new connection is establishedConnection.Updated- When connection status changes (including data availability)
Complete Examples
Minimal Implementation
import { open } from '@getpassage/capacitor';
// Simple connection function - no setup required!
async function connectAccount(integrationId: string) {
// Fetch intent token from your backend
const response = await fetch('/api/intent-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ integrationId })
});
const { intentToken } = await response.json();
// Open Passage
await open({
intentToken,
onConnectionComplete: (data) => {
console.log(`Connected! ID: ${data.connectionId}`);
console.log('Data received:', data.history?.length || 0, 'items');
},
onConnectionError: (error) => {
console.error(`Connection failed: ${error.error}`);
}
});
}
// Usage
connectAccount('netflix');
Production-Ready Example
import { open, close } from '@getpassage/capacitor';
import type { PassageSuccessData, PassageErrorData, PassageDataResult } from '@getpassage/capacitor';
// State management
let isLoading = false;
// Fetch intent token from backend
const fetchIntentToken = async (integrationId: string): Promise<string> => {
try {
const response = await fetch('https://your-api.com/intent-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ integrationId })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data.intentToken;
} catch (error) {
console.error('Failed to fetch intent token:', error);
throw error;
}
};
// Handle successful connection
const handleConnectionSuccess = (data: PassageSuccessData) => {
console.log('✅ Connection completed!');
console.log('Connection ID:', data.connectionId);
console.log('History items received:', data.history?.length || 0);
// Process the captured data
const historyItems = data.history || [];
console.log(`Processing ${historyItems.length} data items...`);
historyItems.forEach((item, index) => {
if (item.structuredData) {
console.log(`Item ${index + 1}:`, item.structuredData);
}
});
showNotification(`Successfully connected! ID: ${data.connectionId}`, 'success');
isLoading = false;
};
// Handle connection errors
const handleConnectionError = (error: PassageErrorData) => {
console.error('❌ Connection failed:', error.error);
showNotification(`Connection failed: ${error.error}`, 'error');
isLoading = false;
};
// Handle user exit
const handleExit = (data: { reason?: string }) => {
console.log('🚪 User exited. Reason:', data.reason || 'User closed');
isLoading = false;
};
// Notification system
const showNotification = (message: string, type: 'success' | 'error') => {
console.log(`[${type.toUpperCase()}] ${message}`);
};
// Main connection function
export const connectToService = async (integrationId: string) => {
if (isLoading) {
throw new Error('Connection already in progress.');
}
isLoading = true;
try {
console.log(`Starting connection to ${integrationId}...`);
// Fetch intent token from backend
const intentToken = await fetchIntentToken(integrationId);
// Open Passage with event handling
await open({
intentToken,
onConnectionComplete: handleConnectionSuccess,
onConnectionError: handleConnectionError,
onExit: handleExit
});
} catch (error) {
console.error('Connection process failed:', error);
isLoading = false;
throw error;
}
};
// Close connection
export const closeConnection = async () => {
try {
await close();
isLoading = false;
console.log('Connection closed');
} catch (error) {
console.error('Error closing connection:', error);
}
};
// Getters for state
export const getLoadingState = () => isLoading;
// Usage Example
(async () => {
// Connect to different services
document.getElementById('netflix-btn')?.addEventListener('click', async () => {
try {
await connectToService('netflix');
} catch (error) {
console.error('Netflix connection failed:', error);
}
});
document.getElementById('amazon-btn')?.addEventListener('click', async () => {
try {
await connectToService('amazon');
} catch (error) {
console.error('Amazon connection failed:', error);
}
});
})();
API Reference
Core Methods
import { open, close } from '@getpassage/capacitor';
// Open connection flow
open(options: PassageOpenOptions): Promise<void>;
// Close connection programmatically
close(): Promise<void>;
Data Types
PassageOpenOptions
interface PassageOpenOptions {
intentToken?: string;
onConnectionComplete?: (data: PassageSuccessData) => void;
onConnectionError?: (data: PassageErrorData) => void;
onExit?: (data: { reason?: string }) => void;
}
PassageSuccessData
interface PassageSuccessData {
history: PassageHistoryItem[]; // Captured data items
connectionId: string; // Unique connection identifier
}
interface PassageHistoryItem {
structuredData?: any; // Integration-specific data (see Integration Data Types)
additionalData: Record<string, any>; // Additional metadata
}
📚 For detailed data structures returned by each integration, see Integration Data Types →
PassageErrorData
interface PassageErrorData {
error: string; // Error message
data?: any; // Additional error data
}
PassageDataResult
interface PassageDataResult {
data?: any; // Raw data payload
prompts?: Record<string, any>[]; // Associated prompts
}
Troubleshooting
Common Issues
1. iOS Build Errors
# Clean build
cd ios/App
rm -rf build
pod install --repo-update
cd ../..
# Clean Xcode derived data
rm -rf ~/Library/Developer/Xcode/DerivedData
# Rebuild
npx cap sync ios
npx cap build ios
2. Android Build Errors
Missing JitPack Repository:
If you see dependency resolution errors, ensure JitPack is configured in your android/build.gradle:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' } // Required for Passage SDK
}
}
General Build Issues:
# Clean build
cd android
./gradlew clean
cd ..
# Sync and rebuild
npx cap sync android
npx cap build android
# If Gradle issues persist
cd android
./gradlew --stop
./gradlew build
3. Backend Connection Issues
# Test intent token endpoint
curl http://your-domain.com/api/intent-token
# Check environment variables
node -e "console.log(process.env.CLIENT_ID ? 'CLIENT_ID set' : 'CLIENT_ID missing')"
Debug Mode
To enable detailed debug logging on both iOS and Android, use the optional configure() method:
import { configure } from '@getpassage/capacitor';
// Enable debug logging for detailed troubleshooting
await configure({
debug: true
});
// Then use open() normally
await open({
intentToken,
// ... callbacks
});
This will provide detailed console logs to help with troubleshooting integration issues.
Data Types
Success Data Structure
interface PassageSuccessData {
history: PassageHistoryItem[]; // Captured data items
connectionId: string; // Unique connection identifier
}
interface PassageHistoryItem {
structuredData?: any; // Integration-specific data (see Integration Data Types)
additionalData: Record<string, any>; // Additional metadata
}
📚 For detailed data structures returned by each integration, see Integration Data Types →
Error Data Structure
interface PassageErrorData {
error: string; // Error message
data?: any; // Additional error data
}
Integration Data Types
- Browse integrations and resources in the explorer
Support
For additional support:
- Check the GitHub Issues
- Review the API Documentation
- Contact the development team
This guide covers the essential aspects of integrating the Passage Capacitor SDK. For specific implementation details, refer to the example code in this repository.