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.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.0,
"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.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.