Linking a Plaid Bank Account
Use these operations to let a user connect a bank account to Fluz through Plaid Link. Linking is the entry point for all bank-account funding: once an account is connected, you can read its balance and spend power, pull its transaction history, and repair the connection if it later drops — each covered on its own page.
Transactional Graph Service (TGS) exposes a public GraphQL wrapper over the Fluz Plaid integration. Identity-service remains the system of record for Plaid tokens, bank-account ingestion, balances, historical transactions, and Plaid webhooks. TGS never returns Plaid access tokens or public tokens to your app.
Auth
Call /api/v1/graphql with a Fluz user Bearer access token that includes the MANAGE_PAYMENT scope.
The Plaid fields do not allow Basic auth. Obtain a user access token through the standard TGS auth flow first, then call these fields with:
Authorization: Bearer <fluz-user-access-token>The link flow at a glance
- Create a Link token (
createPlaidLinkToken). - Open Plaid Link with that token.
- On Plaid's
onSuccess, send the returnedpublic_tokenback to TGS (completePlaidLink). - Store the
platformItemIdfrom the completion response — it's the safe public identifier you'll use later to relink.
1. Create a Link token
mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) {
createPlaidLinkToken(input: $input) {
linkToken
expiration
requestId
mode
}
}{
"input": {}
}For native Link, pass deviceOs as IOS or ANDROID so identity-service includes the correct Plaid OAuth redirect option.
{
"input": {
"deviceOs": "IOS"
}
}2. Open Plaid Link
Initialize Plaid Link with the returned linkToken. (See the Web SDK example below.)
3. Complete the link
In Plaid Link's onSuccess, send the returned public_token to TGS.
mutation CompletePlaidLink($input: CompletePlaidLinkInput!) {
completePlaidLink(input: $input) {
requiresAddress
bankAccountId
bankInstitutionAuthId
newlyLinkedBankInstitutionAuthId
bankInstitutionName
platformItemId
bankAccounts {
bankInstitutionAuthId
bankAccountId
bankName
lastFour
type
subtype
}
}
}{
"input": {
"publicToken": "public-sandbox-..."
}
}4. Store the platformItemId
platformItemIdSave platformItemId from the completion response. This is the safe public identifier used later to relink a disconnected connection. If it isn't returned, call getPlaidBankAccounts for the user and store the persisted platformItemId from that response.
If completePlaidLink returns requiresAddress: true, attach an address before the account is usable — see Attach an address below.
Plaid Web SDK example
Plaid's Web SDK script must be loaded directly from Plaid's CDN.
<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>Create a Link token through TGS, initialize Plaid Link with that token, and pass Plaid's public_token back to TGS in onSuccess. Calling startPlaidLink() with no argument begins a new link; calling startPlaidLink(existingPlatformItemId) begins a relink (see Relinking Disconnected Bank Accounts).
const FLUZ_GRAPHQL_URL = '<fluz-api-base-url>/api/v1/graphql';
const fluzUserAccessToken = '<fluz-user-access-token>';
const fluzGraphql = async (query, variables) => {
const response = await fetch(FLUZ_GRAPHQL_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${fluzUserAccessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
const body = await response.json();
if (!response.ok || body.errors?.length) {
throw new Error(body.errors?.[0]?.message ?? 'Fluz GraphQL request failed');
}
return body.data;
};
const createPlaidLinkToken = async (platformItemId) => {
const data = await fluzGraphql(
`
mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) {
createPlaidLinkToken(input: $input) {
linkToken
mode
}
}
`,
{ input: { platformItemId } },
);
return data.createPlaidLinkToken;
};
const completePlaidLink = async (publicToken, platformItemId) => {
const data = await fluzGraphql(
`
mutation CompletePlaidLink($input: CompletePlaidLinkInput!) {
completePlaidLink(input: $input) {
requiresAddress
bankInstitutionAuthId
newlyLinkedBankInstitutionAuthId
bankAccountId
platformItemId
bankAccounts {
bankInstitutionAuthId
bankAccountId
bankName
lastFour
type
subtype
}
}
}
`,
{ input: { publicToken, platformItemId } },
);
return data.completePlaidLink;
};
const getPlaidBankAccounts = async (input) => {
const data = await fluzGraphql(
`
query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) {
getPlaidBankAccounts(input: $input) {
platformItemId
bankInstitutionAuthId
bankAccountId
bankInstitutionName
lastFour
type
subtype
}
}
`,
{ input },
);
return data.getPlaidBankAccounts;
};
const resolvePlatformItemId = async (completion) => {
if (completion.platformItemId) return completion.platformItemId;
const bankInstitutionAuthId =
completion.bankInstitutionAuthId ?? completion.newlyLinkedBankInstitutionAuthId;
const accounts = await getPlaidBankAccounts({ bankInstitutionAuthId });
return accounts[0]?.platformItemId;
};
const startPlaidLink = async (platformItemId) => {
const { linkToken } = await createPlaidLinkToken(platformItemId);
const handler = window.Plaid.create({
token: linkToken,
onSuccess: async (publicToken, metadata) => {
const result = await completePlaidLink(publicToken, platformItemId);
const persistedPlatformItemId = await resolvePlatformItemId(result);
await saveConnectionForRelinkLater({
platformItemId: persistedPlatformItemId,
bankInstitutionAuthId:
result.bankInstitutionAuthId ?? result.newlyLinkedBankInstitutionAuthId,
bankAccounts: result.bankAccounts,
plaidLinkSessionId: metadata.link_session_id,
});
},
onExit: (error, metadata) => {
console.log('Plaid Link exited', { error, metadata });
},
});
handler.open();
};List linked accounts
Return the safe Plaid bank accounts for the authenticated user.
query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) {
getPlaidBankAccounts(input: $input) {
platformItemId
bankInstitutionAuthId
bankInstitutionName
bankAccountId
accountName
lastFour
type
subtype
status
}
}{
"input": {
"platformItemId": "plaid-item-id"
}
}Attach an address
If completePlaidLink returned requiresAddress: true, attach an address to the linked bank institution.
mutation CreatePlaidLinkAddress($input: CreatePlaidLinkAddressInput!) {
createPlaidLinkAddress(input: $input) {
addressId
}
}{
"input": {
"bankInstitutionAuthId": "bank-institution-auth-id",
"address": {
"streetAddressLine1": "123 Main St",
"streetAddressLine2": "Apt 4",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
}Remove a bank connection
Remove a Plaid bank institution from the user's account.
mutation RemovePlaidBankInstitution($input: RemovePlaidBankInstitutionInput!) {
removePlaidBankInstitution(input: $input) {
removed
bankInstitutionAuthId
}
}{
"input": {
"bankInstitutionAuthId": "bank-institution-auth-id",
"reason": "USER_REQUESTED"
}
}Security boundaries
TGS never returns Plaid access tokens, Plaid public tokens, identity-match details, bank account numbers, or routing numbers. Bank account names and nicknames may be user-entered banking labels and are returned only as account metadata.
Identity-service remains responsible for:
- Plaid token exchange
- Plaid access token storage
- Bank institution and bank account creation or repair
- Balance fetching
- Historical transaction ingestion
- Bank institution removal
- Plaid webhooks
TGS does not expose identity-service directly and does not add a public Plaid webhook route.
Unsupported flows
Manual Plaid micro-deposit linking is intentionally unsupported by this public wrapper. New links and relinks must use the normal Plaid Link flow, which supports balances and historical transactions.
If a user starts a new link for the same institution instead of choosing relink, complete it as a normal new link. Identity-service owns bank-account dedupe and repair; store the returned platformItemId after completion.
Error handling
If completePlaidLink times out or fails after Plaid returned a public_token, first call getPlaidBankAccounts using the stored or expected platformItemId before retrying. Retry only if Plaid returns a fresh public_token from a new Link session — Plaid public tokens are short-lived and single-use.
Related pages
- Managing Bank Account Spend Power — read balances and spend power, and trigger a realtime refresh.
- Relinking Disconnected Bank Accounts — repair a connection that has dropped.
- Getting Bank Account Transaction Data — read historical bank transactions.
