Migration Guide
Migrating from the legacy Passage SDK (@getpassage/react-js) to the new Links API and @getpassage/web-react SDK.
What changed
The core change: intent tokens are replaced by Links. Instead of exchanging OAuth credentials for an intent token, you create a Link with your API key and pass a claimCode to your frontend. The flow is the same shape — your backend creates something, your frontend opens it — but simpler auth and better retry handling.
| Concept | Legacy | New |
|---|---|---|
| Package | @getpassage/react-js | @getpassage/web-react |
| Auth | OAuth client credentials → access token | API key (Bearer token) |
| Backend call | POST /v1/intent → intentToken + shortToken | POST /v1/links → claimCode + linkId |
| SDK method | openAppClip({ intentToken, shortCode }) | openAppClip({ claimCode }) |
| Success callback | onSuccess({ connectionId, status }) | onConnectionComplete() |
| Error callback | onError({ message, code }) | onConnectionError({ error, code }) |
Step 1: Update the SDK package
- npm install @getpassage/react-js
+ npm install @getpassage/web-reactStep 2: Update your backend
Replace the OAuth intent token flow with a single link creation call. No more client credentials exchange — just use your API key directly.
Before
// 1. Exchange OAuth credentials for access token
const { accessToken } = await fetch('https://api.getpassage.ai/oauth/token', {
method: 'POST',
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: 'client_credentials',
}),
}).then(r => r.json());
// 2. Create intent token
const { intentToken, shortToken } = await fetch('https://api.getpassage.ai/v1/intent', {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
body: JSON.stringify({ integrationId, resources }),
}).then(r => r.json());
// 3. Return to frontend
return { intentToken, shortToken };After
// 1. Create a link (API key auth — no OAuth flow needed)
const link = await fetch('https://connect.services.getpassage.ai/v1/links', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
integrationId: 'tmobile',
resource: 'mobileBillingStatement',
action: 'read',
webhookUrl: 'https://your-server.com/webhook', // optional
expiresIn: 14400, // optional, seconds (default: 4 hours)
}),
}).then(r => r.json());
// 2. Return to frontend
return { claimCode: link.claimCode, linkId: link.linkId };Key differences:
- No OAuth token exchange — authenticate with an API key directly
- One call instead of two — no separate token exchange step before creating the intent
- Flat resource fields —
resourceandactionare top-level instead of nested in aresourcesobject
Step 3: Update your frontend
Update imports to the new package, pass claimCode instead of intentToken + shortCode, and use the new callback names.
Before
import { usePassage } from '@getpassage/react-js';
const { openAppClip } = usePassage();
const handleLink = async () => {
const { intentToken, shortToken } = await fetch('/api/create-intent').then(r => r.json());
openAppClip({
intentToken,
shortCode: shortToken,
companyName: 'Your Company',
logoUrl: 'https://example.com/logo.png',
onSuccess: ({ connectionId, status }) => {
console.log('Connected:', connectionId);
},
onError: ({ message, code }) => {
console.error('Failed:', message);
},
onExit: () => {
console.log('User dismissed');
},
});
};After
import { usePassage } from '@getpassage/web-react';
const { openAppClip } = usePassage();
const handleLink = async () => {
const { claimCode } = await fetch('/api/create-link', { method: 'POST' }).then(r => r.json());
openAppClip({
claimCode,
onConnectionComplete: () => {
console.log('Account connected!');
},
onConnectionError: (error) => {
console.error('Failed:', error.error);
},
});
};Key differences:
openAppCliptakes aclaimCodeinstead ofintentToken+shortCode- Callbacks renamed:
onSuccess→onConnectionComplete,onError→onConnectionError - No more
onExitcallback — the modal auto-closes on completion or error - Branding options (
companyName,logoUrl) are no longer passed here
Step 4: Receive results
Option A: Webhooks (recommended)
Set webhookUrl when creating the link. You’ll receive an ES256-signed POST on completion:
{
"event": "link.complete",
"linkId": "link_abc123",
"status": "complete",
"result": {
"billing": [
{
"periodStart": "01/15/2025",
"periodEnd": "02/14/2025",
"amountDue": 85.00,
"currency": "USD"
}
]
},
"timestamp": 1704067200000
}See the Webhook Verification guide for signature verification.
Option B: Fetch on success
When the onConnectionComplete callback fires on the client, call your backend to fetch the link result:
// Frontend
openAppClip({
claimCode,
onConnectionComplete: async () => {
const result = await fetch(`/api/links/${linkId}/result`).then(r => r.json());
// use result
},
});// Backend
app.get('/api/links/:id/result', async (req, res) => {
const link = await fetch(`https://connect.services.getpassage.ai/v1/links/${req.params.id}`, {
headers: { 'Authorization': `Bearer ${process.env.PASSAGE_API_KEY}` },
}).then(r => r.json());
res.json(link.result);
});Available providers
Use GET /v1/providers to discover the full catalog. Current operations:
integrationId | Resource | Actions |
|---|---|---|
tmobile | mobileBillingStatement | read |
tmobile | paymentMethod | read, write |
att | mobileBillingStatement | read |
att | paymentMethod | read, write |
verizon | mobileBillingStatement | read |
verizon | paymentMethod | read, write |
See List Providers API for full details and result schemas.
Need help?
Reach out to the Passage team if you run into any issues during migration.