Skip to Content
Passage ConnectGuidesMigration Guide

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.

ConceptLegacyNew
Package@getpassage/react-js@getpassage/web-react
AuthOAuth client credentials → access tokenAPI key (Bearer token)
Backend callPOST /v1/intentintentToken + shortTokenPOST /v1/linksclaimCode + linkId
SDK methodopenAppClip({ intentToken, shortCode })openAppClip({ claimCode })
Success callbackonSuccess({ connectionId, status })onConnectionComplete()
Error callbackonError({ message, code })onConnectionError({ error, code })

Step 1: Update the SDK package

- npm install @getpassage/react-js + npm install @getpassage/web-react

Step 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 fieldsresource and action are top-level instead of nested in a resources object

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:

  • openAppClip takes a claimCode instead of intentToken + shortCode
  • Callbacks renamed: onSuccessonConnectionComplete, onErroronConnectionError
  • No more onExit callback — the modal auto-closes on completion or error
  • Branding options (companyName, logoUrl) are no longer passed here

Step 4: Receive results

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:

integrationIdResourceActions
tmobilemobileBillingStatementread
tmobilepaymentMethodread, write
attmobileBillingStatementread
attpaymentMethodread, write
verizonmobileBillingStatementread
verizonpaymentMethodread, 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.

Last updated on