Add Bank Account With Plaid Link
Transactional Graph Service exposes a public GraphQL wrapper for the Fluz Plaid v2 integration. Identity-service remains the system of record for Plaid tokens, bank account ingestion, balances, historical transactions, and Plaid webhooks.
Auth
Call /api/v1/graphql with a Fluz user Bearer access token that includes MANAGE_PAYMENT.
These mutations do not allow Basic auth. Developer applications should use the existing TGS authentication flow to obtain a user access token first, then call the Plaid mutations with:
Authorization: Bearer <fluz-user-access-token>New Bank Link Flow
Use this flow when the user is adding a new bank connection.
- Before opening Plaid Link, 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 can include the correct Plaid OAuth redirect option.
{
"input": {
"deviceOs": "IOS"
}
}-
Initialize Plaid Link with
linkToken. -
In Plaid Link
onSuccess, send the returnedpublic_tokento 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-..."
}
}- Store
platformItemIdfrom the completion response. This is the safe public identifier to use later for relinking. If it is not returned, callgetPlaidBankAccountsfor the user and store the persistedplatformItemIdfrom that response.
Relink Flow
Use this flow when a previously linked Plaid connection is disconnected and needs repair.
- Start Link token creation with the stored
platformItemId.
mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) {
createPlaidLinkToken(input: $input) {
linkToken
expiration
requestId
mode
}
}{
"input": {
"platformItemId": "plaid-item-id"
}
}TGS uses platformItemId to retrieve the Plaid access token from identity-service, then asks identity-service to create a Plaid update-mode Link token. The access token is never returned to the caller.
For native relink, also pass deviceOs as IOS or ANDROID.
-
Initialize Plaid Link with the returned
linkToken. -
In Plaid Link
onSuccess, complete the flow with bothpublicTokenand the sameplatformItemId.
mutation CompletePlaidLink($input: CompletePlaidLinkInput!) {
completePlaidLink(input: $input) {
requiresAddress
bankAccountId
bankInstitutionAuthId
newlyLinkedBankInstitutionAuthId
bankInstitutionName
platformItemId
bankAccounts {
bankInstitutionAuthId
bankAccountId
bankName
lastFour
type
subtype
}
}
}{
"input": {
"publicToken": "public-sandbox-...",
"platformItemId": "plaid-item-id"
}
}- Update stored
platformItemIdfrom the response if it changed. If it is not returned, callgetPlaidBankAccountsfor the user and store the persistedplatformItemIdfrom that response.
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>Then create a Link token through TGS, initialize Plaid Link with that token, and pass Plaid's public_token back to TGS in onSuccess.
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();
};Call startPlaidLink() for a new bank link. Call startPlaidLink(existingPlatformItemId) to relink a disconnected Plaid connection.
Bank Account And Balance APIs
List safe Plaid bank accounts for the authenticated account.
query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) {
getPlaidBankAccounts(input: $input) {
platformItemId
bankInstitutionAuthId
bankInstitutionName
bankAccountId
accountName
lastFour
type
subtype
status
}
}{
"input": {
"platformItemId": "plaid-item-id"
}
}Get latest stored balances.
query GetPlaidBankBalances($input: PlaidBankBalanceFilterInput) {
getPlaidBankBalances(input: $input) {
platformItemId
bankInstitutionAuthId
bankAccountId
amount
current
source
trigger
startedAt
}
}Request a cached refresh for all connected Plaid bank data for the authenticated account.
mutation RefreshPlaidBankConnections {
refreshPlaidBankConnections {
status
balances {
bankAccountId
amount
current
}
verifyMembers {
platformItemId
bankInstitutionAuthId
}
}
}If refreshPlaidBankConnections.verifyMembers is returned, use the platformItemId to start the relink flow.
The public API intentionally does not expose Plaid realtime balance refresh. Use getPlaidBankBalances for the
latest stored balances, and refreshPlaidBankConnections when the app needs identity-service to perform its cached
connection refresh.
Transaction History
Read historical bank transactions populated by identity-service.
query GetPlaidBankTransactions($input: PlaidBankTransactionFilterInput) {
getPlaidBankTransactions(input: $input) {
totalCount
transactions {
historicalBankTransactionId
platformItemId
bankInstitutionAuthId
bankAccountId
transactionId
transactionStatus
transactionType
transactionDate
amount
currencyCode
description
merchantName
category
pending
}
}
}{
"input": {
"bankAccountId": "bank-account-id",
"startDate": "2026-01-01T00:00:00.000Z",
"endDate": "2026-06-04T23:59:59.999Z",
"paginate": {
"limit": 50,
"offset": 0
}
}
}Address And Removal
If completePlaidLink returns 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 Plaid bank institution.
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 that 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 the bank account dedupe and repair behavior; store the returned platformItemId after completion.
Error Handling
If createPlaidLinkToken fails with No active Plaid connection found for the provided platformItemId., the caller should treat the stored connection as no longer relinkable and guide the user through a new bank link.
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.
Updated 1 day ago
