Passage Capacitor SDK Integration Guide
This guide provides comprehensive instructions for integrating the Passage Capacitor SDK into your iOS application and implementing the required backend infrastructure.
Table of Contents
Installation
1. Install Dependencies
# Install the Passage Capacitor SDK
npm install @getpassage/capacitor
# Install peer dependencies
npm install @capacitor/core @capacitor/ios
2. iOS Platform Setup
# Add iOS platform if not already added
npx cap add ios
# Sync the plugin
npx cap sync ios
SDK Integration
Overview
To integrate Passage Capacitor SDK, you need to complete these steps:
- Install & Initialize → Create SDK instance
- 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: Initialize SDK
import { createPassage } from "@getpassage/capacitor";
const passage = createPassage();
Step 2: Get Intent Token
const response = await fetch("/api/intent-token");
const { intentToken } = await response.json();
Step 3: Open Passage Flow
await passage.open(intentToken, {
onSuccess: data => console.log("Connected!", data),
onError: error => console.error("Failed:", error)
});
Detailed Implementation
Complete Frontend Integration
import { createPassage } from "@getpassage/capacitor";
// Initialize SDK
const passage = createPassage();
async function connectAccount() {
try {
// 1. Get intent token from your backend
const response = await fetch("/api/intent-token");
const { intentToken } = await response.json();
// 2. Open Passage with callbacks
await passage.open(intentToken, {
presentationStyle: "modal", // or 'fullScreen'
onSuccess: data => {
// Connection successful - data contains history
console.log("Success:", data.data.history);
alert("Account connected successfully!");
},
onError: error => {
// Connection failed
console.error("Error:", error);
alert("Failed to connect account");
},
onClose: () => {
// Modal closed (with or without success)
console.log("Modal closed");
}
});
} catch (error) {
console.error("Request failed:", error);
}
}
// Usage
document.getElementById("connect-btn").onclick = connectAccount;
Configuration Options
// Optional: Enable debug logging
const passage = createPassage({ debug: true });
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
products: ["history"] // Requested data products
};
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 { createPassage } from "@getpassage/capacitor";
const passage = createPassage();
async function connectAccount() {
const response = await fetch("/api/intent-token");
const { intentToken } = await response.json();
await passage.open(intentToken, {
onSuccess: data => {
alert("Connected! Got " + data.data.history.length + " transactions");
},
onError: error => {
alert("Connection failed: " + error.error);
}
});
}
Production-Ready Example
import { createPassage } from "@getpassage/capacitor";
class AccountConnector {
constructor() {
this.passage = createPassage({ debug: true });
}
async connect() {
try {
const { intentToken } = await this.getIntentToken();
await this.passage.open(intentToken, {
presentationStyle: "modal",
onSuccess: data => this.handleSuccess(data),
onError: error => this.handleError(error),
onClose: () => this.handleClose()
});
} catch (error) {
this.handleError({ error: error.message });
}
}
async getIntentToken() {
const response = await fetch("/api/intent-token");
if (!response.ok) throw new Error("Failed to get token");
return response.json();
}
handleSuccess(data) {
// data.data.history contains transactions
// data.data.connectionId is the connection ID
console.log("Connected:", data.data.connectionId);
this.showNotification("Account connected successfully!", "success");
}
handleError(error) {
console.error("Connection error:", error);
this.showNotification("Failed to connect account", "error");
}
handleClose() {
console.log("Connection flow closed");
}
showNotification(message, type) {
// Your notification logic
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
// Usage
const connector = new AccountConnector();
document.getElementById("connect-btn").onclick = () => connector.connect();
Error Handling Patterns
// Handle specific error types
onError: error => {
switch (error.error) {
case "USER_CANCELLED":
console.log("User cancelled");
break;
case "NETWORK_ERROR":
console.error("Network issue");
break;
case "INVALID_TOKEN":
console.error("Token expired");
break;
default:
console.error("Unknown error:", error);
}
};
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. 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
Enable debug mode for detailed logging:
const passage = createPassage({
baseUrl: "https://passage-infra-web.vercel.app",
socketUrl: "https://passage-infra-web.vercel.app",
socketNamespace: "/ws",
debug: true // Enable debug logging
});
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.