# Create a single-use virtual card Source: https://docs.fluz.app/Create-Single-Use-Virtual-Cards Issue a virtual card that locks itself automatically after one transaction. A **single-use card** is a virtual card that locks itself as soon as one transaction completes. It is the standard control for one-time payouts, trial signups, and any charge you want to guarantee cannot repeat. There is no `cardType: SINGLE_USE` parameter. `cardType` is a **response** field, not an input. You get single-use behavior by setting `lockCardNextUse: true` when you issue the card. > [!WARNING] > > **Locking is reversible. This is not a hard one-time-use guarantee.** > > A locked card can be reopened with [`unlockVirtualCard`](/features/unlock-virtual-card) by anyone with access to it, and once unlocked it will authorize again. `lockCardNextUse` is an automation, not an irreversible control. > > If your risk model requires that a card can *never* transact twice, pair the lock with a `spendLimit` sized to the expected charge and a near-term `lockDate` — see [Making it stick](#making-it-stick). *** ## Issue the card > **Restricted Access** > > This mutation requires a Bearer token with the `CREATE_VIRTUALCARD` scope. Set `lockCardNextUse: true` on [`createVirtualCard`](/features/create-card). Everything else about the request is a normal card issuance. ```graphql theme={null} mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId cardholderName virtualCardLast4 expiryMonth expiryYear status cardType initialAmount usedAmount createdAt } } ``` ```json theme={null} { "input": { "idempotencyKey": "63b2c9e0-62d1-42ab-b1c2-1a7ee2f8c0a9", "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "spendLimit": 75.00, "spendLimitDuration": "LIFETIME", "lockCardNextUse": true, "cardNickname": "Vendor payout — one time", "primaryFundingSource": "FLUZ_BALANCE", "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128" } } ``` ### The relevant fields | Field | Type | Description | | :- | :- | :- | | `lockCardNextUse` | `Boolean` | When `true`, the card locks automatically once its next transaction completes. Defaults to `false`. | | `spendLimit` | `Float!` | Maximum chargeable amount. Minimum `$5`. You are only charged for what is actually used. | | `spendLimitDuration` | `VirtualCardSpendLimitDuration` | Use `LIFETIME` for single-use cards so the limit never refreshes. Defaults to `LIFETIME`. | | `lockDate` | `String` | Date the card locks regardless of use. Format `yyyy-mm-dd`. Defaults to 47 months out. | > [!NOTE] > > **`cardType` in the response is not a reliable single-use indicator.** Read back the card's `lockCardNextUse` setting rather than inferring one-time behavior from `cardType`, which reflects the card program rather than this setting. *** ## Making it stick `lockCardNextUse` on its own is reversible. For payout and disbursement flows where a second charge would be a real loss, stack the controls — each one fails closed independently: **1. Size the spend limit to the charge** Set `spendLimit` to the exact expected amount, with `spendLimitDuration: LIFETIME` so it never refreshes. Even if the card is unlocked, there is no headroom left to spend against. **2. Set a near-term lockDate** A card issued for a payout that should clear within a week does not need to stay open for 47 months. `lockDate` locks the card on that date whether or not it was ever used, which also cleans up abandoned cards. **3. Restrict the funding composition** Set `usePrepaymentBalance: false` and `useRewardsBalance: false` so the card draws only from the `userCashBalanceId` you nominate, rather than reaching into prepaid or rewards balances. **4. Reconcile against the ledger** Do not treat issuance as completion. Confirm the charge landed with [`getVirtualCardTransactions`](/features/get-virtual-card-transactions) before marking the payout settled in your own system. ```json theme={null} { "input": { "idempotencyKey": "8f14c2b1-9d3e-4a77-b0c5-2e6f1a8d4b93", "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "spendLimit": 75.00, "spendLimitDuration": "LIFETIME", "lockCardNextUse": true, "lockDate": "2026-08-24", "primaryFundingSource": "FLUZ_BALANCE", "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128", "usePrepaymentBalance": false, "useRewardsBalance": false, "cardNickname": "Payout #48812" } } ``` *** ## Converting an existing card `lockCardNextUse` is also editable after issuance through [`editVirtualCard`](/features/edit-virtual-card), so you can retire a multi-use card by letting its next transaction be its last. ```graphql theme={null} mutation EditVirtualCard($input: EditVirtualCardInput!) { editVirtualCard(input: $input) { virtualCardId virtualCardLast4 status cardType } } ``` ```json theme={null} { "input": { "idempotencyKey": "d3b07384-d9a0-4f1e-9b2c-7a5e8c1f2d64", "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "lockCardNextUse": true } } ``` To stop a card immediately rather than after one more transaction, use [`lockVirtualCard`](/features/lock-virtual-card) instead — that takes effect at once and does not wait for a transaction. *** ## Issuing in bulk `lockCardNextUse` is available per order item on [`createVirtualCardBulkOrder`](/features/create-bulk-order), which is the right call for batch disbursements rather than looping `createVirtualCard`. ```graphql theme={null} mutation CreateVirtualCardBulkOrder($input: CreateVirtualCardBulkOrderInput!) { createVirtualCardBulkOrder(input: $input) { orderId orderStatus } } ``` ```json theme={null} { "input": { "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "orderItems": [ { "quantity": 100, "spendLimit": 50, "spendLimitDuration": "LIFETIME", "lockCardNextUse": true, "cardNickname": "Q3 rebate", "primaryFundingSource": "FLUZ_BALANCE" } ] } } ``` Bulk orders are asynchronous. Poll [`getVirtualCardBulkOrderStatus`](/features/get-bulk-order-status) and handle partial failures — an order can complete with fewer cards than requested. *** ## Edge cases These are the situations where "one transaction" turns out to be ambiguous. Design for them before you ship a payout flow. | Situation | What to expect | | :- | :- | | **Verification auths** | Some merchants run a $0 or $1 authorization to validate the card before the real charge. If that counts as the card's use, the card locks before the intended purchase and the real charge declines. Test against your specific merchant category. | | **Declined transactions** | A decline is not a completed transaction and should not consume the card. Confirm the card is still `ACTIVE` before reissuing — reissuing on a false assumption leaves a live card outstanding. | | **Partial authorizations** | A merchant may authorize less than the full amount. Reconcile against actual transaction amounts rather than assuming `spendLimit` was captured. | | **Refunds after lock** | If a refund needs to land on a card that has already locked, confirm the credit posts before you close out the payout in your own system. | | **Recurring merchants** | A single-use card will fail the second billing cycle by design. Do not issue single-use cards for subscriptions — use `lockDate` to bound a recurring card instead. | > [!NOTE] > > **Locked is not deleted.** Virtual cards cannot be deleted. A locked card stays visible and queryable on the account indefinitely, with its transaction history intact. Plan for accumulation if you are issuing single-use cards at volume, and use `cardNickname` and `transactionCategory` at issuance so they remain reconcilable later. *** ## Common mistakes > **Passing cardType: SINGLE\_USE to createVirtualCard** > > `CreateVirtualCardInput` has no `cardType` field — the request fails with `ARG-0001`. `cardType` only appears in the response. Set `lockCardNextUse: true` instead. > **Treating the lock as irreversible** > > `unlockVirtualCard` reopens a locked card, and the card will authorize again if spend headroom remains. If a second charge would be a real loss, constrain `spendLimit` and `lockDate` as well rather than relying on the lock alone. > **Using a refreshing spend limit** > > `spendLimitDuration: DAILY` or `MONTHLY` refreshes the available limit on schedule. On a card that gets unlocked, that restores spending capacity you thought was spent. Use `LIFETIME` for single-use cards. > **Marking the payout complete at issuance** > > Issuing a card is not the same as money moving. The card may never be used, may be used for less than the limit, or may lock on a verification auth. Reconcile against `getVirtualCardTransactions` before closing the payout. *** > **[Issue Cards](/features/create-card)** > > The full `createVirtualCard` reference. > **[Create Bulk Order](/features/create-bulk-order)** > > Issue single-use cards at volume in one asynchronous request. > **[Lock Virtual Card](/features/lock-virtual-card)** > > Stop a card immediately instead of after its next transaction. > **[Merchant-Locked Cards](/features/create-merchant-locked-card)** > > Restrict a card to one merchant instead of one transaction. # KYB testing Source: https://docs.fluz.app/KYB-testing Test data for simulating KYB verification outcomes in the staging environment Use these values to drive `registerBusiness` to a predictable outcome in **staging**, and to get each owner to `READY`. > [!NOTE] > > **Staging only.** These values only work on `https://transactional-graph.staging.fluzapp.com/api/v1/graphql`. Never submit a real business's tax ID or owner details to staging. > [!NOTE] > > **Before you start, you'll need:** > > * an **OAuth Integration** application that Fluz has moved to `ACTIVE`, with the `REGISTER_BUSINESS` scope on its **Permissions** list and a non-empty **Business permissions** list (see [Prepare Your Accounts](/get-started/prepare-accounts) and [Business Accounts in OAuth](/business-accounts-in-o-auth)); > * an applicant user who has **completed the OAuth grant** for that application (see [Client-Facing OAuth Grant Flow](/client-facing-o-auth-grant-flow)); > * that applicant's KYC returned `APPROVED` through the **SSN** path (see [Test KYC Flows](/test-kyc-flows)); > * a **`CONSUMER`** token for that applicant, minted with their personal `accountId` (see [How accounts work](/get-started/accounts-and-applications)). ## The order of operations Every staging KYB test follows the same sequence. Most errors in staging come from running a step early or with the wrong token. 1. **Create the applicant** with `registerUser` (leave out `deferSeatAssignment`), or use an existing staging user. 2. **Send the applicant through OAuth.** In staging, no SMS is sent: enter `123456` as the sign-in code. If the account picker appears, choose **Apply for a business account**. 3. **Verify the applicant** with `verifyUserInformation` and the approved test identity, using a `CONSUMER` token. 4. **Register the business** with `registerBusiness`, still using the `CONSUMER` token. 5. **Switch to a `BUSINESS` token** (same `userId`, the new business `accountId`) and track owners and `kybStatus` with `getBusiness`. > [!NOTE] > > **Known issue — applicants created with `registerUser`.** Some of these users are missing an OAuth setting, so `registerBusiness` fails with `AUTH-0008` ("does not have permission to access the application") even after a valid grant. Until the fix ships, send the user through `/authorize` once, choose **Apply for a business account**, and reuse the same `external_id` as their existing grant. If it still fails, contact your Fluz contact with the user ID. ## Test tax IDs Pass these as `taxId` on `registerBusiness`, in the format `XX-XXXXXXX`. The tax ID is the only field that decides the staging outcome. | `taxId` | Outcome | | :- | :- | | `11-1111111` | Approved | | `22-2222222` | Declined | | `33-3333333` | Declined | Any other value goes to review and isn't guaranteed to resolve in staging. ## Test legal address The business legal address is checked against an address-validation provider, even in staging. Invented streets fail with `BS-0002`. Use a value from [Test Addresses](/test-addresses). ## Example: an auto-approved business Call with the applicant's `CONSUMER` token. Get `businessCategoryId` and `businessSubCategoryId` from [getBusinessCategories](/business-categories) (also `CONSUMER`); never hardcode them. ```graphql Mutation theme={null} mutation RegisterBusiness($input: RegisterBusinessInput!) { registerBusiness(input: $input) { accountId kybStatus success error { code message } } } ``` ```json Variables theme={null} { "input": { "businessName": "Test Approved LLC", "dbaName": "Test Approved", "businessStructure": "LLC", "businessLegalAddress": { "streetAddressLine1": "", "city": "", "state": "", "postalCode": "", "country": "United States" }, "stateOfIncorporation": "Delaware", "taxId": "11-1111111", "businessCategoryId": "", "businessSubCategoryId": "", "natureOfBusiness": "Integration testing", "businessAccountUsage": ["CORPORATE_SPENDING_ADMIN"], "externalReferenceId": "test-business-001", "confirm": { "allOwnersWith25PercentageOwnershipListed": true, "noOwnersMoreThan25Percentage": false }, "owners": [ { "firstName": "John", "lastName": "Smith", "emailAddress": "", "phoneNumber": "", "title": "Chief Executive Officer", "ownershipPercentage": 100, "isControlPerson": true, "isInvited": false, "isUsPerson": true } ] } } ``` > [!NOTE] > > Use the `RegisterBusinessInput` field names shown here: `businessName`, `taxId`, `businessStructure`. The applicant owner's email or phone must match the user the token belongs to. Every business needs one control person; beneficial owners are only required for anyone holding 25% or more. A success response returns an `accountId` and `kybStatus: SUBMITTED`. Store the `accountId`: you need it to mint the `BUSINESS` token for everything that follows. The applicant's existing grant extends to the new business account, so `generateUserAccessToken` works for it while KYB is still pending. ## Getting owners to `READY` Mint a `BUSINESS` token (the same `userId` plus the new business `accountId`) and call `getBusiness`. Each owner has a `verificationType`: | `verificationType` | How to complete it in staging | | :- | :- | | `SSN` | The owner verifies **as themselves**: a token for the owner's own user, calling `verifyUserInformation` with an approved test identity. See [Test KYC Flows](/test-kyc-flows) | | `DOCUMENTS` | Call `requestOwnerDocumentVerificationLink` with the `BUSINESS` token and complete the hosted flow | > [!WARNING] > > `requestOwnerDocumentVerificationLink` returns `ARG-0001` for `SSN` owners. It only works for `DOCUMENTS` owners. > [!NOTE] > > `kybStatus: APPROVED` is about the business. Each owner is tracked separately, so an owner can still read `PENDING_CIP` after the business is approved. Treat the business as ready only when `kybStatus` is final **and** every owner reports `READY`. ## Common staging errors | Error | Where you see it | Cause | Fix | | :- | :- | :- | :- | | `AUTH-0030 … current status: PERSONAL` | Any call | The application was made with **Create new app** and isn't an active OAuth Integration application | Use an OAuth Integration application that Fluz has activated | | `AUTH-0031` | `generateUserAccessToken`, `getAccountsByUserId` | The applicant hasn't completed the OAuth grant | Run step 2 of [the order of operations](#the-order-of-operations) | | `AUTH-0008` "Invalid user access" | `generateUserAccessToken` | The applicant was registered with `deferSeatAssignment: true` | Register a new applicant without the flag | | `AUTH-0002` "requires account type CONSUMER" | `getBusinessCategories`, `registerBusiness` | Called with a `BUSINESS` token | Use the applicant's `CONSUMER` token | | `AUTH-0008` "does not have permission to access the application" | `registerBusiness` | Known issue for applicants created with `registerUser` | See the known-issue note above | | `ARG-0001` | `registerBusiness` | The applicant has no successful KYC on file | Verify the applicant first | | `ARG-0001` | `requestOwnerDocumentVerificationLink` | The owner's `verificationType` is `SSN` | Verify that owner with `verifyUserInformation` instead | | Owner stuck at `PENDING_CIP`, `provided: false` | `getBusiness` | The owner (often the applicant) has no SSN verification on file | Run `verifyUserInformation` for that owner with the approved test identity | | `BS-0002` | `registerBusiness` | The legal address failed validation | Use a value from [Test Addresses](/test-addresses) | | `BS-0007` | `registerBusiness` | The applicant already has an application in flight | Use a fresh applicant | ## Changing a business after approval There is no API to change a business's legal name, address, or owners once it's registered. Contact Fluz support for those changes. ## Resetting There is no API to reset a business's KYB outcome, or to cancel an in-flight application. A successful submission blocks the same applicant from registering again (`BS-0007`) until it resolves. To test repeatedly, use a fresh applicant user, or ask your Fluz contact to reset one. ## Next steps > **[Register & Verify Businesses](/kyb-overview)** > > Prerequisites, token types, and the KYB lifecycle. > **[Onboard & Verify a Business](/quickstart/onboard-businesses)** > > The whole business path end to end in staging. # Open loop card funding sources Source: https://docs.fluz.app/Manage-Open-Loop-Virtual-Card-Funding-Sources Set which spend account pays for each hosted open-loop card you generate. Open-loop cards are funded from **your** account, not the recipient's. When someone claims a hosted card link and spends it, the money comes out of a spend account you nominate at the moment you generate the link. > [!WARNING] > > **The funding options here are narrower than on `createVirtualCard`.** > > The open-loop path exposes exactly one funding control: `userCashBalanceId`. There is no `primaryFundingSource`, no `bankAccountId`, no `usePrepaymentBalance`, and no `useRewardsBalance`. External bank accounts cannot fund an open-loop card — the spend account must be funded first. > > If you are porting an integration from `createVirtualCard`, do not assume the same fields carry over. See [Differences from standard cards](#differences-from-standard-cards). *** ## Setting the funding source Pass `userCashBalanceId` on [`generateVCShareLinks`](/features/open-loop-cards/send-open-loop-cards). It applies to every card generated in that request. ```graphql theme={null} mutation GenerateVCShareLinks($input: GenerateVCShareLinksInput!) { generateVCShareLinks(input: $input) { shareRequestId generatedShareLinks { linkUrl status } } } ``` ```json theme={null} { "input": { "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128", "shareMethod": "GENERATE_URL", "spendLimit": 50.00, "daysUntilExpiration": 30 } } ``` > [!NOTE] > > **`userCashBalanceId` is marked optional in the schema, but treat it as required.** > > Omit it and the cards fall back to a default balance on your account. For a payout or rewards program that is almost never what you want — it makes spend unattributable and puts campaign disbursements against general account funds. Always set it explicitly, on every request. > **Restricted Access** > > This mutation requires a Bearer token with the `CREATE_SHARE_LINK` scope. Basic auth is rejected. *** ## Per-card funding across a batch `userCashBalanceId` is set per **request**, not per link. Every card in one `generateVCShareLinks` call draws from the same spend account. To fund different cards from different accounts, issue one request per funding source: ```typescript theme={null} // One request per campaign, each pointed at its own spend account. const campaigns = [ { name: 'Q3 referral', cashBalanceId: 'b1155504-...', limit: 25, count: 200 }, { name: 'Q3 win-back', cashBalanceId: 'c2266615-...', limit: 50, count: 50 }, ]; for (const campaign of campaigns) { await graphql( `mutation GenerateVCShareLinks($input: GenerateVCShareLinksInput!) { generateVCShareLinks(input: $input) { shareRequestId generatedShareLinks { linkUrl status } } }`, { input: { offerId, userCashBalanceId: campaign.cashBalanceId, shareMethod: 'GENERATE_URL', spendLimit: campaign.limit, daysUntilExpiration: 30, }, }, ); } ``` > [!NOTE] > > **Use one spend account per program, not one shared pot.** A dedicated spend account per campaign gives you a clean balance to reconcile against, an obvious place to see remaining exposure, and a natural stopping point if something goes wrong — draining one account cannot touch another. It also means [`getSpendAccountVirtualAccountNumbers`](/features/virtual-account-numbers) gives that campaign its own routing and account number for direct funding. *** ## Funding the spend account Because open-loop cards cannot pull from an external bank at spend time, the spend account has to hold funds before recipients start claiming. Three ways to get money in: | Method | Use when | | :- | :- | | [Deposit from an external account](/features/deposit-from-external-accounts) | Standard top-up from a linked bank account or card. | | [Virtual account numbers](/features/virtual-account-numbers) | You want to push funds in by ACH, wire, RTP, or FedNow. Each spend account gets its own routing and account number. | | [Transfer between spend accounts](/features/transfer-between-spend-accounts) | Moving budget from one program to another. | Check the balance before a large batch with [`getUserCashBalances`](/features/get-spend-accounts), reading `availableCashBalance` rather than `totalCashBalance`. *** ## Differences from standard cards | Control | `createVirtualCard` | `generateVCShareLinks` | | :- | :- | :- | | Specific spend account | `userCashBalanceId` | `userCashBalanceId` | | External bank account | `primaryFundingSource: BANK_ACCOUNT` + `bankAccountId` | Not available | | Prepayment balance | `usePrepaymentBalance` | Not exposed | | Rewards balance | `useRewardsBalance` | Not exposed | | Change funding after issuance | `editVirtualCard` | Not available | The practical consequence: open-loop programs are **prefunded**. You cannot run them off an external bank account the way you can run a standard corporate card, so treat spend account balance as an operational requirement and monitor it. For the full picture on standard cards, see [Manage Virtual Card Funding Sources](/features/virtual-card-funding-sources). *** ## Common mistakes > **Passing primaryFundingSource or bankAccountId** > > Neither field exists on `GenerateVCShareLinksInput`. Open-loop cards are funded from a spend account only. Fund the spend account first, then generate links against it. > **Omitting userCashBalanceId because the schema says it is optional** > > It is optional in the type definition but effectively required in practice. Without it your cards draw from a default balance, and campaign spend becomes impossible to attribute or cap. > **Assuming funding can be changed after the link is generated** > > There is no edit path for an issued share link's funding. If you nominated the wrong spend account, deactivate the links with `deactivateVCShareLinks` and generate a new batch. > **Generating a large batch without checking the balance** > > Confirm `availableCashBalance` covers the batch — `spendLimit` multiplied by the number of links — before generating. A batch that outruns its funding fails at the recipient, not at your API call, which means your support team hears about it before you do. *** > **[Open Loop Cards Overview](/features/open-loop-cards/send-open-loop-cards)** > > Generating, listing, and deactivating hosted card links. > **[Delivery Methods](/features/open-loop-cards/open-loop-card-delivery-methods)** > > The four ways to get a card link to a recipient. > **[Virtual Account Numbers](/features/virtual-account-numbers)** > > Give each program's spend account its own routing and account number. > **[Virtual Card Funding Sources](/features/virtual-card-funding-sources)** > > The fuller set of funding controls on standard cards. # Manage virtual card funding sources Source: https://docs.fluz.app/Manage-Virtual-Card-Funding-Sources Choose what pays for a virtual card — a spend account, an external bank account, rewards, prepayment balance, or a combination. Every virtual card draws from something when it is charged. Fluz gives you four sources, and a card can use more than one at a time. > [!WARNING] > > **By default, cards draw from three balances, not one.** > > If you set a `userCashBalanceId` and nothing else, the card can still reach into the rewards balance and the prepayment balance when they have funds. That is the default and it is deliberate — but it means a card you believed was ringfenced to one budget can spend money from elsewhere on the account. > > To isolate a card to a single spend account, you must explicitly set `usePrepaymentBalance: false` and `useRewardsBalance: false`. See [Isolating a card](#isolating-a-card-to-one-spend-account). *** ## The two layers Funding is configured in two independent layers. Getting them confused is the most common source of unexpected declines. **1. Primary funding source — where money is pulled from** `primaryFundingSource` is either `FLUZ_BALANCE` (money already held on the account) or `BANK_ACCOUNT` (pulled from a linked external bank via ACH). Defaults to `FLUZ_BALANCE`. **2. Balance composition — which internal balances can contribute** When the primary source is `FLUZ_BALANCE`, the card can draw from a specific spend account, the prepayment balance, and the rewards balance. `userCashBalanceId`, `usePrepaymentBalance`, and `useRewardsBalance` control which. *** ## The four options ### 1. A specific spend account Spend accounts are named sub-ledgers on the account — "Team Travel", "Marketing", "Payouts". Nominate one with `userCashBalanceId` and the card draws from that balance rather than the account's general cash balance. ```json theme={null} { "primaryFundingSource": "FLUZ_BALANCE", "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128" } ``` This is the right default for anything that needs to be budgeted, attributed, or reconciled separately. To create additional spend accounts, see [Create Spend Accounts](/features/create-spend-accounts); to list the ones you already have and read their balances, see [Get Spend Accounts](/features/get-spend-accounts). ### 2. An external bank account Set `primaryFundingSource: BANK_ACCOUNT` and supply `bankAccountId`. Fluz pulls from the linked account after the card is spent, so you do not have to pre-fund a balance. ```json theme={null} { "primaryFundingSource": "BANK_ACCOUNT", "bankAccountId": "432bff1b-89b2-4df8-8298-0b6ae5e92945" } ``` > [!NOTE] > > `bankAccountId` is **required** when `primaryFundingSource` is `BANK_ACCOUNT`. Omitting it fails the request with `ARG-0001`. Bank accounts are linked through Plaid — see [Linking Bank Account via Plaid](/features/link-via-plaid) and [Manage linked bank accounts](/features/link-external-bank-accounts). To read the accounts already linked, use [View Funding Sources](/features/view-funding-sources). ### 3. Prepayment balance The prepayment balance is a non-withdrawable balance usable toward gift card and virtual card purchases. It contributes automatically unless you switch it off. ```json theme={null} { "usePrepaymentBalance": false } ``` > [!NOTE] > > **This balance goes by several names.** It appears as `usePrepaymentBalance` on card input, as `giftCardCashBalance` in the [`getWallet`](/features/check-account-balance) response, and as "prepaid (gift card) balance" in prose. They are the same pool of money. ### 4. Rewards balance Accumulated cashback and bonus rewards. Like the prepayment balance, it contributes by default. ```json theme={null} { "useRewardsBalance": false } ``` Leaving rewards on maximizes the use of money already earned. Switching it off keeps rewards intact for withdrawal and makes card spend attributable to a single budget — which is usually what a business or platform program wants. *** ## Reference table | Field | Type | Default | Controls | | :- | :- | :- | :- | | `primaryFundingSource` | `VirtualCardFundingSource` | `FLUZ_BALANCE` | Whether the card pulls from Fluz-held funds or an external bank. | | `bankAccountId` | `UUID` | — | Which linked bank account. Required when source is `BANK_ACCOUNT`. | | `userCashBalanceId` | `UUID` | — | Which spend account the card draws from. | | `usePrepaymentBalance` | `Boolean` | `true` | Whether the prepayment balance may contribute. | | `useRewardsBalance` | `Boolean` | `true` | Whether the rewards balance may contribute. | *** ## Isolating a card to one spend account The complete configuration for a card that can only ever spend from one nominated budget: ```json theme={null} { "input": { "idempotencyKey": "07df5653-43a8-4532-9881-3ab5857bbe13", "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "spendLimit": 150.00, "primaryFundingSource": "FLUZ_BALANCE", "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128", "usePrepaymentBalance": false, "useRewardsBalance": false } } ``` All three of `userCashBalanceId`, `usePrepaymentBalance: false`, and `useRewardsBalance: false` are required for true isolation. Setting the spend account alone is not enough. *** ## Reading what is available Before issuing, check that the source you are about to nominate actually has funds. [`getWallet`](/features/check-account-balance) returns every balance and linked funding source in one call. ```graphql theme={null} query { getWallet { bankAccounts { bankAccountId } balances { rewardsBalance { availableBalance } cashBalance { availableBalance } giftCardCashBalance { availableBalance } userCashBalances(paginate: { limit: 20, offset: 0 }) { userCashBalanceId nickname availableCashBalance status } } } } ``` Use `availableCashBalance` rather than `totalCashBalance` — the total can include funds that are pending and not yet spendable. *** ## Changing funding after issuance [`editVirtualCard`](/features/edit-virtual-card) accepts `primaryFundingSource` and `bankAccountId`, so you can move a card between Fluz balance and an external bank, or swap which bank account it pulls from. ```json theme={null} { "input": { "idempotencyKey": "d3b07384-d9a0-4f1e-9b2c-7a5e8c1f2d64", "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "primaryFundingSource": "BANK_ACCOUNT", "bankAccountId": "432bff1b-89b2-4df8-8298-0b6ae5e92945" } } ``` > [!WARNING] > > **Balance composition appears to be set at issuance only.** `userCashBalanceId`, `usePrepaymentBalance`, and `useRewardsBalance` are not in the published `EditVirtualCardInput`. If you need a card pointed at a different spend account, plan to issue a replacement rather than edit the existing card. *** ## Account-level defaults are a different thing Fluz also has an account-wide primary and backup payment method, set with [`setPrimaryFundingSource`](/features/primary-and-backup-funding) and `setBackupFundingSource`. These are **not** the same as per-card funding: | | Scope | Set with | | :- | :- | :- | | **Per-card funding** | One virtual card | `createVirtualCard` / `editVirtualCard` | | **Account primary/backup** | Account-wide default, used when nothing more specific applies | `setPrimaryFundingSource` / `setBackupFundingSource` | The backup payment method exists to cover the case where the primary source cannot be charged. See [Primary & Backup Funding](/features/primary-and-backup-funding). *** > **[Create Spend Accounts](/features/create-spend-accounts)** > > Add a new spend account to fund cards from. > **[Link Bank Accounts via Plaid](/features/link-via-plaid)** > > Connect an external bank account for `BANK_ACCOUNT` funding. > **[Check Account Balance](/features/check-account-balance)** > > Read every balance and linked source before issuing. > **[Open Loop Card Funding](/features/open-loop-cards/open-loop-card-funding)** > > How funding differs on hosted open-loop cards. # Simulate virtual card transactions Source: https://docs.fluz.app/Simulate-Virtual-Card-Transactions Put a test spend on a staging virtual card — how to request an authorization, clearing, decline, or refund, and how to verify the result over the API. Staging virtual cards are real card records with real BINs, but they aren't on a live payment network — no merchant can swipe them. To exercise your spend, balance, and reconciliation logic, Fluz injects an authorization against your card through the issuer processor's test environment. Everything downstream of that point is the production code path: the same authorization service, the same spend controls, the same ledger entries, the same webhooks. > [!NOTE] > > **Staging only** > > Simulated transactions exist only in the staging environment (`https://transactional-graph.staging.fluzapp.com/api/v1/graphql`). No funds move, no interchange is generated, and nothing is submitted to Mastercard. In production, transactions arrive only from real merchant activity. > [!WARNING] > > **Simulations are triggered by Fluz** > > There is no public mutation that injects a transaction onto a card. Authorization simulation happens in the issuer processor's dashboard, which sits inside Fluz's PCI environment and isn't exposed to partners. Request the transactions you need through your integration channel (shared Slack channel or [partnerships@fluz.app](mailto:partnerships@fluz.app)) and we'll run them against your card, usually the same business day. Everything else on this page — issuing the card, reading the result — is fully self-serve. ## Before you request a simulation An authorization runs through the full control stack, so a card that isn't set up correctly will decline for reasons that have nothing to do with your test. **1. The account has passed KYC** Cards can only be issued — and only authorize — on a verified account. See [Testing KYC Flows](/test-kyc-flows) for identities that return a pass. **2. You have an ACTIVE card** Issue one with `createVirtualCard` using an offer from [Test Virtual Card Offers](/test-virtual-card-offers). Hold on to the `virtual_card_id` — it's how we locate the card. **3. The card is funded and unlocked** The card draws on your Fluz balance. Confirm the spend limit covers your test amount and that the card hasn't been locked, expired, or spent down. `getVirtualCardBalance` shows you `remainingBalance` at a glance. ## What you can simulate Ask for whichever leg of the lifecycle you need. Each maps to a distinct set of records and webhooks on your side. > **Authorization (approval)** > > The base case: a purchase at a merchant you name, for an amount you name. The card's available balance is reduced immediately and the transaction lands in a pending state. Use this to verify that spend controls, balance decrementing, and your `TRANSACTION_CREATE` handler all behave. > **Clearing (capture)** > > The settlement leg that follows an authorization, sometimes days later in the real world. We can either capture in a single step alongside the authorization, or leave the authorization open so you can observe the pending state and then request the capture separately. The second option is the more faithful rehearsal of production. > **Decline** > > A transaction the authorization service rejects. Tell us which decline you want to see — a control-driven decline (over the spend limit, wrong merchant on a brand-locked card, locked card) or an authentication decline (CVV mismatch). Declines surface a `declineReason` and `declineCategory`; see [Decline Codes](/features/decline-codes) for the full set. > **Reversal** > > An authorization released before it clears — the merchant abandoned the sale, or the terminal timed out. The held amount returns to the card. Worth testing if you reconcile on authorizations rather than clearings. > **Refund** > > Value returned to the card after a purchase has cleared, in full or in part. Appears as a `REFUND` transaction type rather than a reduction of the original purchase, so your ledger needs to handle it as a separate record. > **Zero-dollar and AVS checks** > > Some merchants probe a card with a $0.00 or $0.01 authorization before charging it. These appear as their own records and are reversed shortly after. If your reconciliation sums authorizations, test this case — it's a common source of double-counting. ## What to send us The more of this you provide, the fewer round trips. | Field | Required | Notes | | - | - | - | | `virtual_card_id` | Yes | Returned by `createVirtualCard`. Card last four also works. | | Amount | Yes | In USD. | | Outcome | Yes | Approve or decline — and if decline, which reason you want to exercise. | | Merchant name | No | Defaults to a generic test merchant. Set it if you match on descriptors. | | MCC | No | Set it if you're testing category-based logic. | | Single-step clearing | No | On to capture immediately; off to leave the authorization pending. | ## Verifying the result Once we confirm the simulation has run, everything is readable over the API. Nothing about reading a simulated transaction differs from reading a real one. ```graphql Transactions on the card theme={null} query { getVirtualCardTransactions(input: { virtualCardIds: [""] filters: { transactionTypes: [PURCHASE, REFUND, DECLINE] } }) { virtualCardId transactions { transactionId transactionDate transactionType transactionStatus transactionAmount transactionApproval transactionResponseCode merchantName merchantDescriptor mcc } } } ``` ```graphql Balance after the spend theme={null} query { getVirtualCardBalance(input: { virtualCardIds: [""] }) { virtualCardId spentAmount remainingBalance spendLimit spendLimitDuration } } ``` A simulated purchase should show up as a `PURCHASE` row with a matching drop in `remainingBalance`. A decline shows up under the `DECLINE` type and leaves the balance untouched — declines are also queryable in isolation through [Get Declined Transactions](/features/get-decline-transactions). > [!TIP] > > Card-level queries only cover card activity. To see the same event in the account's unified ledger alongside deposits and transfers, use `getTransactions` — see [Transactions Overview](/features/transactions-details-overview). ### Webhooks Simulated transactions fire the same events as real ones, which makes this the cleanest way to test your endpoint end to end: | Event | Fires when | | - | - | | `TRANSACTION_CREATE` | The authorization is approved. `status` is `PENDING`. | | `TRANSACTION_UPDATE` | The transaction clears. `status` moves to `SETTLED`. | | `TRANSACTION_DECLINE` | The authorization is rejected, with `declineReason` and `declineCategory`. | If you asked for an authorization without single-step clearing, you should see `TRANSACTION_CREATE` on its own and `TRANSACTION_UPDATE` only after the capture is run. See [Webhooks](/fluz-dashboard/webhooks) for payloads and signature verification. ## Troubleshooting | What you see | Likely cause | | - | - | | Declined when you expected an approval | Spend limit below the amount, card locked, insufficient Fluz balance, or a brand-locked card at the wrong merchant. | | Nothing appears on the card | The simulation ran against a different card. Confirm the `virtual_card_id` you sent. | | Transaction stays pending | Expected — the authorization hasn't been captured. Request the clearing leg. | | No webhook received | Check the subscription and required scopes; the transaction itself is still visible over the API. | | Balance doesn't move on a \$0.01 charge | Expected for an AVS probe. It reverses on its own. | ## Next steps > **[Your First Virtual Card Purchase](/quickstart/create-and-spend-with-a-virtual-card)** > > The full happy path — pick a program, issue a card, reveal it, and track its spend. > **[Get Virtual Card Transactions](/features/get-virtual-card-transactions)** > > Filter card activity by type and date range, and read every field on a transaction. > **[Decline Codes](/features/decline-codes)** > > Every decline reason and category, and what your app should do with each. > **[Webhooks](/fluz-dashboard/webhooks)** > > Subscribe to transaction events, verify signatures, and handle retries. **Want to learn more?** Contact us at [partnerships@fluz.app](mailto:partnerships@fluz.app). Speak with our experts for more info or to request a demo. # API explorer Source: https://docs.fluz.app/api-explorer # Catalog fields and descriptions Source: https://docs.fluz.app/catalog-fields ## Merchant Table | Field Name | Constraints | Description | Table | | - | - | - | - | | `merchant_name` | Text | This is the name of the specific gift card merchant. In the event that you are calling for a catalog that will show both fixed and variable offers on the same merchant, there will be a unique record for each offer type on each merchant. | Merchant | | `merchant_slug` | Text | This is the shortened name for the merchant. It can be used for passing the purchases to Fluz via API for a specific merchant. It is also used to generate the merchant's URL on the Fluz web application. The URL structure is [https://fluz.app/store/merchant\_slug](https://fluz.app/store/merchant_slug) | Merchant | | `updated_at` | Date, time stamp | Last update made to that record on the merchant table | Merchant | | `created_at` | Date, time stamp | Date this merchant was added to our catalog | Merchant | | `merchant_status` | Active or Inactive | This will indicate if a merchant is available for purchase at this time. A merchant's status can either be active or inactive. If a merchant is archived it will be removed from the catalog export entirely. | Merchant | | `logo_url` | URL | Image of the merchant logo. This will be a square image of the merchant logo. It will be a vector image at 300 x 300 pixels. | Merchant | | `faceplate_image` | JSON | JSON obj of images of different sizes for the gift card. This will be the official images of the gift card as provided by the merchant. | Merchant | | `primary_color` | Hex Code | For merchandising within your experience, Fluz will provide you with the primary color in the merchant logo image. | Merchant | | `type` | Gift Card or Card Linked Offer | Fluz has both gift card offers and card linked offers. This is based on the specific merchant. Each merchant can have either option. This field will confirm the offer type as either a gift card offer or card linked offer. | Merchant | | `category` | List | This is the mapping of the merchant within the overall Fluz catalog. | Merchant | | `daily_limit-consumer` | Number | This is the total maximum dollar amount that can be purchased on a single merchant in a single day. This is by a single user account that is registered as a consumer account. A calendar day is run based off UTC time zones. The limits will reset daily. | Merchant | | `daily_limit-commercial` | Number | This is the total maximum dollar amount that can be purchased on a single merchant in a single day. This is by a single user account that is registered as a commercial account. A calendar day is run based off UTC time zones. The limits will reset daily. | Merchant | | `monthly_limit-consumer` | Number | This is the total maximum dollar amount that can be purchased on a single merchant in a single calendar month. This is by a single user account that is registered as a consumer account. A calendar month is run based off UTC time zones. It will run from the first of the month to the last day of the calendar month. The limits will reset monthly. | Merchant | | `monthly_limit-commercial` | Number | This is the total maximum dollar amount that can be purchased on a single merchant in a single calendar month. This is by a single user account that is registered as a consumer account. A calendar month is run based off UTC time zones. It will run from the first of the month to the last day of the calendar month. The limits will reset monthly. | Merchant | | `website_url` | URL | This is URL of that specific merchant. Please note, if the merchant has multiple URLs or sub domains, Fluz will be providing a single URL for that given merchant. | Merchant | | `redemption_information` | Text | These are details on how to redeem the gift card at that specific merchant. These can be specific | Merchant | | `parent_company` | Text | If the specific merchant's gift card is able to be used at other retail brands, we will list the name of the parent company that | Merchant | | `redeemable_at_other_brands` | List | What other merchants this gift card is redeemable at. Sometimes it will be other child merchants under the same parent merchant. But other times it will be an assortment of merchants for a multi brand card. Examples: Uber gift cards are redeemable at Uber + Uber Eats Gap gift cards are redeemable at Gap + Old Navy + Banana Republic + Athleta Happy Card Retail Therapy cards are redeemable at Lululemon, Macys, Nike, Boohoo, etc | Merchant | | `merchant_gift_card_redemption_url` | Text | If the merchant has a specific URL to apply the gift card balance directly to your account with that merchant, that specific URL will be provided in this field. | Merchant | | `description` | Text | This is the fully fleshed out description of the merchant. | Merchant | | `short_description` | Text | This is the shortened version of the description on that merchant. | Merchant | | `merchant_support_phone_number` | Phone Number | If the merchant provides a dedicated phone number address to support issues regarding their gift card program, that information will be provided here. | Merchant | | `merchant_support_email` | Email address | If the merchant provides a dedicated email address to support issues regarding their gift card program, that information will be provided here. | Merchant | ## Offer Table | Field Name | Constraints | Description | Table | | - | - | - | - | | `offer_id` | UUID | This is the public offer ID that you will reference | Offer | | `rate` | number | This is the best rate on the respective merchant. If the fixed and variable have the same rate, then just include the variable. If variable is less than fixed value, include both the variable and fixed value offers, unless you are calling the catalog that just has the variable value offers only. In the event that the merchant has multiple different fixed value denomination offers with different rates, this will have a separate offer for each denomination + rate combo. | Offer | | `denomination_type` | Fixed or Variable | This field will be corresponding to the denomination of that particular offer. Each highest value offer will have their own record. | Offer | | `denominations` | number range | This field will be corresponding to the denomination of that particular offer. **Variable Value Offers** - We will provide two values for denominations. The first value will be the lower limit of the offer. The second value will be the upper limit of the offer. When placing an order you can pass any value between that range. **Fixed Value Offers** - We will list all denominations on that offer. Please note if there are different rates on a various fixed values of a merchant, each one will be a separate offer. In the respective offer, we will list the values on that specific rate. | Offer | | `card_format` | URL or code | Some of merchants only allow for a hosted URL for the gift card delivery. In that case, we will tell you the format you can expect. | Offer | | `barcode_type` | 128, UPC, QR, None | Some retailers require a barcode or QR code and some of these retailers require manipulation of the gift code to produce the barcode number, such as having a prefix, dropping some digits, or adding the PIN. To remove this complexity from our clients we return the barcode information in our Issue response data. | Offer | | `currency` | Text | This is the currency that the value on the gift card is tied to. | Offer | | `countries` | Country list | This is saying which countries the gift cards can be redeemed in *Please note that the underlying value on each gift card will be tied to the currency listed on the gift card. Even if it works in other countries that generally operate in a different currency, the value will be tied to the listed currency.* | Offer | | `gift_card_expiration_window` | Number | This is the amount of months that the gift card expires in. This is based on the months from card issuance. If null, that means that the gift card does not expire. | Offer | | `merchant_terms` | Text | These are the terms of use provided by the merchant on the terms of use for the specific merchant's gift card program. | Offer | # Changelog Source: https://docs.fluz.app/changelog Recent updates, additions, and breaking changes to the Fluz API. > [!NOTE] > > Subscribe to updates on the [Fluz Status Page](https://you-up.fluz.app/) for incidents and proactive change notifications. You can also subscribe to this changelog via [RSS](/changelog/rss.xml). **v0.0.85** October 2026 * New webhook event `OAUTH_USER_UNLINKED`, currently available in **staging only** — it is not yet delivered in production. Fired once when a user's OAuth link to your application is revoked, whether the user removes your app under **Apps & Integrations** or Fluz revokes the link on their behalf. The payload carries `accountId`, `userId`, `appId`, `externalReferenceId`, `reason` (`USER_REVOKED` or `ADMIN_REVOKED`), and `occurredAt`; it never includes contact details or scopes. No scope is required, and like `OAUTH_USER_LINKED` it is delivered to public/OAuth and widget apps only. Once it fires, the user's access and refresh tokens are rejected, `generateUserAccessToken` with the revoked `externalReferenceId` is refused, and no other events are delivered for that user until they link again, which fires `OAUTH_USER_LINKED` as usual. See [OAuth user unlinked](/fluz-dashboard/webhooks#oauth-user-unlinked-oauth_user_unlinked). **v0.0.84** October 2026 * The `OAUTH_USER_LINKED` webhook event now carries `spendAccountAccess`, currently available in **staging only** — it is not yet delivered in production. It states which of the user's spend accounts the link can act on: `restricted: true` with the granted `userCashBalanceIds`, or `restricted: false` with `userCashBalanceIds: null` when the grant covers all of the user's spend accounts, current and future. Restricted grants also carry `includesFutureSpendAccounts`, which says whether spend accounts the user opens later join the grant automatically. Use it to start calling spend-account operations such as `getSpendAccountPaymentInstructions` right after the link without a follow-up lookup. IDs only — nicknames and balances come from `getWallet`, which remains the source of truth for the permitted spend accounts. The event fires again with the updated value whenever the grant changes, including a change of granted spend account alone. Additive and backward-compatible. See [Spend account access](/fluz-dashboard/webhooks#spend-account-access). * In `registerUser` consent mode, the person must now sign in with the phone number you submitted to approve the `authorizationUrl`. Before, the number was only pre-filled. If they sign in with a different number, Fluz shows them an error and creates no grant, and `registerUser` keeps returning `PENDING_CONSENT`. Submit the phone number the person uses with Fluz. See [Consent mode](/api-reference/mutations/register-user#consent-mode). **v0.0.83** October 2026 * Widget tokens now identify your application with an `appId` claim, the App ID shown on your app's **Overview** tab. Stop signing `apiKey` into the token: your API key is your application's credential, and the token is visible in the user's browser. Tokens that still carry `apiKey` keep working for now. See [Set Up Your Server](/developers/setting-up-your-server#token-claims). * The widget script 1.3.0 no longer takes `apiKey` in `FluzEmbedded.init()`, and it ignores the value if you still pass it. Update the script URL to `?v1.3.0` and remove `apiKey` from your `init()` call. See [Adding the JS Widget](/developers/adding-the-js-widget-to-your-page). **v0.0.82** October 2026 * `registerUser` has a new consent mode, which Fluz turns on per application. In consent mode, `registerUser` creates no user, account, or grant when you call it. It returns `status: PENDING_CONSENT` with an `authorizationUrl` and `expiresAt`, where the person signs up or logs in, with the phone number you submitted pre-filled, and approves your application's scopes. You then call `generateUserAccessToken` with your `externalReferenceId`. The response is the same whether or not the person already has a Fluz account, and `AUTH-0026` and `AUTH-0027` are never returned in this mode. If your application already holds a grant for the `externalReferenceId`, the response is `status: LINKED` with the `userId` and `accountId`. New arguments: `externalReferenceId` (required in consent mode), `steps`, and `callbackUrl`. `dateOfBirth`, `billingAddress`, and `acceptCardholderAgreement` are now nullable in the schema; direct registration still requires them, and leaving one out now returns `ARG-0002` or `ARG-0001` in `data` instead of a GraphQL validation error. New response fields: `status`, `authorizationUrl`, `expiresAt`. Additive and backward-compatible for applications that are not switched. See [Consent mode](/api-reference/mutations/register-user#consent-mode). * The enrollment widget token now accepts an empty `steps` array, which ends the flow at consent. See [Choosing the steps](/developers/enrollment-widget#choosing-the-steps). **v0.0.81** September 2026 * The enrollment widget token accepts an optional `targetFundingSource` claim, currently available in **staging only** — it is not yet supported in production. Set it to `BANK_ACCOUNT`, `BANK_CARD`, or `PAYPAL` to open that funding source's flow the first time the user reaches the `FUNDING_SOURCE` step; it changes only where the step starts and does not restrict the user to that type. Requires `FUNDING_SOURCE` in `steps`. An unknown value, or a type that is not enabled for your application, rejects the session. Additive and backward-compatible. See [Opening a specific funding source](/developers/enrollment-widget#opening-a-specific-funding-source). **v0.0.80** September 2026 * The `OAUTH_USER_LINKED` webhook event now carries the linked user's `emailAddress` and `phoneNumber`, currently available in **staging only** — they are not yet delivered in production. This lets you provision the connected user without a follow-up lookup. Either field is `null` when the user has no value on file, and both are absent in the rare case the details can't be loaded, so treat an absent field as unknown rather than as `null`. The phone number format is not normalized, so normalize it before matching against your own records. Additive and backward-compatible. See [Webhooks](/fluz-dashboard/webhooks). **v0.0.79** September 2026 * Transaction webhooks now carry `externalReferenceId`, currently available in **staging only** — it is not yet delivered in production. Present on `TRANSACTION_CREATE`, `TRANSACTION_UPDATE` and `TRANSACTION_DECLINE`, resolved from the OAuth grant between the user and your application, so the same event reaches each application carrying that application's own identifier for the user. Absent for private-app deliveries and for grants created without an external reference, so handle a missing value. `FUNDING_SOURCE_STATUS_UPDATE` also emits it and is now documented as such. Additive and backward-compatible. See [Webhooks](/fluz-dashboard/webhooks#transaction-created-transaction_create). **v0.0.78** September 2026 * `Virtual Card Return` events now carry `isReversal`: `true` for a released authorization hold, whether the card issuer or Fluz released it, and `false` for a merchant refund. Returns pushed before 2026-09-25 20:15 UTC carry no value; for those, and for the few returns we still cannot classify, use `referenceId` and the original authorization to tell a released hold from a refund, as described under [Reversals vs refunds](/fluz-dashboard/webhooks#reversals-vs-refunds). Additive and backward-compatible. **v0.0.77** September 2026 * Bring Your Own Integration for Plaid (BYOI Plaid). Your application can now use its own Plaid client ID and secret for the bank links that your users make through it. The developer of the application registers the credentials with [`registerPlaidIntegration`](/api-reference/mutations/register-plaid-integration), reads them with [`getPlaidIntegration`](/api-reference/queries/get-plaid-integration), and turns BYOI Plaid off with [`disablePlaidIntegration`](/api-reference/mutations/disable-plaid-integration). These three operations need the new `BYOI_PLAID` scope. Applications with a Plaid integration repair a disconnected bank link with [`createPlaidRelinkToken`](/api-reference/mutations/create-plaid-relink-token) and [`completePlaidRelink`](/api-reference/mutations/complete-plaid-relink), which need `MANAGE_PAYMENT`. The bank linking flow does not change, and bank links made with Fluz Plaid continue to work. Additive and backward-compatible. See [BYOI Plaid](/features/byoi-plaid). **v0.0.76** September 2026 * Merchant categories are now available over the API, so you no longer need a CSV export to show or filter by category. [`getMerchants`](/api-reference/queries/get-merchants) returns `categories` on each merchant — a list, because a merchant can belong to more than one, empty when it is uncategorized — and accepts a `categoryId` argument to return only the merchants in one category. The new [`getMerchantCategories`](/api-reference/queries/get-merchant-categories) query lists the categories with their ids, which is the same taxonomy shown in the Fluz catalog. Category ids are stable, so store the id rather than the name if you cache the mapping. Both require the `LIST_OFFERS` scope. Additive and backward-compatible. See [Get Catalog](/get-catalog). **v0.0.75** September 2026 * New webhook event `FUNDING_SOURCE_STATUS_UPDATE`, currently available in **staging only** — it is not yet delivered in production. Fired when a user adds, renames or removes a funding source. One event type covers all three cases — branch on `changeType` (`ADDED`, `UPDATED`, `DELETED`) rather than on the event name. The payload carries `fundingSourceId`, `fundingSourceType` (`BANK_ACCOUNT`, `BANK_CARD`, `PAYPAL`), `brand`, `lastFour`, `nickname`, `status`, and timestamps; full card and account numbers are never included. Requires the `LIST_PAYMENT` scope, and is delivered to private apps as well as OAuth apps. It is emitted from the change to the funding source itself, so it fires wherever the user made the change — mobile app, web app, or embedded widget. The user's Fluz cash balance is not a funding source and produces no event. See [Webhooks](/fluz-dashboard/webhooks). **v0.0.74** September 2026 * Transaction webhooks now carry `isReversal`. It is `true` for an authorization reversal, `false` for a transaction we classified as something else, and absent when we could not classify it. Present on `TRANSACTION_CREATE`, `TRANSACTION_UPDATE` and `TRANSACTION_DECLINE`. Coverage is still partial: today only card verification voids are flagged `true`, and returns carry no value at all, so the field cannot yet tell a released hold apart from a merchant refund. Treat an absent value as unknown, never as `false`. Additive and backward-compatible. See [Webhooks](/fluz-dashboard/webhooks). **v0.0.73** September 2026 * [`createTransfer`](/api-reference/mutations/create-transfer) and [`requestAccountTransfer`](/api-reference/mutations/request-account-transfer) accept optional `useRewardsBalance` on their input types. When `true`, the send is funded from the sender's rewards balance in addition to the resolved spend account (cash first, rewards for the remainder). Omit or pass `false` for the existing cash-only behavior. There is no prepayment field on these mutations. Additive and backward-compatible. See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). **v0.0.72** September 2026 * [`getUserPurchases`](/api-reference/queries/get-user-purchases) now accepts `purchaseDisplayId` on its `filter` argument, so you can fetch a single order by the reference ID returned at purchase time instead of paginating your history to find it. It is the same value exposed as `referenceId` on [`getTransactions`](/api-reference/queries/get-transactions), and the purchase record carries the full details — `seatRewardValue` for cashback, the payment breakdown, and the linked `giftCard` — in one call. The filter applies on top of `scope` and only narrows it. `scope` values combine with AND, so passing both `TOKEN_USER` and `TOKEN_ACCOUNT` returns only purchases the token's user made on the token's account — an order placed by another user is returned when `scope` is `TOKEN_ACCOUNT` on its own. Additive and backward-compatible. See [`UserPurchaseFilterInput`](/api-reference/types/user-purchase-filter-input). **v0.0.71** September 2026 * [`getVirtualCardBalance`](/api-reference/queries/get-virtual-card-balance) now returns `status`, `locked` and `authorizationSetting` alongside the spend figures, so you can read card state for many cards in one call instead of calling `revealVirtualCardByVirtualCardId` per card and hitting the issuer's rate limit. A locked card still reports `status: ACTIVE`, so use `locked`, which covers locks set by the cardholder, by Fluz once the lock date passes, and by Fluz support. `authorizationSetting` is the same object reveal returns. Additive and backward-compatible. See [Get Virtual Card Balance](/recipes/get-virtual-card-balance). **v0.0.70** August 2026 * Documented how debit cards are used for money movement. Deposits from a debit card into `CASH_BALANCE` may be processed as account funding transactions (AFT), which carry their own fee rate — read `depositFee` from the response rather than computing it. Push-to-card withdrawals (`method: BANK_CARD`) are delivered as original credit transactions (OCT). Card eligibility for either is not available in advance and is independent per direction — a card a user can deposit from is not necessarily a card you can send a withdrawal to. See [Deposit From External Accounts](/features/deposit-from-external-accounts) and [Withdraw to External Account](/features/withdraw-to-external-account). * `isExpedited` on `WithdrawCashBalanceInput` now controls push-to-card delivery speed: `true` delivers to the card immediately, `false` or omitted settles on the standard schedule. A standard push-to-card withdrawal can still fail after it is submitted and be refunded to the source balance — so a `PENDING` withdrawal is not yet final. * Documented [`getWithdrawFeeEstimate`](/api-reference/queries/get-withdraw-fee-estimate) on [Withdraw to External Account](/features/withdraw-to-external-account) for previewing `fee`, `netAmount`, `feePercent`, `feeMaxCap`, and `settlementDelayDays` before submitting a withdrawal. * Corrections to [Withdraw to External Account](/features/withdraw-to-external-account): the required scope is `MAKE_WITHDRAWAL` (not `MANAGE_PAYMENT`), error codes are the real `WDR-*` / `ARG-*` / `HN-*` / `BC-*` / `G-*` codes, and `FLUZPAY` — though present on the `WithdrawMethods` enum — is not a usable withdrawal method: `withdrawCashBalance` rejects it with `WDR-0004` and `getWithdrawFeeEstimate` with `ARG-0001`. **v0.0.69** August 2026 * Digital wallet signal on transactions. The `TRANSACTION_CREATE` and `TRANSACTION_UPDATE` webhook events and the `getTransactions` API now include `digitalWalletProvider` (`APPLE_PAY`, `GOOGLE_PAY`, or absent) so you can tell when a card transaction was made through a mobile wallet, plus `digitalWalletToken` (the network token id). Use `digitalWalletProvider` as the wallet signal — `digitalWalletToken` is also set for non-wallet network tokens, so its presence alone does not mean a wallet was used. Additive and backward-compatible. See [Webhooks](/fluz-dashboard/webhooks). **v0.0.68** August 2026 * Open Loop Cards: recipients are no longer prompted to create a PIN during the initial activation flow, and the card is no longer auto-revealed on claim. Revealing a card now prompts the recipient to enter their PIN, or create one if they haven't set one yet. See [Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience). **v0.0.67** July 2026 * Update GenerateVCShareLinksInput for Open Loop Cards. See [Open Loop Cards](/features/open-loop-cards/send-open-loop-cards). **v0.0.66** July 2026 * Updates to [Primary & backup funding page](https://docs.fluz.app/features/primary-and-backup-funding). **v0.0.65** July 2026 * New API endpoint exposing account-related declined transactions. See [Get Declined Transactions](/features/get-decline-transactions). **v0.0.63** June 2026 * `GiftCard.purchaseId` (UUID) — the purchase that created the gift card; the same ID returned by `purchaseGiftCard` and `getUserPurchases`. Returns `null` if no associated purchase. * `GiftCard.purchaseDisplayId` (String) — the short, human-readable Fluz transaction ID (e.g. `1047283`) that support references in manual reviews and exports. Returns `null` if no associated purchase. * `GiftCard.purchaseValue` (Float) — the face value (denomination) the gift card was purchased at, in the card's currency. * `GiftCard.currentValue` (Float) — the remaining balance on the gift card; equals `purchaseValue` for single-use cards. * `GiftCard.currency` (String) — the ISO currency code of the gift card's value (e.g. `USD`). All five fields are now returned by `getGiftCards` (and anywhere `GiftCard` is exposed), enabling order/value reconciliation without calling `revealGiftCardByGiftCardId` per card. Additive and backward-compatible. **v0.0.62** June 2026 * Added Plaid Link support. * Updated Overview page with `llms.txt` information. * Updated header with AI link to `llms.txt`. **v0.0.60** May 2026 * New `getCardProvisioningUrl` **query** — mints a short, single-use URL that, when opened on a mobile device, launches Fluz and adds a virtual card to Apple Pay (iOS) or Google Pay (Android). Surface it as a QR code, SMS, email, or in-app button. Each URL expires \~5 minutes after creation. Requires the `CREATE_VIRTUALCARD` scope. * New input `GetCardProvisioningUrlInput { offerId, platform }`. * New enum `ProvisioningPlatform` (`IOS` | `ANDROID` | `OTHER`) — controls the desktop / unknown-device fallback only; iOS and Android opens are auto-routed. * New return type `CardProvisioningUrl { url, expiresAt }`. **v0.0.59** May 2026 * Added samples to [Generate Share Links](/features/generate-share-links). **v0.0.58** May 2026 * Added `addVirtualCardAddress` mutation to save billing addresses for virtual card issuance. * Updated `createVirtualCard` mutation to support creating cards on behalf of authorized users. * Added full flow using `registerUser`, `addAuthorizedUser`, `addVirtualCardAddress`, and `createVirtualCard`. * Retry pending issuer approvals with `VC-0020` and returned `addressId`. **v0.0.57** May 2026 * `redeemFluzGiftCard` — Redeem a Fluz Gift Card using a code. Credits `giftCardCashBalance`. Requires `MAKE_DEPOSIT`. See [Redeem a Fluz Gift Card](/features/redeem-fluz-gift-card). * `unlockVirtualCard` — Unlock a previously locked virtual card to restore transaction capability. Requires `EDIT_VIRTUALCARD`. See [Unlock a Virtual Card](/features/unlock-virtual-card). **v0.0.55** May 2026 **Transaction-related webhooks** * Added description for `TRANSACTION_CREATE`, `TRANSACTION_UPDATE` & `TRANSACTION_DECLINE` webhooks. * Included new fields after webhook enrichment (MCC-related, `virtualCardId`, etc.). **v0.0.54** May 2026 * Added `mcc`, `merchantCountryCode`, `originalCurrencyCode`, `originalCurrencyAmount`, and `currencyConversionRate` to `VirtualCardTransaction`. Now returned by `getVirtualCardTransactions`. * Made `virtualCardIds` optional on `getVirtualCardTransactions` — omit to query transactions across all cards on the authenticated account. Added `dateRangeStart` and `dateRangeEnd` filters. Default `limit` of 100 when `virtualCardIds` is omitted; `limit` is capped at 500. `transactionAmount` is now nullable for rows without a settled amount (e.g., AVS-only decline records). * Added `usePrepaymentBalance` and `useRewardsBalance` to `CreateVirtualCardInput`. Now accepted by `createVirtualCard`. Both default to `true` (existing behavior preserved). Set both to `false` to restrict a card to drawing only from the specified `userCashBalanceId`, excluding prepaid (gift card) and rewards funds. **v0.0.53** April 2026 * Added `shortDescription` (Merchant) and `termsAndConditions` (Offer). Now returned by `getMerchants` and `getOfferQuote`. * Added `deliveryFormat` (GiftCard). Now returned by `getGiftCards` — use `deliveryFormat` for purchased cards. **v0.0.52** April 2026 * New `setVirtualCardPIN` mutation, allowing a user to set their PIN on eligible virtual cards. See [Set a Virtual Card PIN](/recipes/set-virtual-card-pin). **v0.0.51** April 2026 * New `getUserCashBalances` & `getUserCashBalanceById` queries. These queries allow a user to retrieve a list of their spend accounts and a specific spend account created on their account. **v0.0.49** April 2026 **Share Virtual Cards API** * New "Share Virtual Cards" capability under Manage Virtual Cards. Contact sales for access. * Added `CREATE_SHARE_LINK` scope to the [application scopes](/fluz-dashboard/application-scopes) page. **v0.0.45** February 2026 **Business registration API** * New query `getBusinessCategories: [BusinessCategory]` for retrieving business categories, with response types `BusinessCategory` and `BusinessSubCategory`. * New mutation `registerBusiness(input: RegisterBusinessInput!): RegisterBusinessResult` for programmatic business registration. * New input types: `RegisterBusinessInput`, `BusinessLegalAddressInput`, `BusinessOwnerInput`, `OwnerAddressInput`. * New enums: `BusinessStructure` (`LLC`, `CORPORATION`, `PARTNERSHIP`, `SOLE_PROPRIETORSHIP`, `COOP`) and `BusinessAccountUsage`. * New response types: `RegisterBusinessResult { accountId, kybStatus, success, error }` and `RegisterBusinessError { message, code }`. * Both operations require authentication with the `REGISTER_BUSINESS` OAuth scope. **v0.0.44** January 2026 **Updates to `getMerchants` query** * The `getMerchants` query now returns all available offers properly. * The `getMerchants` query can now return an `exclusiveRateId` field (within the `offers` object). This value can be used in `purchaseGiftCard` to specify the exclusive rate offer you want to purchase with. **v0.0.43** December 2025 **Withdrawals & Spend Accounts APIs** * Added `withdrawCashBalance` mutation with `MAKE_WITHDRAWAL` scope requirement. * Added `WithdrawMethods` enum supporting `PAYPAL`, `BANK_ACH`, `BANK_CARD`, and `VENMO` methods. * Added `WithdrawSource` enum supporting `CASH_BALANCE` and `REWARDS_BALANCE` sources. * Added `WithdrawCashBalanceInput` with idempotency support, plus `Withdraw` and `WithdrawCashBalanceResponse` types with full withdrawal record details including fees, status, and timestamps. * Added `transferUserCashBalance` mutation with `MAKE_INTERNAL_TRANSFER` scope requirement, plus `TransferInternalBalanceInput` and `TransferInternalBalanceResponse` types. * Added `userCashBalanceId` to `EditVirtualCardInput` to enable changing the funding source to a specified spend account via `editVirtualCard`. * Added `userCashBalanceIds` to `TransactionFilterInput` for filtering transactions by spend accounts via `getTransactions`, and `userCashBalanceId` to `getGiftCards` for filtering gift cards by spend account. * Added `updateUserCashBalance` and `closeUserCashBalance` mutations with `MANAGE_PAYMENT` scope requirement, plus supporting input and response types (`UpdateUserCashBalanceInput`, `CloseUserCashBalanceInput`, `CloseUserCashBalanceResponse`, `ClosedUserCashBalance`, `CloseUserCashBalanceResponseVirtualCard`). * Added application action logging for audit trail. **v0.0.42** November 2025 * Added new `userCashBalances` field to the `UserBalances` type in the `getWallet` query. Returns detailed information about individual user cash balance accounts with pagination via `paginate` (limit/offset), ordered by creation date (most recent first). Includes `userCashBalanceId`, `totalCashBalance`, `availableCashBalance`, `lifetimeCashBalance`, `nickname`, `status`, and `createdAt` — enabling tracking of multiple cash balance accounts per user with custom nicknames and status monitoring. **v0.0.40** November 2025 * A new field `userCashBalanceId` is added to `DepositCashBalanceInput` to specify the cash balance (spend account) to deposit the funds into. **v0.0.39** November 2025 * New mutations for managing saved bank cards: `updateBankCardNickname`, `updateBankCardPreferredMerchantCategoryCode`, and `deleteBankCard`, with corresponding input types. * `nickname` field added to the `BankCard` type. * Optional fields added to `AddBankCardInput`: `nickname` and `preferredMerchantCategoryCode` (MCC, normalized to 4 digits). **v0.0.37** September 2025 * `getVirtualCardBalance`: new query retrieving balance information for a list of virtual cards, including `spentAmount`, `remainingBalance`, `spendLimit`, and `spendLimitDuration`. * `getVirtualCardTransactions`: new query retrieving transactions for a list of virtual cards, with filters such as `transactionTypes`. **v0.0.36** September 2025 * The `getUserPurchases` query now includes the ability to filter the purchases returned in the response via a new optional `UserPurchaseFilterInput` variable. Filter purchases made by the user, the account, or both. **v0.0.35** September 2025 * The `revealVirtualCardByVirtualCardId` mutation response now includes a new `authorizationSetting` field. * **Bulk Virtual Card API**: new API for creating and managing a large number of virtual cards asynchronously. * `createVirtualCardBulkOrder` **mutation**: submit a bulk order and receive a unique `orderId`. * `getVirtualCardBulkOrderStatus` **query**: poll to track order status and retrieve card details upon completion. * The bulk card creation process is asynchronous — check order status to retrieve card details. **v0.0.32** July 2025 * **New query `getVirtualCardOffers`**: comprehensive list of all active virtual card offers available for creation, including `offerId` (essential for card creation), `programName`, `bankName`, `rewardValue`, and detailed program limits (daily, weekly, monthly spend limits). * **Updated mutation `createVirtualCard`**: programmatically issue virtual cards by providing an `offerId` (from `getVirtualCardOffers`), a `spendLimit`, and customizable attributes like `cardNickname` and `spendLimitDuration`. * **New mutation `editVirtualCard`**: update key parameters of an existing virtual card — `spendLimit`, `spendLimitDuration`, `lockDate`, `lockCardNextUse`, and `cardNickname`. * **New mutation `lockVirtualCard`**: immediately lock a virtual card, preventing any further transactions. **v0.0.31** July 2025 * The `Offer` type in the `getMerchants` response now includes a `deliveryFormat` field specifying how the offer is fulfilled: `URL`, `CODES`, `PIN_AS_CODE`, or `PIN_WITH_URL`. * The `getMerchants` query has been enhanced with a new `filterBy` argument, allowing you to filter results to only include merchants with offers matching a specific `deliveryFormat`. **v0.0.30** 2025 * Enhanced the `getMerchants` query with a new `offerTypes` input argument, allowing filtering by `giftCardOffer`s, `cardLinkedOffer`s, or both. By default, if `offerTypes` is not provided, the query returns only gift card offers. **v0.0.29** 2025 * Introduced a new query `getReferralUrl` which returns a referral string based on merchant input, with a new `MerchantInput` type. * Introduced an automatic repurchase attempt on the `purchaseGiftCard` mutation. If a gift card purchase using a bank card, bank account, or PayPal fails, the system automatically attempts to repurchase using the Fluz balance. * Introduced the `defaultToBalance` field on `PurchaseGiftCardInput` to set the Fluz balance as the fallback payment method if the primary payment method fails. Defaults to `true`. **v0.0.27** 2025 * Introduced a new access scope, `CREATE_VIRTUALCARD`, to create virtual cards. * Introduced a new function, `createVirtualCard`, to create virtual cards with specific parameters. * Added new query, `getBINs`, to retrieve BINs associated with virtual cards. * Introduced a `merchantCategoryCode` input for cash balance deposits, allowing for enhanced categorization of transactions. * Added a new query, `getMccList`, to retrieve a list of merchant category codes. * Enhanced scope validation for the `getApplicationScopes` query and improved GraphQL type definition management. **v0.0.26** 2025 * Enhanced the handling of stock information for merchant offers: the `Offer` object's `stockInfo` field now returns either `StockInfoFixedType` or `StockInfoVariableType` depending on whether the offer is fixed or variable. * Enhanced error handling with more intuitive error message responses — improved responses for `purchaseGiftCard` no-offer errors and mismatched account and payment methods. **v0.0.17 – v0.0.21** 2025 **v0.0.21** * Introduced an `idempotencyKey` parameter for `depositCashBalance` and `purchaseGiftCards` to prevent duplicate requests. * Added `purchaseGiftCardInput` types to streamline parameter handling for transactions. * Introduced a new field `purchaseDisplayId` on the `UserPurchase` type, providing a display ID for purchases. * Fixed the `Offer` object's `offeringMerchantId` field to return `offering_merchant_id` instead of `offer_id`. **v0.0.20** * Added `accountId` requirement to `generateUserAccessToken`. * Implemented a new query, `getAccountsByUserId`, to fetch accounts based on `userId`. * Added a new `AccountType` enum to categorize accounts as `CONSUMER` or `BUSINESS`, plus new `Account`, `Business`, and `Seat` objects. **v0.0.17** * Fixed `depositCashBalance` platform channel input so the deposit transaction log source is logged as `API`. ## Change management Additive changes (new fields, new enum values, new operations) are announced at least **7 days** before release when they require action from integrators. Breaking changes are announced at least **14 days** in advance and are typically batched into major release windows. For a complete list of API operations, see the [API reference](/api-reference/overview). # Check account balance Source: https://docs.fluz.app/check-account-balance ## Account Balances Your Fluz account can have up to four different balances: 1. **Rewards balance** - Cashback and bonus rewards. Withdraw to an external account, or pass `useRewardsBalance` on `createTransfer` / `requestAccountTransfer` to send to another Fluz account. 2. **Cash balance (Spend Account)** - Withdrawable balances that you can use to fund your gift card and virtual card purchases, among other uses. 3. **Fluz prepayment balance** - A non-withdrawable balance you can use towards gift card and virtual card purchases. 4. **Reserve balance** - A balance held in Fluz to cover any transactions that fail to settle, ensuring your account remains in good standing. In order to see your current balances you can use the `getWallet` [query](/api-reference/queries/get-wallet). This can help you determine how much you want to deposit or apply towards a gift card purchase. The sum of these balances makes up your 'available Fluz balance', which determines the total amount you can use towards funding a payment on Fluz. ## User Cash Balance Accounts (Spend Accounts) You can access detailed information about individual cash balance accounts through the `userCashBalances` field. Monitoring account status and creation dates The `userCashBalances` field supports pagination and returns an array of cash balance accounts ordered by creation date (most recent first). ### Sample Response: ```json theme={null} { "data": { "getWallet": { "bankCards": [BankCard], "bankAccounts": [BankAccount], "paypalAccounts": [Paypal], "blockedPaymentTypes": ["BANK_CARD"], "balances": { "rewardsBalance": { "availableBalance": "50.00", "totalBalance": "50.00", "lifetimeBalance": "125.00" }, "cashBalance": { "availableBalance": "100.00", "totalBalance": "100.00", "pendingBalance": "0.00", "lifetimeBalance": "500.00" }, "giftCardCashBalance": { "availableBalance": "25.00", "totalBalance": "25.00", "pendingBalance": "0.00", "lifetimeBalance": "75.00" }, "userCashBalances": [ { "userCashBalanceId": "123e4567-e89b-12d3-a456-426614174000", "totalCashBalance": "100.00", "availableCashBalance": "100.00", "lifetimeCashBalance": "500.00", "nickname": "Primary Account", "status": "ACTIVE", "createdAt": "2024-01-15T10:30:00Z" }, { "userCashBalanceId": "223e4567-e89b-12d3-a456-426614174001", "totalCashBalance": "25.00", "availableCashBalance": "25.00", "lifetimeCashBalance": "75.00", "nickname": "Savings", "status": "ACTIVE", "createdAt": "2024-02-20T14:15:00Z" } ] } } } } ``` ### Query with Pagination: You can paginate through cash balance accounts using the `paginate` parameter: ```graphql theme={null} query { getWallet { balances { userCashBalances(paginate: { limit: 10, offset: 0 }) { userCashBalanceId totalCashBalance availableCashBalance lifetimeCashBalance nickname status createdAt } } } } ``` ### UserCashBalance Fields: | Field | Type | Description | | - | - | - | | `userCashBalanceId` | UUID | Unique identifier for the user cash balance account | | `totalCashBalance` | String | Total cash balance in the account | | `availableCashBalance` | String | Available cash balance that can be used for transactions | | `lifetimeCashBalance` | String | Cumulative total ever deposited into this account | | `nickname` | String | Custom nickname for the balance account (optional) | | `status` | UserCashBalanceStatus | Status of the cash balance account (e.g., ACTIVE, INACTIVE) | | `createdAt` | DateTime | Date and time when the account was created | # Create spend account for authorized user Source: https://docs.fluz.app/create-wallet-for-authorized-user Create a spend account on your account and assign an authorized user as its owner. Spend accounts are always created **on the caller's account** — there is no way to create one that lives on someone else's account. What you can do is create the spend account and then record an authorized user as its **owner**, so the account has a named person responsible for it. Unlike [`createVirtualCard`](/features/create-virtual-card-for-authorized-user), `createUserCashBalance` does **not** take an `authUserId`. Ownership is a separate, second call: [`assignObjectOwner`](/api-reference/mutations/assign-object-owner) with `objectType: SPEND_ACCOUNTS`. > [!NOTE] > > **Ownership is metadata, not access.** > > Assigning an owner records who is responsible for a spend account. It does not, on its own, grant that person the ability to spend from it. Effective access is still the higher of the user's account role ([`UACRoleType`](/api-reference/types/uacrole-type)) and any item-level access granted on the spend account itself. > > Assign the owner **and** make sure the user's role gives them the access you actually intend. *** ## Prerequisites **1. The authorized user exists and is ACTIVE** Add them with [`addAuthorizedUser`](/features/create-authorized-users) and confirm the returned `status` is `ACTIVE`. A `PENDING` assignment cannot be used as an owner — the user must accept the invite first. **2. Your token carries both scopes** `createUserCashBalance` requires `MANAGE_PAYMENT`. `assignObjectOwner` requires `MANAGE_SUBUSERS`. A single Bearer token needs both to run this flow end to end. **3. You have the authorized user's userId** `assignObjectOwner` is keyed on `userId`, not `authUserId`. See [Resolving the userId](#resolving-the-userid) below. *** ## Step 1 — Create the spend account Create the spend account exactly as you normally would. It is created on the caller's account with no owner attached. ```graphql theme={null} mutation CreateUserCashBalance($input: CreateUserCashBalanceInput!) { createUserCashBalance(input: $input) { userCashBalanceId nickname availableCashBalance status createdAt } } ``` ```json theme={null} { "input": { "nickname": "Ada — Field Ops" } } ``` Save the returned `userCashBalanceId`. That value is the `objectId` in Step 3. > [!NOTE] > > Give the account a nickname that identifies the owner. Ownership metadata is not surfaced in every list view, so a nickname like `"Ada — Field Ops"` keeps the account legible in [`getUserCashBalances`](/features/get-spend-accounts) without an extra lookup. *** ## Resolving the userId `assignObjectOwner` takes the authorized user's **`userId`** — the underlying user record. This is a different value from the **`authUserId`** returned by `addAuthorizedUser` and [`authorizedUsers`](/features/query-authorized-user), which identifies the UAC *role assignment*. The `AuthorizedUser` type does not currently expose `userId`. Today the documented ways to obtain it are: | Source | How you get it | | :- | :- | | [`createVirtualCard`](/features/create-virtual-card-for-authorized-user) | When called with `authUserId`, the response `userId` is the authorized user's user ID. | | [`registerUser`](/user-registration) | If your platform registered the user, persist the user ID at registration time and store it against your own record of that person. | > [!WARNING] > > Do not pass an `authUserId` where `userId` is expected. The two are both `UUID` and the mutation will not catch the substitution as a type error — you will get a failed or misdirected ownership assignment instead. *** ## Step 2 — Assign the authorized user as owner > **Restricted Access** > > This mutation requires a Bearer token with the `MANAGE_SUBUSERS` scope. ```graphql theme={null} mutation AssignObjectOwner( $objectType: ObjectOwnerObjectType! $objectId: UUID! $userId: UUID! ) { assignObjectOwner( objectType: $objectType objectId: $objectId userId: $userId ) { success objectOwnerId accountId objectType objectId userId createdAt error { code message } } } ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `objectType` | `ObjectOwnerObjectType!` | Yes | Use `SPEND_ACCOUNTS`. Other values are `VIRTUAL_CARDS`, `GIFT_CARDS`, and `FUNDING_SOURCES`. | | `objectId` | `UUID!` | Yes | The `userCashBalanceId` returned in Step 1. | | `userId` | `UUID!` | Yes | The authorized user's user ID. Must be a user on the caller's account. Not the `authUserId`. | > [!NOTE] > > `assignObjectOwner` only assigns an owner to an object that **does not have one yet**. If the spend account already has an owner, the call will not overwrite it — use [`transferObjectOwner`](#reassigning-ownership) instead. ### Sample response ```json theme={null} { "data": { "assignObjectOwner": { "success": true, "objectOwnerId": "3c7a1b52-9e4d-4f88-a2c1-5d6e7f8a9b01", "accountId": "b41e2d90-6a77-4c35-9f12-8e0d3a4b5c6d", "objectType": "SPEND_ACCOUNTS", "objectId": "f8a3c9e1-7b2d-4f5e-9c8a-1d2e3f4a5b6c", "userId": "f1320ac4-52dc-4c67-9e80-24e506b18450", "createdAt": "2026-08-10T14:22:00.000Z", "error": null } } } ``` ### Response fields | Field | Type | Description | | :- | :- | :- | | `success` | `Boolean!` | Whether the assignment was recorded. | | `objectOwnerId` | `UUID` | The ownership record ID. **Save this** — `transferObjectOwner` is keyed on it. | | `accountId` | `UUID` | The account the object and owner belong to. | | `objectType` | `String` | Echoes the object domain, `SPEND_ACCOUNTS`. | | `objectId` | `UUID` | The spend account ID that was assigned an owner. | | `userId` | `UUID` | The user now recorded as owner. | | `createdAt` | `DateTime` | When the ownership record was created. | | `error` | `ObjectOwnerError` | Error details when `success` is `false`. | *** ## Reassigning ownership Ownership moves with [`transferObjectOwner`](/api-reference/mutations/transfer-object-owner), which takes the `objectOwnerId` from the original assignment rather than the spend account ID. ```graphql theme={null} mutation TransferObjectOwner($objectOwnerId: UUID!, $userId: UUID!) { transferObjectOwner(objectOwnerId: $objectOwnerId, userId: $userId) { success objectOwnerId objectId userId updatedAt error { code message } } } ``` The new owner must be a user on the same account. This is the call to make when an authorized user leaves the team and their spend accounts need to land with someone else — removing the authorized user does not reassign the objects they owned. *** ## Full flow Add an authorized user, create a spend account for them, and record them as its owner. **Step 1 — Add the authorized user.** ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddAuthorizedUser($email: String, $roles: [UACRoleType!]!) { addAuthorizedUser(email: $email, roles: $roles) { success authUserId roles status error { code message } } }", "variables": { "email": "ada.lovelace@example.com", "roles": ["SPENDER"] } }' ``` Continue only once `status` is `ACTIVE`. **Step 2 — Create the spend account.** ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation CreateUserCashBalance($input: CreateUserCashBalanceInput!) { createUserCashBalance(input: $input) { userCashBalanceId nickname availableCashBalance status createdAt } }", "variables": { "input": { "nickname": "Ada — Field Ops" } } }' ``` **Step 3 — Assign the authorized user as owner.** ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AssignObjectOwner($objectType: ObjectOwnerObjectType!, $objectId: UUID!, $userId: UUID!) { assignObjectOwner(objectType: $objectType, objectId: $objectId, userId: $userId) { success objectOwnerId objectId userId createdAt error { code message } } }", "variables": { "objectType": "SPEND_ACCOUNTS", "objectId": "f8a3c9e1-7b2d-4f5e-9c8a-1d2e3f4a5b6c", "userId": "f1320ac4-52dc-4c67-9e80-24e506b18450" } }' ``` **Step 4 — Fund it.** The spend account starts at a zero balance. Deposit into it with [`depositCashBalance`](/features/deposit-from-external-accounts) or move funds across from another spend account with [`transferInternalBalance`](/features/transfer-between-spend-accounts), targeting the new `userCashBalanceId`. ### TypeScript ```typescript theme={null} const graphql = async (query: string, variables: Record) => { const response = await fetch( 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query, variables }), }, ); return response.json(); }; // 1. Create the spend account. const created = await graphql( `mutation CreateUserCashBalance($input: CreateUserCashBalanceInput!) { createUserCashBalance(input: $input) { userCashBalanceId nickname status } }`, { input: { nickname: 'Ada — Field Ops' } }, ); const { userCashBalanceId } = created.data.createUserCashBalance; // 2. Record the authorized user as its owner. const assigned = await graphql( `mutation AssignObjectOwner( $objectType: ObjectOwnerObjectType! $objectId: UUID! $userId: UUID! ) { assignObjectOwner( objectType: $objectType objectId: $objectId userId: $userId ) { success objectOwnerId error { code message } } }`, { objectType: 'SPEND_ACCOUNTS', objectId: userCashBalanceId, userId: authorizedUserUserId, }, ); // Persist objectOwnerId — transferObjectOwner is keyed on it, not on the // spend account ID. const { objectOwnerId } = assigned.data.assignObjectOwner; ``` *** ## Error codes | Code | Description | | :- | :- | | `ARG-0001` | Required input is missing or invalid — `objectId` is not a spend account on the caller's account, or `userId` is not a user on that account. | | `AUTH-0008` | The Bearer token could not be resolved to a caller. Verify the token is valid. | | `AUTH-0031` | The token is missing the `MANAGE_SUBUSERS` scope required to assign or transfer object ownership. | *** > **[Authorized User Overview](/features/authorized-user-overview)** > > Roles, statuses, and the full authorized user lifecycle. > **[Create Virtual Card for Authorized User](/features/create-virtual-card-for-authorized-user)** > > Issue a card on an authorized user's behalf with `authUserId`. > **[Spend Accounts Overview](/features/spend-accounts)** > > Creating, reading, editing, and closing spend accounts. > **[Remove Authorized User](/features/remove-authorized-user)** > > What happens to owned objects when access is revoked. # Developer dashboard Source: https://docs.fluz.app/developer-dashboard # Digital wallet push provisioning Source: https://docs.fluz.app/digital-wallet-push-provisioning Use the `getCardProvisioningUrl` query to mint a short-lived URL that — when opened on your end-user's phone — launches Fluz on the device and adds a specific virtual card to Apple Pay (iOS) or Google Pay (Android). You can deliver the URL however you like: a QR code, an SMS, an email, or an in-app button. Fluz handles the wallet-provisioning flow from there. ## When to use it Anywhere you would normally hand the user a virtual card and want them to be able to tap it in their phone's wallet without typing the card number. Typical flows: * After creating a card offer for the user, present a QR code on screen. * Text or email the URL to the cardholder. * Embed an "Add to Apple Pay / Google Pay" button in your own mobile app. > [!NOTE] > > **Prerequisites:** an OAuth access token for the end-user that includes the `CREATE_VIRTUALCARD` scope, and something to select the card with. Either the `virtualCardId` returned by `createVirtualCard`, or an `offerId` accessible to the account. If the offer hasn't been redeemed into a card yet, one is created as part of the provisioning flow. > [!WARNING] > > Basic-auth (`client_id` / `client_secret`) is **not** accepted on this query — it must be a user-context bearer token. ## The query By card (recommended): ```graphql theme={null} query ProvisionCard($virtualCardId: String!) { getCardProvisioningUrl(input: { virtualCardId: $virtualCardId, platform: IOS }) { url expiresAt } } ``` By offer, when the account holds at most one active card on it: ```graphql theme={null} query ProvisionCard($offerId: String!) { getCardProvisioningUrl(input: { offerId: $offerId, platform: IOS }) { url expiresAt } } ``` ### Input Supply **exactly one** of `virtualCardId` or `offerId`. | Field | Type | Required | Description | | - | - | - | - | | `virtualCardId` | `String` (UUIDv4) | one of | The card to provision, as returned by `createVirtualCard`. Must belong to the caller's account and be `ACTIVE`. Prefer this: it names the card unambiguously. | | `offerId` | `String` (UUIDv4) | one of | The virtual-card offer to provision. Must be active, tokenization-eligible, and accessible to the user's account. Only resolves while the account holds at most one active card on that offer. | | `platform` | `ProvisioningPlatform` | no | Hint about where the user will open the URL. Defaults to `IOS`. See below — this **does not** lock the URL to one platform. | > [!WARNING] > > An offer can back many cards on the same account. Once there is more than one active card on an offer, `offerId` no longer identifies a single card and the query fails with `VirtualCard.AMBIGUOUS_CARD_SELECTION` rather than picking one for you. If you issue more than one card per offer, use `virtualCardId`. #### `ProvisioningPlatform` The returned URL is platform-aware: iOS users automatically get the App Clip experience, Android users automatically get the Fluz app deep link. The `platform` value only affects what happens when the URL is opened somewhere **other than** an iOS or Android device (e.g. a desktop browser). | Value | Desktop / unknown-device fallback | | - | - | | `IOS` (default) | Apple App Clip launcher | | `ANDROID` | Fluz Android deep link | | `OTHER` | Fluz web app | Pick the value that best matches where you expect the URL to be opened. If in doubt, leave it as `IOS`. ### Output | Field | Type | Description | | - | - | - | | `url` | `String!` | The short URL to deliver to the user. Targets exactly one card. | | `expiresAt` | `String!` | ISO-8601 timestamp. The URL stops working at this time (≈ 5 minutes after creation). | ## Example ### Request ```bash theme={null} curl -X POST https://transactional-graph.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "query($id:String!){ getCardProvisioningUrl(input:{ virtualCardId:$id, platform:IOS }){ url expiresAt } }", "variables": { "id": "11111111-2222-4333-8444-555555555555" } }' ``` Or select by offer instead: ```bash theme={null} curl -X POST https://transactional-graph.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "query($id:String!){ getCardProvisioningUrl(input:{ offerId:$id, platform:IOS }){ url expiresAt } }", "variables": { "id": "66666666-7777-4888-9999-000000000000" } }' ``` ### Response ```json theme={null} { "data": { "getCardProvisioningUrl": { "url": "https://fluz.app.link/abc123XYZ", "expiresAt": "2026-05-26T17:42:11.000Z" } } } ``` ## How to deliver the URL The URL is opaque and carries no card number, but it is a bearer secret: whoever opens it can add that card to their wallet. Deliver it to the cardholder over a channel you trust. Common patterns: * **QR code on a screen** — generate a QR from `url` and show it; the user scans with their phone camera. * **SMS / email** — send the link directly to the user's phone or inbox. * **In-app deep link** — wire a button in your mobile app that opens `url`. Whatever the delivery channel, the user must open the URL on a mobile device — that's where the wallet provisioning happens. ## Lifetime & re-issuing * Each call returns a **fresh** URL targeting one card. * The URL is valid until `expiresAt` (≈ 5 minutes). * Opening the URL does not immediately invalidate it. It stays redeemable for a short grace window after first use, so treat it as a bearer secret: deliver it over a private channel, and don't log or cache it. * If a user doesn't act in time, simply call `getCardProvisioningUrl` again to mint a new one. There is no separate "refresh" endpoint. * To provision several cards, call the query once per card with each card's `virtualCardId`. ## Error handling All errors come back in the standard Fluz GraphQL error shape, with the error name in `extensions.code`. | Code | What it means | What to do | | - | - | - | | `Arguments.INVALID` | The id isn't a valid UUID, or you supplied both selectors or neither. | Send exactly one of `virtualCardId` or `offerId`. | | `VirtualCard.CARD_NOT_FOUND` | No such card on the caller's account. | Confirm the `virtualCardId` and that it belongs to this account. | | `VirtualCard.CARD_NOT_ACTIVE` | The card exists but isn't `ACTIVE`. | Don't retry; the card can't be added to a wallet in this state. | | `VirtualCard.AMBIGUOUS_CARD_SELECTION` | The account holds more than one active card on the `offerId` you sent. | Retry with `virtualCardId`. | | `VirtualCard.OFFER_NOT_FOUND` | The card's offer doesn't exist or is inactive. | Confirm the offer id; create a new offer if needed. | | `VirtualCard.OFFER_NOT_ACCESSIBLE` | The user's account isn't eligible for this offer (campaign access). | Surface a generic "not available" message. | | `VirtualCard.OFFER_NOT_TOKENIZATION_ELIGIBLE` | The offer's card program doesn't support wallet provisioning. | Don't offer "Add to wallet" for this card program. | | `Auth.USER_REVOKED_DEVELOPER_ACCESS` | The user has revoked OAuth access to your app. | Send the user back through OAuth consent. | | `Auth.INVALID_SCOPE` | The token doesn't have `CREATE_VIRTUALCARD`. | Request the scope during OAuth. | | `Generic.EXTERNAL_SERVICE_ERROR` | Transient downstream issue. | Retry — the call is safe to repeat. | ## FAQ > **Does the URL contain the card number?** > > No. The URL carries a short-lived opaque lookup id only. Credentials are exchanged securely on the device once the user opens the link. > **Can the same URL be reused?** > > Treat it as single-use: generate a new one each time you need to surface the flow. Opening the link doesn't invalidate it instantly, so it stays redeemable for a short grace window. Deliver it privately and don't log or cache it. > **Can one URL add several cards?** > > No. Each URL targets exactly one card. Call the query once per card, passing that card's `virtualCardId`. > **What if the user is on desktop?** > > They'll land on the fallback you selected with `platform` (App Clip launcher, Android deep link, or Fluz web app). For the wallet provisioning itself to work, they need to end up opening the URL on a mobile device. > **Do I need to do anything different for iOS vs. Android?** > > No. The same URL works for both — Fluz routes per-device automatically. ## Next steps > **[Send cards](/features/send-cards)** > > Distribute virtual cards to recipients by email, SMS, or share link. > **[Set a virtual card PIN](/set-virtual-card-pin)** > > Set a PIN on eligible cards before in-person, PIN-prompted transactions. # Go to Fluz app Source: https://docs.fluz.app/explore-fluz-app # API features Source: https://docs.fluz.app/features Everything the Fluz API can do, on your own account or on your customers'. Every capability below works the same way on any account a token belongs to: personal or business, yours or a connected customer's. A handful of onboarding operations require a specific account type. They're listed in [How accounts work](/get-started/accounts-and-applications#every-token-is-bound-to-one-account). > **[Funding sources](/features/funding-sources)** > > Link bank cards and bank accounts via Plaid, then pull funds on demand. > **[Wallets & transfers](/features/move-funds-with-external-accounts)** > > Spend accounts, deposits, withdrawals, and internal or cross-account transfers. > **[Virtual cards](/features/virtual-cards)** > > Spend controls, lock/unlock, PINs, wallet provisioning, and bulk issuance up to 10,000. > **[Card status, locks & limits](/features/virtual-card-status)** > > What each card status means and how to read a card's current limit. > **[Gift cards](/merchant-catalog)** > > Thousands of brands, offer comparison, stock checks, and discount pricing. > **[Open loop cards](/features/open-loop-cards/send-open-loop-cards)** > > Generate hosted card links that recipients claim, with full link lifecycle control. > **[Send money](/features/account-to-account-transfers)** > > Look up recipients by phone or email and transfer funds, including rewards, to other Fluz wallets. > **[Transactions](/features/get-all-transactions)** > > Query all activity and annotate with memos, categories, and attachments. > **[Approvals & requests](/features/approvals-and-requests)** > > Request manager approval for cards, transfers, purchases, and limit changes. > **[Authorized users](/features/create-authorized-users)** > > Add team members, issue them cards, and route approvals. ## Go deeper > **[API reference](/api-reference/overview)** > > Every query, mutation, and type in the GraphQL schema. > **[Code examples](/recipes/generate-user-access-token)** > > Copy-paste scripts for single operations: tokens, cards, money movement. > **[Changelog](/changelog)** > > Recent updates, additions, and breaking changes. > [!NOTE] > > **Building for customers instead?** Every capability on this page works identically on connected accounts. Send a customer-scoped OAuth token instead of your own. You'll need an **OAuth Integration** application that Fluz has activated. See [Build a platform](/build-a-platform). # Overview Source: https://docs.fluz.app/features/account-to-account-transfers **Account to account transfers** move funds between two Fluz accounts using the `createTransfer` mutation. Unlike an internal transfer — which moves money between two of *your own* spend accounts — an account to account transfer sends money to a **different Fluz account**: one of your users, your application, or between two of your users. Every transfer is a two-step flow: first identify the recipient, then create the transfer. | Step | Page | Purpose | | - | - | - | | 1. Find the recipient | [Wallet Transfer Recipient Lookup](/features/recipient-lookup) | Resolve a phone, email, or company name into the recipient's account ID. | | 2. Send the funds | [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet) | Move the funds to the recipient with `createTransfer`. | | Optional. Fund with rewards | [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards) | Debit rewards, or rewards plus one spend account. | > [!NOTE] > > **Not the same as an internal transfer.** > > To move money between your own spend accounts, use [Transfer Funds Internally](/features/transfer-between-spend-accounts) instead. Account to account transfers are for moving funds to a **separate** Fluz account. *** ## The Three Transfer Directions The same `createTransfer` mutation supports three directions. Which one you're performing is determined by your authentication method and whether you provide a `destination`. | Direction | Auth method | Sender | Typical use | | - | - | - | - | | **User → Application** | Bearer token | The user | Collecting a payment from a user (no `destination` needed — defaults to your app). | | **Application → User** | Basic Auth | Your application | Disbursing funds to a user (`destination` required). | | **User → User** | Bearer token | The user | Moving funds between two of your users (`destination` set to the recipient). | > [!WARNING] > > **Account to account transfers require a public application.** > > Basic Auth (application-as-sender) transfers require your application to be in `ACTIVE` status. Personal applications cannot send via Basic Auth. The recipient must also be a registered user of your application. *** ## Step 1 — Look Up the Recipient Before sending, resolve the recipient into an `accountId` using the lookup queries (both require the `QUERY_RECIPIENT` scope): * `lookupUser` — find an individual by **phone number** (E.164 format) or **email**. * `lookupBusiness` — find a business by **company name** (case-insensitive exact match; searches both the registered name and DBA). If you already store the recipient's Fluz `accountId` — or assigned them an `externalReferenceId` when you created them — you can skip the lookup and pass that identifier directly in Step 2. See [Wallet Transfer Recipient Lookup](/features/recipient-lookup) for inputs, responses, and error codes. *** ## Step 2 — Create the Transfer Send the funds with the `createTransfer` mutation. At a minimum you provide an `idempotencyKey` and an `amount`; the `destination` and funding source depend on the direction. * **Identify the recipient** by `accountId` or by the `externalReferenceId` you assigned — provide one, not both. Optionally target a specific `userCashBalanceId` on the recipient's account. * **Fund the transfer** from the sender's spend account by default. Pass `useRewardsBalance: true` on `CreateTransferInput` to include the sender's rewards balance (cash first, rewards for the remainder). Bearer token only, you may instead deposit from a linked `bankCardId`, `bankAccountId`, or `paypalVaultId` first. At most one of those external sources per request. * **Annotate** the transfer with an optional `memo`, `transactionCategory`, or `attachmentId`. Bearer-token (user-sent) transfers require the `MAKE_PAYOUT_TRANSFER_SEND` scope. See [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet) for the full input reference, examples for each direction, and error handling. See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards) for `useRewardsBalance` on `createTransfer` and `requestAccountTransfer`. *** ## Idempotency Every transfer must include a unique, client-generated `idempotencyKey`. If the same key is submitted more than once, the API returns the result of the original request instead of creating a duplicate transfer. Keys are valid for 10 minutes. *** ## Requirements at a Glance | Operation | Auth | Scope | | - | - | - | | Look up a recipient | Bearer token | `QUERY_RECIPIENT` | | Create a transfer (user-sent) | Bearer token | `MAKE_PAYOUT_TRANSFER_SEND` | | Create a transfer (application-sent) | Basic Auth (Api Key) | Public / `ACTIVE` application | *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Add bank card Source: https://docs.fluz.app/features/add-bank-card # Background A funding source, otherwise known as payment method, is used on Fluz to purchase gift cards, fund virtual card transactions, deposit funds to your Fluz balance, and more. The first thing you'll want to do is make sure you have at least one funding source to complete transactions on Fluz. A user account can have the following types: * **Bank Card** - A debit, credit or prepaid card. Please note, credit cards can carry higher fees and lower cashback rates than debit cards. * **Bank Account** - Bank accounts have no fees. However, they can only be added in Fluz app or web portal. * **PayPal Account** - Digital wallets like PayPal often carry the highest fees and need to be added from the Fluz app or web portal. You must add a bank card as a backup payment method to be able to make a purchase. > [!NOTE] > > **Currently, users can only add bank cards as a funding source via the API.** # Add Bank Card ## Sample request You can add a new bank card with the `addBankCard` [mutation](/api-reference/overview). This mutation allows you to provide the necessary details for adding a bank card to a user's account. The query takes an `AddBankCardInput` input object. ```json theme={null} { "query": "mutation addBankCard($input: AddBankCardInput!) { addBankCard(input: $input) { bankCardId ownerAccountId addedUserId cardType cardProcessor cardholderName lastFourDigits expirationMonth expirationYear cardStatus billingAddressId nickname }}", "variables": { "input": { "cardNumber": "4111111111111111", "expirationMonth": "01", "expirationYear": "2029", "cvv": "111", "cardholderName": "John Doe", "nickname": "My travel card", "preferredMerchantCategoryCode": "5411", "billingAddress": { "streetAddressLine1": "123 Example St", "streetAddressLine2": "Unit 1", "country": "United States", "city": "New York City", "state": "New York", "postalCode": "12345" } } } } ``` This mutation requires the `AddBankCardInput` input type. Any field marked with an exclamation mark (`!`) in the schema is mandatory and must be included in the request. To associate an address with your bank card, you must use either the `billingAddress` or `userAddressId` input field. Ensure that only one of these fields is provided to meet validation requirements. * **New Address**: To add a new address, use the `billingAddress` input field to define the address details associated with this card. * **Existing Address**: To link an existing address, use the `userAddressId` input field, specifying the identifier for the address already stored in the system. You can use the `getUserAddresses` query to look up an existing `userAddressId`. > [!NOTE] > > **Billing address formatting** > > Provide the billing address exactly as it appears on the card issuer's records, using the structured fields (`streetAddressLine1`, `streetAddressLine2`, `city`, `state`, `postalCode`, `country`). A mismatched or malformed billing address can cause the card to fail the address verification (AVS) check. International billing addresses are accepted for bank cards. See [Address Formatting Requirements](/concepts/address-formatting-requirements) for formatting rules and examples. | Field name | Type | Description | | - | - | - | | cardNumber | String! | The card number. This field requires the full card number, typically 16 digits. | | expirationMonth | String! | The expiration month of the card, formatted as MM (e.g., "07" for July). | | expirationYear | String! | The three- or four-digit security code, found on the back or front of the card. | | cvv | String! | The three- or four-digit security code, found on the back or front of the card. | | cardholderName | String! | The name of the cardholder as it appears on the card. | | billingAddress | UserAddressInput | Optional. Use this field to create the card with a new billing address. | | userAddressId | UUID | Optional. Use this field to associate the card with an existing user address. | | nickname | String | Optional. Use this field to create a card with a nickname. | | preferredMerchantCategoryCode | String | Optional. Use this field to associate the card with a merchant category code. | ## AddBankCardInput ```json theme={null} { "cardNumber": "xyz789", "expirationMonth": "xyz789", "expirationYear": "abc123", "cvv": "abc123", "cardholderName": "abc123", "billingAddress": UserAddressInput, "userAddressId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "nickname": "My travel card", "preferredMerchantCategoryCode": "5411" } ``` ## Sample response The response from the `addBankCard` mutation will include the fields that you specified in the mutation request, along with the relevant ID values and card processor. ```json theme={null} { "data": { "addBankCard": { "bankCardId": "4111111111111111", "ownerAccountId": "a578fe07-7165-47b8-b147-2251c99b7fc1", "addedUserId": "8d197a56-53df-439b-85cd-bb88dfca9a5f", "cardType": "Visa", "cardProcessor": "Stripe", "cardholderName": "John Doe", "lastFourDigits": "1234", "expirationMonth": "12", "expirationYear": "2025", "cardStatus": "active", "billingAddressId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "nickname": "My travel card" } } } ``` > [!NOTE] > > **Only available to personal applications** > > Currently, only personal applications will be granted the scope to add bank cards to an account. Publicly available applications looking to add bank cards to an account will need to complete PCI certification and an attestation of PCI compliance. If you would like to set a given funding source as the primary, preferred or backup on your account, this will need to be managed in the Fluz dashboard. **Want to learn more?** Contact us at your support email. Speak with our experts for more info or to request a demo. # Add virtual card address Source: https://docs.fluz.app/features/add-billing-address Save a billing address for virtual card issuance on the caller's account. The address is saved as a `BILLING` user address and can later be passed to `createVirtualCard` as `userAddressId`. When `authUserId` is provided, the address is saved for that authorized user's underlying user while remaining attached to the caller's account. The `authUserId` must be an ACTIVE non-owner authorized user assignment on the caller's account. If an identical billing address already exists for the target cardholder on the caller's account, the existing address is returned instead of creating a duplicate. Matching is done on address fields, account, target user, and `BILLING` type. > [!NOTE] > > **Prerequisites:** a Bearer token with the `CREATE_VIRTUALCARD` scope. > [!NOTE] > > **The billing address must be verifiable** > > This mutation stores the billing address. It must be a real, deliverable **US** address (`US`, `USA`, or `United States`) with a correct city, state, and ZIP, and **PO boxes are not accepted**. The USPS/Smarty check runs when a virtual card is created — an address that can't be verified there causes card creation to fail with `VC-0025`. See [Address Formatting Requirements](/concepts/address-formatting-requirements) for the full field rules and examples. ```graphql theme={null} mutation AddVirtualCardAddress($input: AddVirtualCardAddressInput!) { addVirtualCardAddress(input: $input) { userAddressId streetAddressLine1 streetAddressLine2 country city state postalCode } } ``` ## Parameters | Parameter | Type | Required | Description | | - | - | - | - | | input | AddVirtualCardAddressInput! | Yes | Wrapper object for the billing address request. | | input.billingAddress | VirtualCardBillingAddressInput! | Yes | Billing address to save for virtual card issuance. | | input.billingAddress.streetAddressLine1 | String! | Yes | Street address. PO boxes are not accepted by the issuer. | | input.billingAddress.streetAddressLine2 | String | No | Optional apartment, suite, or secondary address line. Empty values are saved as `null`; non-empty values are trimmed. | | input.billingAddress.country | String! | Yes | Country name. Currently only United States addresses are supported by the card issuer. | | input.billingAddress.city | String! | Yes | City. Saved in uppercase. | | input.billingAddress.state | String! | Yes | State name or two-letter US state code. Saved in uppercase. | | input.billingAddress.postalCode | String! | Yes | 5-digit US ZIP code. Saved in uppercase. | | input.authUserId | UUID | No | Authorized user ID (UAC role assignment ID). When provided, the address is saved for that authorized cardholder. | ## Response ```json theme={null} { "data": { "addVirtualCardAddress": { "userAddressId": "1f6b2c3d-4e5f-4a67-9a10-2b3c4d5e6f70", "streetAddressLine1": "123 Main St", "streetAddressLine2": "Apt 5", "country": "United States", "city": "NEW YORK", "state": "NY", "postalCode": "10001" } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `userAddressId` | UUID | Saved billing address ID. Pass this value as `userAddressId` to `createVirtualCard`. | | `streetAddressLine1` | String | Street address line 1. | | `streetAddressLine2` | String | Street address line 2, or `null` when not provided. | | `country` | String | Country name. | | `city` | String | City saved on the address. | | `state` | String | State saved on the address. | | `postalCode` | String | Postal code saved on the address. | ## Example requests Save a billing address for the caller: ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddVirtualCardAddress($input: AddVirtualCardAddressInput!) { addVirtualCardAddress(input: $input) { userAddressId streetAddressLine1 streetAddressLine2 country city state postalCode } }", "variables": { "input": { "billingAddress": { "streetAddressLine1": "123 Main St", "streetAddressLine2": "Apt 5", "country": "United States", "city": "New York", "state": "NY", "postalCode": "10001" } } } }' ``` Save a billing address for an authorized user: ```bash cURL theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddVirtualCardAddress($input: AddVirtualCardAddressInput!) { addVirtualCardAddress(input: $input) { userAddressId streetAddressLine1 streetAddressLine2 country city state postalCode } }", "variables": { "input": { "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "billingAddress": { "streetAddressLine1": "456 Market St", "streetAddressLine2": "Suite 200", "country": "United States", "city": "San Francisco", "state": "CA", "postalCode": "94105" } } } }' ``` ```typescript TypeScript theme={null} const response = await fetch('https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query: ` mutation AddVirtualCardAddress( $input: AddVirtualCardAddressInput! ) { addVirtualCardAddress(input: $input) { userAddressId streetAddressLine1 streetAddressLine2 country city state postalCode } } `, variables: { input: { authUserId: '8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d', billingAddress: { streetAddressLine1: '456 Market St', streetAddressLine2: 'Suite 200', country: 'United States', city: 'San Francisco', state: 'CA', postalCode: '94105', }, }, }, }), }); const data = await response.json(); console.log('Virtual card address saved:', data.data.addVirtualCardAddress); ``` ## Error codes | Code | Message | Description | | - | - | - | | `ARG-0001` | Invalid arguments received | A required address field is missing, a value is empty, or `authUserId` is not a valid UUID. | | `AUTH-0008` | Invalid user access | The Bearer token could not be resolved to a caller. Verify the token is valid. | | `AUTH-0031` | The requested scopes must be granted by the user first. | The token is missing the `CREATE_VIRTUALCARD` scope required to save virtual card addresses. | | `AUTH-0034` | No authorized user found with this id on the caller's account. | The `authUserId` does not exist on the caller's account, is not ACTIVE, or refers to an OWNER assignment. | | `VC-0001` | Please try another payment method. If you continue experiencing issues, please contact our support team. | A general failure occurred while saving the billing address. Please retry or contact support. | ## Next steps > **[Create a virtual card](/features/create-card)** > > Pass the returned `userAddressId` into `createVirtualCard`. > **[Set a virtual card PIN](/set-virtual-card-pin)** > > Required before the card can be used for in-person or PIN-prompted transactions. # Authorized user overview Source: https://docs.fluz.app/features/authorized-user-overview Give other Fluz users scoped access to your account, control what they can do with roles, and issue virtual cards on their behalf. An **authorized user** is an existing Fluz user who has been granted one or more roles on your account. Authorized users let a business put a team on one account — a bookkeeper who can only view activity, a manager who can approve spend, a contractor who can spend from a single card — without sharing a login or handing over the full account balance. Authorized user management is a small, self-contained part of the API: one mutation to add, one query to list, one mutation to remove, and an `authUserId` you can pass into virtual card operations to issue cards on someone else's behalf. > [!NOTE] > > **Adding an authorized user does not create a Fluz user.** > > `addAuthorizedUser` looks up an **existing** Fluz user by email or phone and creates a role assignment for them on your account. If the person does not have a Fluz account yet, register them first with [User Registration](/user-registration), then add them. *** ## How Access Is Scoped The target account is always resolved from the caller's credentials — there is no account ID parameter, and no way to manage authorized users on an account you do not control: | Authentication | Account acted on | | - | - | | Bearer (user access token) | The account the token was issued for. | | Basic (``) | The application's configured operator account. | Every authorized user is identified by an **`authUserId`** — the ID of the role assignment, not the user. You get it back from `addAuthorizedUser` and from `authorizedUsers`, and you pass it to `removeAuthorizedUser`, `addVirtualCardAddress`, and `createVirtualCard`. *** ## Roles Roles are assigned with the `UACRoleType` enum. An authorized user can hold more than one role, and the effective access is the highest of them. | Role | Assignable | Typical use | | - | - | - | | `OWNER` | No | The account holder. Cannot be assigned or removed through the API. | | `ADMIN` | Yes | Full management of the account, its funds, cards, and other authorized users. | | `MANAGER` | Yes | Oversight of spend — reviewing and approving requests from other users. | | `SPENDER` | Yes | Day-to-day spending on the cards and spend accounts they have access to. | | `VIEWER` | Yes | Read-only visibility into account activity. | Roles are set at the time you add the user. To change someone's roles, remove the assignment and add it again with the new role set. *** ## Status Lifecycle Every role assignment carries a `UACRoleStatusType` status: | Status | Meaning | | - | - | | `PENDING` | The invite has been created and is waiting for the user to accept. | | `ACTIVE` | The user has access to the account with the assigned roles. | | `DECLINED` | The user rejected the invite. | | `INACTIVE` | Access has been revoked, either by removal or by the assignment being deactivated. | ```mermaid theme={null} stateDiagram-v2 [*] --> PENDING PENDING --> ACTIVE PENDING --> DECLINED ACTIVE --> INACTIVE INACTIVE --> ACTIVE DECLINED --> ACTIVE ``` By default, `addAuthorizedUser` creates the assignment as `PENDING` and sends an invite. If your product already handles its own consent flow, you can set `status: ACTIVE` and `sendInvite: false` to provision the user immediately with no invite and no acceptance step. Re-adding a user whose assignment is `INACTIVE` or `DECLINED` reactivates that assignment with the new roles rather than creating a duplicate. > [!WARNING] > > **`PENDING` assignments cannot be used yet.** An `authUserId` must be `ACTIVE` before you can save a billing address or create a virtual card for that user. If `addAuthorizedUser` returns `PENDING`, wait for acceptance before calling `addVirtualCardAddress` or `createVirtualCard`. *** ## What You Can Do | Action | Query / Mutation | Scope | Description | | - | - | - | - | | [Create an authorized user](/features/create-authorized-users) | `addAuthorizedUser` | `MANAGE_SUBUSERS` | Grant an existing Fluz user one or more roles on your account. | | [Query authorized users](/features/query-authorized-user) | `authorizedUsers` | `VIEW_SUBUSERS` | List everyone with a role assignment on your account, optionally filtered. | | [Remove an authorized user](/features/remove-authorized-user) | `removeAuthorizedUser` | `MANAGE_SUBUSERS` | Revoke access by deactivating the role assignment. | | [Create a virtual card for an authorized user](/features/create-virtual-card-for-authorized-user) | `createVirtualCard` | `CREATE_VIRTUALCARD` | Issue a card in the authorized user's name that stays on your account. | `addAuthorizedUser`, `authorizedUsers`, and `removeAuthorizedUser` accept both Bearer and Basic authentication. `createVirtualCard` requires a Bearer token. *** ## Issuing Cards for Authorized Users A virtual card created with `input.authUserId` is issued against the authorized user's cardholder record — the card carries their name and returns their `userId` — while the card itself, its funding, and its transactions remain on your account. This is how you put a card in an employee's or contractor's hands without opening a separate account for them. The full sequence is: **1. Register the user** Skip this if they already have a Fluz account. Otherwise call `registerUser`. **2. Add them as an authorized user** Call `addAuthorizedUser` and hold on to the returned `authUserId`. Continue only once the status is `ACTIVE`. **3. Save a billing address** Call `addVirtualCardAddress` with the `authUserId` to store a billing address for that cardholder. **4. Create the card** Call `createVirtualCard` with `input.authUserId` and the returned `userAddressId`. See [Create Virtual Card for Authorized User](/features/create-virtual-card-for-authorized-user) for the complete request and response reference. *** ## Error Handling The authorized user operations return errors **in the response data**, not as GraphQL errors. Always check `success` and read the `error` object when it is `false`: ```json theme={null} { "data": { "addAuthorizedUser": { "success": false, "authUserId": null, "error": { "code": "AUTH-0034", "message": "No Fluz user found with the provided email or phone number." } } } } ``` The codes you are most likely to hit: | Code | When it happens | | - | - | | `AUTH-0031` | The token is missing `MANAGE_SUBUSERS` or `VIEW_SUBUSERS`. | | `AUTH-0034` | No Fluz user matches the email or phone, or the `authUserId` is not on your account. | | `AUTH-0035` | The user already has an active role assignment on your account. | | `AUTH-0036` | You tried to remove the account owner. | | `AUTH-0037` | A general failure occurred managing the role assignment. Retry or contact support. | `createVirtualCard` behaves differently — it raises standard GraphQL errors, including `VC-0020` when a billing address is still pending issuer approval. See [Virtual Card Error Codes](/features/virtual-card-error-codes). *** ## Requirements Listing authorized users requires the `VIEW_SUBUSERS` scope. Adding and removing them requires `MANAGE_SUBUSERS`. Issuing a card on their behalf requires `CREATE_VIRTUALCARD`. Confirm your user access token carries the right scopes before calling these operations — see [Application Scopes](/fluz-dashboard/application-scopes). *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Billing address overview Source: https://docs.fluz.app/features/billing-address Save a billing address for virtual card issuance on the caller's account. The address is saved as a `BILLING` user address and can later be passed to `createVirtualCard` as `userAddressId`. When `authUserId` is provided, the address is saved for that authorized user's underlying user while remaining attached to the caller's account. The `authUserId` must be an ACTIVE non-owner authorized user assignment on the caller's account. If an identical billing address already exists for the target cardholder on the caller's account, the existing address is returned instead of creating a duplicate. Matching is done on address fields, account, target user, and `BILLING` type. > [!NOTE] > > **Prerequisites:** a Bearer token with the `CREATE_VIRTUALCARD` scope. > [!NOTE] > > **The billing address must be verifiable** > > This mutation stores the billing address. It must be a real, deliverable **US** address (`US`, `USA`, or `United States`) with a correct city, state, and ZIP, and **PO boxes are not accepted**. The USPS/Smarty check runs when a virtual card is created — an address that can't be verified there causes card creation to fail with `VC-0025`. See [Address Formatting Requirements](/concepts/address-formatting-requirements) for the full field rules and examples. ## How it fits together 1. Save an address with `addVirtualCardAddress` — you get back a `userAddressId`. 2. Pass that `userAddressId` to [`createVirtualCard`](/features/create-card). It takes precedence over any inline `billingAddress`. 3. Reuse the same `userAddressId` across as many cards as you like — the address is stored once on the account. You can also skip pre-saving entirely and pass a `billingAddress` object inline on `createVirtualCard`; a matching saved address will be reused, or a new one created. ## Next steps > **[Add virtual card address](/features/add-billing-address)** > > The full mutation contract — parameters, responses, examples, and error codes. > **[Create a virtual card](/features/create-card)** > > Put the saved `userAddressId` to work issuing a card. # Bring your own integration for Plaid (BYOI Plaid) Source: https://docs.fluz.app/features/byoi-plaid Use your own Plaid client ID and secret for the bank links that your users make through your application. Bring Your Own Integration for Plaid (BYOI Plaid) lets your application use its own Plaid account for bank linking. When BYOI Plaid is on, Fluz uses your Plaid client ID and secret for the bank links that your users make through your application. > [!NOTE] > > BYOI Plaid is a setting for the full application. It applies to all users of your application. Your users do not need a new permission, and you do not change your bank linking code. ## Terms on this page | Term | Meaning | | - | - | | Developer | The Fluz user who owns the application in the developer dashboard. | | Plaid integration | Your Plaid client ID and secret, registered with Fluz for one application in one environment. | | Bank link | One Plaid connection to one bank login (a Plaid Item). Fluz identifies each bank link with its `platformItemId`. | | Fluz Plaid | The Plaid credentials of Fluz. Fluz uses them when BYOI Plaid is off. | ## How BYOI Plaid works 1. You add the `BYOI_PLAID` permission to your application. 2. The developer registers your Plaid client ID and secret with `registerPlaidIntegration`. 3. Fluz sends a test request to Plaid to validate the credentials. Then Fluz stores them in an encrypted vault. 4. BYOI Plaid starts immediately for all users of your application. 5. Each new bank link that a user makes through your application uses your Plaid credentials. You continue to use the same operations: `createPlaidLinkToken`, `completePlaidLink`, and the other operations in [Linking Bank Account via Plaid](/features/link-via-plaid). ### Which credentials each bank link uses Each bank link always uses the Plaid credentials that created it. | Bank link | Credentials that Fluz uses | Available to your application | | - | - | - | | New bank link made through your application while BYOI Plaid is on | Your Plaid credentials | Yes | | Bank link made with Fluz Plaid, for example before you turned on BYOI Plaid | Fluz Plaid | Yes | | Bank link made with the Plaid integration of a different application | The credentials of that application | No | | Bank link made with an earlier Plaid integration of your application that used a different client ID | The earlier credentials | No. See [Change your Plaid client ID](#change-your-plaid-client-id). | Fluz can also use the bank links that your credentials created. For example, Fluz checks the balance of these bank accounts. For these requests, Fluz uses your Plaid credentials. ## Before you start | Requirement | Details | | - | - | | Fluz application | An enabled application. Only the developer of the application can register, read, or disable its Plaid integration. | | Plaid account | API access in the Plaid environment that matches the Fluz environment. See the table below. | | Plaid products | Auth, Transactions, Identity, Signal, and Balance must be available for your client ID. Fluz requests Auth, Transactions, Identity, and Signal when it creates a Link token. Fluz uses Balance for real-time balance checks. | | Country | United States. Fluz creates Link tokens for US banks only. | | User permissions | Your users need `MANAGE_PAYMENT`, the same as for all bank linking. Your users do not need `BYOI_PLAID`. | ### Environments Each Fluz environment uses one Plaid environment. Register your credentials in each Fluz environment separately. A registration in one environment does not apply to the other environment. | Fluz environment | GraphQL URL | Plaid environment | Secret to register | | - | - | - | - | | Staging | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | Sandbox | Your Plaid Sandbox secret | | Live | `https://transactional-graph.fluzapp.com/api/v1/graphql` | Production | Your Plaid Production secret | ## Step 1: Add the BYOI\_PLAID permission to your application 1. In the developer dashboard, open **Your apps**. 2. Select your application. 3. Open the permissions tab that matches the Fluz account of the developer: * For a consumer account, open the **Permissions** tab. * For a business account, open the **Business permissions** tab. 4. Select **Link bank accounts through your own Plaid integration**. This is the `BYOI_PLAID` permission in the **Update Account Info** group. 5. Save the application. ## Step 2: Get an access token for the developer The Plaid integration operations need a user access token with these properties: * The token belongs to the developer of the application. * The token includes the `BYOI_PLAID` scope. * The token is a Bearer token. Basic auth is not accepted. To get this token: 1. Send the developer through the OAuth grant flow. See [Client-Facing OAuth Grant Flow](/client-facing-o-auth-grant-flow). 2. Log in as the developer and approve the requested permissions. The consent screen includes `BYOI_PLAID` because you added it in Step 1. 3. Exchange the authorization code for tokens. See [Exchange an OAuth Code](/exchange-an-o-auth-authorization-code). 4. Make sure that the `scope` list in the token response includes `BYOI_PLAID`. If the developer authorized your application before you added `BYOI_PLAID`, the old grant does not include it. Do the OAuth grant flow again. Send the token in each request: ```http theme={null} Authorization: Bearer ``` ## Step 3: Register your Plaid credentials ```graphql theme={null} mutation RegisterPlaidIntegration($input: RegisterPlaidIntegrationInput!) { registerPlaidIntegration(input: $input) { change plaidIntegration { plaidIntegrationId name environment status validatedAt } } } ``` ```json theme={null} { "input": { "clientId": "", "secret": "", "name": "Acme Plaid" } } ``` ```json theme={null} { "data": { "registerPlaidIntegration": { "change": "CREATED", "plaidIntegration": { "plaidIntegrationId": "5f0c3c1e-6a4e-4a7b-9f53-2f1b8d0f4a11", "name": "Acme Plaid", "environment": "PRODUCTION", "status": "ACTIVE", "validatedAt": "2026-09-23T19:45:00.000Z" } } } } ``` ### Input fields | Field | Required | Description | | - | - | - | | `clientId` | Yes | Your Plaid client ID. 1 to 255 characters. | | `secret` | Yes | Your Plaid secret for the matching Plaid environment. 1 to 255 characters. | | `name` | No | A label for the integration. The default is the name of your application. The name must be unique in your Fluz account for this environment, also among disabled integrations. Fluz uses the name only when it creates a new integration. | ### Response fields | Field | Description | | - | - | | `change` | What the request changed. See the next table. | | `plaidIntegration.plaidIntegrationId` | The Fluz ID of the Plaid integration. | | `plaidIntegration.name` | The label of the integration. | | `plaidIntegration.environment` | `SANDBOX` or `PRODUCTION`. | | `plaidIntegration.status` | `ACTIVE` or `DISABLED`. | | `plaidIntegration.validatedAt` | The time when Plaid accepted the current secret. | The API never returns your client ID or your secret. ### Values of `change` | Value | Meaning | | - | - | | `CREATED` | Fluz created a new Plaid integration. BYOI Plaid is now on. | | `UNCHANGED` | The same client ID and secret are already active. Fluz changed nothing. You can safely send the same request again. | | `UPDATED` | The client ID is already active and the secret is new. Fluz validated the new secret and saved it. | | `REACTIVATED` | A disabled integration with this client ID existed. Fluz turned it on again. If the secret was different, Fluz validated and saved the new secret. | ### Registration rules * Each application can have one active Plaid integration in each environment. * Fluz validates the credentials with Plaid before it saves them. If Plaid rejects them, Fluz saves nothing. * Registration turns on BYOI Plaid for the application. The change starts with the next request of each user. ## Read your Plaid integration ```graphql theme={null} query GetPlaidIntegration { getPlaidIntegration { plaidIntegrationId name environment status validatedAt } } ``` The query returns the active integration. If no integration is active, it returns the most recently changed integration. If you never registered an integration in this environment, it returns `null`. ## Replace your Plaid secret Use this procedure when you rotate your Plaid secret. Your existing bank links continue to work. 1. In the Plaid Dashboard, create a new secret for the same client ID. 2. Call `registerPlaidIntegration` with the same `clientId` and the new `secret`. 3. Make sure that `change` is `UPDATED`. 4. In the Plaid Dashboard, delete the old secret. ## Change your Plaid client ID Fluz does not replace an active client ID with a different client ID. If you try, Fluz returns `PLAID_INTEGRATION_CLIENT_ID_MISMATCH`. 1. Call `disablePlaidIntegration`. 2. Call `registerPlaidIntegration` with the new `clientId` and `secret`. 3. Make sure that `change` is `CREATED`. > [!WARNING] > > Bank links made with the old client ID stop working when you change the client ID. Your users must link these banks again. The old bank links work again only if you register the old client ID again. ## Disable BYOI Plaid ```graphql theme={null} mutation DisablePlaidIntegration { disablePlaidIntegration { change plaidIntegration { plaidIntegrationId status } } } ``` If an integration was active, `change` is `DISABLED`. If no integration was active, `change` is `UNCHANGED`. After you disable BYOI Plaid: * New bank links use Fluz Plaid. * Bank links made with your credentials stop working. Fluz cannot get new balances or transactions for them, and the users cannot relink them. * Fluz does not process Plaid webhooks for bank links made with your credentials. To turn BYOI Plaid on again, register the same client ID. The `change` value is `REACTIVATED`, and the old bank links work again. > [!WARNING] > > Disable BYOI Plaid before you remove the `BYOI_PLAID` permission from your application. Removing the permission does not turn off BYOI Plaid. It only prevents new tokens from including `BYOI_PLAID`. Without that scope, you cannot call `disablePlaidIntegration`. ## Link a bank account When BYOI Plaid is on, the linking flow does not change. Follow [Linking Bank Account via Plaid](/features/link-via-plaid): 1. Call `createPlaidLinkToken` with an empty `input`. 2. Open Plaid Link with the returned `linkToken`. 3. Send the `public_token` from Plaid Link `onSuccess` to `completePlaidLink`. 4. Store the returned `platformItemId`. Plaid Link shows **Fluz** as the application name. Fluz requests 730 days of transaction history. > [!WARNING] > > Do not send `deviceOs` when BYOI Plaid is on. With `deviceOs`, Fluz sends Plaid the OAuth redirect settings of the Fluz mobile app. Plaid does not accept these settings for your client ID, so the Link token request fails. Use Plaid Link for web without `deviceOs`. `completePlaidLink` returns only the bank accounts that your application can use. See the table in [Which credentials each bank link uses](#which-credentials-each-bank-link-uses). ## Relink a disconnected bank link A bank link can disconnect, for example when a user changes the bank password. Applications with a Plaid integration use two operations to repair a bank link: * `createPlaidRelinkToken` creates a Plaid update-mode Link token with the credentials that created the bank link. * `completePlaidRelink` confirms the repair with Plaid and updates the bank data. It does not need a `publicToken`. These operations work for bank links made with your credentials and for bank links made with Fluz Plaid. They keep the same Plaid access token and the same `platformItemId`. 1. Create the relink token. ```graphql theme={null} mutation CreatePlaidRelinkToken($input: CreatePlaidRelinkTokenInput!) { createPlaidRelinkToken(input: $input) { linkToken expiration requestId mode } } ``` ```json theme={null} { "input": { "platformItemId": "" } } ``` 2. Open Plaid Link with the returned `linkToken`. The value of `mode` is `RELINK`. 3. When Plaid Link calls `onSuccess`, complete the relink. ```graphql theme={null} mutation CompletePlaidRelink($input: CompletePlaidRelinkInput!) { completePlaidRelink(input: $input) { bankInstitutionAuthId bankInstitutionName platformItemId bankAccounts { bankInstitutionAuthId bankAccountId bankName lastFour type subtype } } } ``` ```json theme={null} { "input": { "platformItemId": "" } } ``` 4. If you get `PLAID_ITEM_STILL_DISCONNECTED`, Plaid has not finished the repair. Wait a short time, then call `completePlaidRelink` again. If you use the relink flow of [Relinking Bank Accounts](/features/relink-accounts) (`createPlaidLinkToken` and `completePlaidLink` with `platformItemId`), it continues to work. If Plaid rejects the `publicToken` of that flow, Fluz completes the relink through the bank link automatically. ## Webhooks You do not need a webhook endpoint. When Fluz creates a Link token with your credentials, Fluz sets the Plaid webhook URL to a Fluz URL for your integration. Plaid sends the updates for these bank links to Fluz. Do not change the webhook URL of these bank links, for example with the Plaid `/item/webhook/update` endpoint. If you change it, Fluz stops receiving updates for the bank link. ## Plaid requests made with your credentials Fluz sends these Plaid requests with your credentials. Plaid charges your Plaid account for them, as specified in your Plaid contract. | Plaid endpoint | When Fluz sends it | | - | - | | `/institutions/get` | When you register or replace credentials, to validate them. | | `/link/token/create` | When a user starts a new link or a relink. | | `/item/public_token/exchange` | When a user completes a new link. | | `/auth/get`, `/accounts/get` | When Fluz saves the bank accounts of a bank link. | | `/accounts/balance/get` | When Fluz gets a real-time balance. | | `/transactions/get` | When Fluz gets transaction history. | | `/item/get` | When a user completes a relink. | | `/item/remove` | When a user removes a bank link. | | `/institutions/get_by_id` | When Fluz saves the details of a bank. | ## Errors Each GraphQL error has a Fluz error code in `extensions.code`. BYOI Plaid errors also have a specific value in `extensions.reason`. ```json theme={null} { "errors": [ { "message": "Plaid rejected the provided clientId and secret.", "extensions": { "code": "G-0007", "reason": "PLAID_INTEGRATION_INVALID_CREDENTIALS" } } ] } ``` ### Errors from the Plaid integration operations | `extensions.reason` | `extensions.code` | Cause and action | | - | - | - | | None | `AUTH-0031` | The token does not include `BYOI_PLAID`. Do Step 2 again. | | `PLAID_INTEGRATION_NOT_APP_DEVELOPER` | `AUTH-0008` | The token is not a Bearer token of the developer of this application. Use the token of the developer. | | None | `AUTH-0034` | Fluz cannot find the application of the token. | | `PLAID_INTEGRATION_INVALID_INPUT` | `G-0007` | `clientId` or `secret` is missing or longer than 255 characters. | | `PLAID_INTEGRATION_INVALID_CREDENTIALS` | `G-0007` | Plaid rejected the credentials. Make sure that the secret is for the Plaid environment in the environments table. | | `PLAID_INTEGRATION_CLIENT_ID_MISMATCH` | `G-0007` | A different client ID is active. See [Change your Plaid client ID](#change-your-plaid-client-id). | | `PLAID_INTEGRATION_NAME_TAKEN` | `G-0007` | Another integration in your Fluz account uses this name. Send a different `name`. | | `PLAID_INTEGRATION_OWNER_ACCOUNT_MISSING` | `G-0007` | The application has no owner account. Contact Fluz support. | | `PLAID_INTEGRATION_CREDENTIALS_UNAVAILABLE` | `G-0010` | Plaid or the credential vault did not respond. Try again later. | ### Errors from bank linking operations | `extensions.reason` | `extensions.code` | Cause and action | | - | - | - | | `PLAID_ITEM_NOT_ACCESSIBLE_TO_APPLICATION` | `AUTH-0008` | The bank link was made with the credentials of a different Plaid integration. Your application cannot use it. The user can link the bank again through your application. | | `PLAID_INTEGRATION_UNAVAILABLE` | `G-0010` | Fluz could not read your Plaid integration. Try again later. | | `PLAID_INTEGRATION_CREDENTIALS_UNAVAILABLE` | `G-0010` | The credentials that created the bank link are disabled, or the vault did not respond. If you disabled the integration, register it again. | | `PLAID_INTEGRATION_REQUEST_FAILED` | `G-0010` | The Plaid request failed. Try again later. | | `PLAID_INTEGRATION_AMBIGUOUS` | `G-0010` | The Plaid integration of the application is not configured correctly. Contact Fluz support. | | `PLAID_INTEGRATION_OWNER_MISMATCH` | `G-0010` | The Plaid integration of the application is not configured correctly. Contact Fluz support. | | `PLAID_ITEM_STILL_DISCONNECTED` | `G-0010` | Plaid has not finished the repair. Wait a short time, then call `completePlaidRelink` again. | | `PLAID_RELINK_ITEM_MISMATCH` | `G-0010` | Fluz could not confirm the bank link. Start the relink again. | | `PLAID_RELINK_ITEM_CHECK_FAILED` | `G-0010` | Fluz could not get the status of the bank link from Plaid. Try again later. | | `PLAID_RELINK_INVALID_REQUEST` | `G-0007` | `platformItemId` is missing or not valid. | If `createPlaidLinkToken` fails after you register, make sure that your client ID has access to Auth, Transactions, Identity, and Signal in the matching Plaid environment. Also make sure that you do not send `deviceOs`. ## Security * Fluz stores your client ID and secret in an encrypted vault. The API never returns them. * The API never returns Plaid access tokens, Plaid public tokens, bank account numbers, or routing numbers. * Only the developer of the application can register, read, or disable its Plaid integration. ## Related pages * [Linking Bank Account via Plaid](/features/link-via-plaid) * [Relinking Bank Accounts](/features/relink-accounts) * [Application Scopes](/fluz-dashboard/application-scopes) * [Configure OAuth App](/create-an-o-auth-app) * [Client-Facing OAuth Grant Flow](/client-facing-o-auth-grant-flow) ## API reference * [`registerPlaidIntegration`](/api-reference/mutations/register-plaid-integration) * [`getPlaidIntegration`](/api-reference/queries/get-plaid-integration) * [`disablePlaidIntegration`](/api-reference/mutations/disable-plaid-integration) * [`createPlaidRelinkToken`](/api-reference/mutations/create-plaid-relink-token) * [`completePlaidRelink`](/api-reference/mutations/complete-plaid-relink) # Card linked offers Source: https://docs.fluz.app/features/card-linked-offers This page details how to retrieve and understand Card Linked Offers (CLOs) using the `getMerchants` query. CLOs are identified by the `type` field value `CARD_LINKED_OFFER` and are often tied to usage of the Fluz Virtual Card — sometimes redeemable exclusively through it. They use a distinct structure, `cloDetails`, for their rate and condition information, differing significantly from standard Gift Card offers. > [!NOTE] > > **Prerequisites:** a user access token with the `LIST_OFFERS` scope. Full query contract: [API Reference](/api-reference/queries/get-merchants). > [!TIP] > > To fetch *only* CLOs, set the `offerTypes` input argument to `{ cardLinkedOffer: true, giftCardOffer: false }`. ## Detailed query for Card Linked Offers This query requests fields specifically relevant to CLOs, focusing on the `cloDetails` object. Fields like `offerRates` and `stockInfo`, which are pertinent to Gift Cards, are typically `null` or empty for CLOs and can often be omitted from the query for CLOs. ```graphql theme={null} query GetCLOMerchants( $name: String, $paginate: OffsetInput, $offerTypes: OfferTypesInput ) { getMerchants( name: $name, paginate: $paginate, offerTypes: $offerTypes ) { merchantId name slug offers { offerId type # Will be CARD_LINKED_OFFER hasStockInfo # Usually false for CLOs denominationsType # Often VARIABLE for CLOs # --- Offer Rates (Usually null/empty for CLOs) --- # offerRates { maxUserRewardValue } # Can query, but expect null # --- Stock Info (Usually null/empty for CLOs) --- # stockInfo { ... on StockInfoVariableType { __typename } } # Usually empty # --- CLO Details (Relevant for CLOs) --- cloDetails { currentRateType regularRate promoRate promoBaseRate promoMaxCap applyPromoBaseRateAfterCap minimumPurchaseAmount activePeriodStartDate activePeriodEndDate periods { offerRateType periodType startDate endDate startTime endTime daysOfWeek daysOfMonth } } } } } # Example Variables to pass with the query: # { # "paginate": { "limit": 20, "offset": 0 }, # "offerTypes": { "cardLinkedOffer": true, "giftCardOffer": false } # } ``` > [!NOTE] > > This query costs 6,396 of the 7,000 points a query can cost. Each field under `periods` costs 625, so adding any one of them (such as `id` from the table below) or uncommenting the `stockInfo` line puts it over the [query cost limit](/concepts/rate-limits). So does a client that adds `__typename` to every selection set, as Apollo Client does by default. Fetch other fields in a separate query. ## Sample response Here's an example response you will get from the `getMerchants` query with CLO Offers Only: ```json JSON theme={null} { "data": { "getMerchants": [ { "merchantId": "4b4195cb-ba98-49e6-8935-761458a76cdd", "name": "REI", "slug": "REI", "offers": [ { "offeringMerchantId": "4b4195cb-ba98-49e6-8935-761458a76cdd", "offerId": "3edd79b7-aa46-4bd6-96de-b78e4f3a79e0", "type": "CARD_LINKED_OFFER", "hasStockInfo": false, "denominationsType": "VARIABLE", "stockInfo": [], "offerRates": null, "cloDetails": { "currentRateType": "PROMO", "regularRate": 22, "promoRate": 25, "promoBaseRate": 20, "promoMaxCap": 1523, "applyPromoBaseRateAfterCap": false, "minimumPurchaseAmount": null, "activePeriodStartDate": null, "activePeriodEndDate": null, "periods": [ { "offerRateType": "PROMO", "periodType": "ALWAYS_AVAILABLE", "startDate": null, "endDate": null, "startTime": null, "endTime": null, "daysOfWeek": null, "daysOfMonth": null }, { "offerRateType": "REGULAR", "periodType": "ALWAYS_AVAILABLE", "startDate": null, "endDate": null, "startTime": null, "endTime": null, "daysOfWeek": null, "daysOfMonth": null } ] } }, { "offeringMerchantId": "4b4195cb-ba98-49e6-8935-761458a76cdd", "offerId": "7ec04538-ad28-4d49-8181-f4ec108facc1", "type": "CARD_LINKED_OFFER", "hasStockInfo": false, "denominationsType": "VARIABLE", "stockInfo": [], "offerRates": null, "cloDetails": { "currentRateType": "PROMO", "regularRate": 4, "promoRate": 17, "promoBaseRate": 12, "promoMaxCap": 1999, "applyPromoBaseRateAfterCap": false, "minimumPurchaseAmount": null, "activePeriodStartDate": "2025-04-21", "activePeriodEndDate": "2025-04-27", "periods": [ { "offerRateType": "PROMO", "periodType": "MULTIPLE_DAYS", "startDate": "2025-04-21", "endDate": "2025-04-27", "startTime": null, "endTime": null, "daysOfWeek": null, "daysOfMonth": null }, { "offerRateType": "REGULAR", "periodType": "ALWAYS_AVAILABLE", "startDate": null, "endDate": null, "startTime": null, "endTime": null, "daysOfWeek": null, "daysOfMonth": null } ] } } ] } ] } } ``` ## Card Linked Offer Specific Fields Explained When `offer.type` is `CARD_LINKED_OFFER`, the primary source of information is the `cloDetails` object within the `Offer`. * **`type`** (`String!`): Will always be `CARD_LINKED_OFFER`. * **`cloDetails`** (`CloDetails`): This complex object contains all the specific rate, promotion, timing rules, and conditions for the Card Linked Offer. Returns `null` for non-CLO types. See the tables below for a breakdown of its fields. * **`offerRates`**: Typically `null` or empty for CLOs. The rate information is contained within `cloDetails`. * **`stockInfo`**: Typically empty for CLOs as they are usually linked to card usage rather than pre-defined stock. * **`hasStockInfo`**: Usually `false` for CLOs. * **`denominationsType`**: Often `VARIABLE` for CLOs, reflecting that the offer applies to a transaction value rather than a fixed gift card amount. ### Understanding the `CloDetails` Object This object provides the specific rate structure for Card Linked Offers. | Field Name | Type | Description | | :- | :- | :- | | `currentRateType` | CloRateTypeEnum! | Indicates if the `REGULAR` or `PROMO` rate is currently active, determined by evaluating all `periods` against the current time. (`REGULAR`, `PROMO`) | | `regularRate` | Float | The standard reward rate applied when no promotion is active. | | `promoRate` | Float | The promotional reward rate applied when a `PROMO` period is active and the purchase amount is below or equal to `promoMaxCap` (if applicable). | | `promoBaseRate` | Float | The reward rate applied during a `PROMO` period *after* the `promoMaxCap` has been exceeded, *if*`applyPromoBaseRateAfterCap` is true. | | `promoMaxCap` | Float | The maximum purchase amount (currency value) up to which the `promoRate` applies during a `PROMO` period. Purchases above this amount may earn `promoBaseRate` or the `regularRate`, depending on flags. | | `applyPromoBaseRateAfterCap` | Boolean | If `true`, the `promoBaseRate` is applied to the portion of the purchase amount *exceeding* the `promoMaxCap` during an active `PROMO` period. If `false`, amounts over the cap might earn no reward or the `regularRate`. | | `minimumPurchaseAmount` | Float | The minimum transaction value required to qualify for *any* reward from this CLO, if specified. | | `activePeriodStartDate` | String | The start date (YYYY-MM-DD) of the *currently active* offer period (could be regular or promo), if the active period has defined start/end dates. `null` if always active or based on other criteria. | | `activePeriodEndDate` | String | The end date (YYYY-MM-DD) of the *currently active* offer period, if applicable. `null` if always active or open-ended. | | `periods` | \[CloPeriod!]! | A list of *all* defined time periods associated with this offer. The system evaluates these periods to determine the `currentRateType`. See the `CloPeriod` table below. | ### Understanding the `CloPeriod` Object (within `CloDetails.periods`) Each object in the `periods` array defines a specific time window and the rate type (`REGULAR` or `PROMO`) associated with it. The combination of these periods determines the offer's availability and active rate. | Field Name | Type | Description | | :- | :- | :- | | `id` | UUID! | Unique identifier for this specific period definition. | | `offerRateType` | CloRateTypeEnum! | Indicates if this period corresponds to the `REGULAR` or `PROMO` rate structure defined in the parent `CloDetails`. | | `periodType` | String | Describes the nature of the period's timing (e.g., `ALWAYS_AVAILABLE`, `MULTIPLE_DAYS`, `SPECIFIC_DAY`, `RECURRING_WEEKLY`). | | `startDate` | String | Start date (YYYY-MM-DD) when this period rule becomes potentially active. | | `endDate` | String | End date (YYYY-MM-DD) when this period rule ceases to be active. | | `startTime` | String | Start time (e.g., "HH:MM" in UTC or a defined timezone) for daily applicability, often used with `daysOfWeek`. | | `endTime` | String | End time (e.g., "HH:MM" in UTC or a defined timezone) for daily applicability. | | `daysOfWeek` | \[String] | Array of days (e.g., \["MONDAY", "FRIDAY"]) when this period rule is active. Used for weekly recurring periods. | | `daysOfMonth` | \[String] | Array of specific days of the month (e.g., \["1", "15"]) when this period rule is active. Used for monthly recurring periods. | ## Next steps > **[Create a virtual card](/features/create-card)** > > Issue the card that these offers reward — CLO earnings apply to its spend. > **[Edit a virtual card](/features/edit-virtual-card)** > > Adjust limits, funding, and lifecycle settings on an existing card. # Close spend accounts Source: https://docs.fluz.app/features/close-spend-accounts # Overview Each cash balance account can be closed, with all it's balance transferred and depending virtual cards' funding source changed or them being locked. # Close User Cash Balance Account ## Sample request You can close a user cash balance account with the `closeUserCashBalance` mutation. This mutation allows you to do the following: * Close your spending account. * Change virtual cards funding source if there are any that have this spending account as one. * Lock related virtual cards. * Transfer the remaining balance to another spend account. The query takes a `CloseUserCashBalanceInput` input object. ```graphql theme={null} mutation closeUserCashBalance($input: CloseUserCashBalanceInput!) { closeUserCashBalance(input: $input) { closedUserCashBalance { userCashBalanceId lifetimeCashBalance nickname status createdAt closedAt } affectedVirtualCards { virtualCardId virtualCardLast4 } } } ``` This mutation requires the `CloseUserCashBalanceInput` input type. Any field marked with an exclamation mark (`!`) in the schema is mandatory and must be included in the request. ## CloseUserCashBalanceInput | Field name | Type | Description | | :- | :- | :- | | idempotencyKey | UUID! | A unique client-generated UUID to ensure a request is processed only once. | | userCashBalanceId | UUID! | An unique identifier for the cash balance account. | | newFundingSource | CloseUserCashBalanceFundingSource | A new funding source for all dependent virtual cards. Required only if there are virtual cards tied to this cash balance account and `lockAllVirtualCards` is not provided. | | lockAllVirtualCards | Boolean | Whether to lock all dependent virtual cards. Required only if there are virtual cards tied to this cash balance account and `newFundingSource` is not provided. | | transferUserCashBalanceId | UUID | An unique identifier for the transfer. Required only if cash balance account's available balance is >0. | ### Example ```json theme={null} { "idempotencyKey": "65666780-c552-47f0-ba05-f90fced0ef9f", "userCashBalanceId": "ca8e1ccf-b42d-41d3-8cad-2237e020d1b1", "lockAllVirtualCards": true, "transferUserCashBalanceId": "6d1b4b19-deef-42f5-80d7-ec34804ce091" } ``` ## CloseUserCashBalanceFundingSource | Field name | Type | Description | | :- | :- | :- | | bankAccountId | UUID | An unique identifier for the bank account. Required if other options aren't present. | | userCashBalanceId | UUID | An unique identifier for the cash balance account. Required if other options aren't present. | | primaryFundingSource: | VirtualCardFundingSource | A type of a primary funding source, either `FLUZ_BALANCE` | | or `BANK_ACCOUNT`. Required if other options aren't present. | | | ### Example ```json theme={null} { "userCashBalanceId": "ca8e1ccf-b42d-41d3-8cad-2237e020d1b1", } ``` ## Sample response The response from the `closeUserCashBalance` mutation will include a closed cash balance account details, including its unique identifier and a list of affected virtual cards. ```json theme={null} { "data": { "closeUserCashBalance": { "closedUserCashBalance": { "userCashBalanceId": "ca8e1ccf-b42d-41d3-8cad-2237e020d1b1", "lifetimeCashBalance": "0.00000", "nickname": "Closed Spend Account", "status": "CLOSED", "createdAt": "2025-12-30T21:18:26.782Z", "closedAt": "2025-12-30T21:22:29.359Z" }, "affectedVirtualCards": [ { "virtualCardId": "afad4d6d-11f0-4ac1-b971-caca7c34c77c", "virtualCardLast4": "4473" } ] } } } ``` ### Response Fields | Field name | Type | Description | | :- | :- | :- | | closeUserCashBalance | ClosedUserCashBalance! | Closed cash balance account details. | | lifetimeCashBalance | \[CloseUserCashBalanceResponseVirtualCard] | A list of virtual cards affected by the closure. | #### closeUserCashBalance | Field name | Type | Description | | :- | :- | :- | | userCashBalanceId | UUID! | Unique identifier for the cash balance account. | | lifetimeCashBalance | String! | Cumulative total of all funds ever deposited (starts at 0). | | nickname | String! | The custom name assigned to the account. | | status | UserCashBalanceStatus! | Current status of the account (should be `CLOSED`). | | createdAt | DateTime! | Timestamp when the account was created. | | closedAt | DateTime! | Timestamp when the account was closed. | #### affectedVirtualCards | Field name | Type | Description | | :- | :- | :- | | virtualCardId | UUID! | Unique identifier for the virtual card. | | virtualCardLast4 | UUID! | The last four digits of the virtual card number. | > ❗️ Authorization required > > This mutation requires the `MANAGE_PAYMENT` scope. Ensure your access token has been granted this scope before attempting to create a cash balance account. # Create authorized user Source: https://docs.fluz.app/features/create-authorized-users Add an existing Fluz user to the caller's account as an authorized user with one or more access roles. This mutation does not create a user — it looks up an existing Fluz user by email or phone, then creates a role assignment on the caller's account (or reactivates a previously `DECLINED`/`INACTIVE` assignment with the new roles). The target account is always resolved from the caller's credentials — Bearer tokens use the token's account; Basic (API key) callers use the application's configured operator account. There is no way to target an account you do not own through this endpoint. 🔒 Restricted Access This mutation requires the `MANAGE_SUBUSERS` scope. It supports both Bearer (user access token) and Basic (``) authentication. The `OWNER` role cannot be assigned through this endpoint. ```graphql theme={null} mutation AddAuthorizedUser( $email: String $phone: String $roles: [UACRoleType!]! $status: UACRoleStatusType $sendInvite: Boolean ) { addAuthorizedUser( email: $email phone: $phone roles: $roles status: $status sendInvite: $sendInvite ) { success authUserId roles status pendingActionId error { code message } } } ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | email | String | No\* | Email address of the existing Fluz user to authorize. \*At least one of `email` or `phone` is required. | | phone | String | No\* | Phone number of the existing Fluz user to authorize. \*At least one of `email` or `phone` is required. | | roles | \[UACRoleType!]! | Yes | One or more roles to assign. Allowed values: `ADMIN`, `MANAGER`, `SPENDER`, `VIEWER`. `OWNER` is not allowed. | | status | UACRoleStatusType | No | The initial status for the role assignment. Defaults to `PENDING`. Set to `ACTIVE` to skip the pending state and activate the assignment immediately (no acceptance required). Allowed values: `PENDING`, `ACTIVE`, `INACTIVE`, `DECLINED`. | | sendInvite | Boolean | No | Whether to send the role assignment invite to the user. Defaults to `true`. Set to `false` to create the assignment without sending an invite, making the invitation optional. | ### Response #### Success Response ```json theme={null} { "data": { "addAuthorizedUser": { "success": true, "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "roles": ["MANAGER", "VIEWER"], "status": "PENDING", "pendingActionId": "2f7c1a3b-9e44-4d2a-8a91-c1b2d3e4f5a6", "error": null } } } ``` ### Response Fields | Field | Type | Description | | :- | :- | :- | | `success` | Boolean | `true` if the role assignment was successfully created or reactivated. | | `authUserId` | UUID | The authorized user ID (UAC role assignment ID). Use this value when calling `removeAuthorizedUser` or filtering results from `authorizedUsers`. | | `roles` | \[UACRoleType] | The roles assigned to the user on this account. | | `status` | UACRoleStatusType | Status of the role assignment: `PENDING`, `ACTIVE`, `INACTIVE`, or `DECLINED`. | | `pendingActionId` | UUID | The pending action ID for the invite, if one was created (returned when the assignment requires user acceptance). | | `error` | AuthorizedUserError | If `success` is false, an Error object containing `code` and `message`. | > Note: This mutation returns errors in the response data, not as GraphQL errors. Always check the `success` field and handle the `error` object when `success` is false. ### Example Request ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddAuthorizedUser($email: String, $phone: String, $roles: [UACRoleType!]!) { addAuthorizedUser(email: $email, phone: $phone, roles: $roles) { success authUserId roles status pendingActionId error { code message } } }", "variables": { "email": "teammate@example.com", "roles": ["MANAGER", "VIEWER"] } }' ``` ```typescript theme={null} const response = await fetch('https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ query: ` mutation AddAuthorizedUser( $email: String $phone: String $roles: [UACRoleType!]! ) { addAuthorizedUser( email: $email phone: $phone roles: $roles ) { success authUserId roles status pendingActionId error { code message } } } `, variables: { email: "teammate@example.com", roles: ["MANAGER", "VIEWER"] } }) }); const data = await response.json(); if (data.data.addAuthorizedUser.success) { console.log('Authorized user added:', data.data.addAuthorizedUser); } else { console.error('Add authorized user failed:', data.data.addAuthorizedUser.error); } ``` #### Skip the pending state and suppress the invite To activate the authorized user immediately without sending an invite, set `status` to `ACTIVE` and `sendInvite` to `false`. The assignment is created in the `ACTIVE` state, no `pendingActionId` is returned, and no invite is sent to the user. ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddAuthorizedUser($email: String, $phone: String, $roles: [UACRoleType!]!, $status: UACRoleStatusType, $sendInvite: Boolean) { addAuthorizedUser(email: $email, phone: $phone, roles: $roles, status: $status, sendInvite: $sendInvite) { success authUserId roles status pendingActionId error { code message } } }", "variables": { "email": "teammate@example.com", "roles": ["MANAGER", "VIEWER"], "status": "ACTIVE", "sendInvite": false } }' ``` ### Error Codes | Code | Message | Description | | :- | :- | :- | | `ARG-0002` | Missing required arguments | Neither `email` nor `phone` was provided, or `roles` is empty. At least one identifier and at least one role are needed. | | `ARG-0001` | Invalid arguments received | One or more values in `roles` are not allowed (for example, `OWNER`), or `status` is not one of `PENDING`, `ACTIVE`, `INACTIVE`, `DECLINED`. Use `ADMIN`, `MANAGER`, `SPENDER`, or `VIEWER` for roles. | | `AUTH-0008` | Invalid user access | The caller could not be resolved from the access token or API key, or the Basic-auth application has no operator account configured. Verify your authentication credentials. | | `AUTH-0031` | The requested scopes must be granted by the user first. | The token is missing the `MANAGE_SUBUSERS` scope required to manage authorized users. | | `AUTH-0034` | No Fluz user found with the provided email or phone number. | No existing Fluz user matches the supplied `email` or `phone`. The target user must already have a Fluz account. | | `AUTH-0035` | This user already has an active role assignment on this account. | The user is already authorized on the caller's account. Use `removeAuthorizedUser` first if you need to reassign roles. | | `AUTH-0037` | Unable to manage authorized user. Please try again or contact support. | A general failure occurred while creating the role assignment. Please retry or contact support. | # Create spend accounts Source: https://docs.fluz.app/features/create-spend-accounts # Overview A user cash balance account is a spending account that allows users to manage and track their available funds within the Fluz platform. Each cash balance account is identified by a unique nickname and maintains separate balances for total, available, and lifetime deposits. Users can create multiple cash balance accounts (e.g., "Team Travel", "Operations Wallet", "Marketing Budget") to organize their spending across different purposes. Cash balance accounts are required for making deposits, purchasing gift cards, and funding virtual card transactions on the Fluz platform. # Create User Cash Balance Account ## Sample request You can create a new user cash balance account with the `createUserCashBalance` mutation. This mutation allows you to set up a new spending account with a custom nickname to help organize your funds. The query takes a `CreateUserCashBalanceInput` input object. ```json theme={null} { "query": "mutation createUserCashBalance($input: CreateUserCashBalanceInput!) { createUserCashBalance(input: $input) { userCashBalanceId totalCashBalance availableCashBalance lifetimeCashBalance nickname status createdAt }}", "variables": { "input": { "nickname": "Team Travel" } } } ``` This mutation requires the `CreateUserCashBalanceInput` input type. Any field marked with an exclamation mark (`!`) in the schema is mandatory and must be included in the request. | Field name | Type | Description | | :- | :- | :- | | nickname | String! | A custom name for the cash balance account. This helps identify the purpose of the account. | ## CreateUserCashBalanceInput ```json theme={null} { "nickname": "Team Travel" } ``` ## Sample response The response from the `createUserCashBalance` mutation will include the newly created cash balance account details, including its unique identifier and initial balance information. ```json theme={null} { "data": { "createUserCashBalance": { "userCashBalanceId": "f8a3c9e1-7b2d-4f5e-9c8a-1d2e3f4g5h6i", "totalCashBalance": "0", "availableCashBalance": "0", "lifetimeCashBalance": "0", "nickname": "Team Travel", "status": "ACTIVE", "createdAt": "2025-01-15T10:30:00.000Z" } } } ``` ### Response Fields | Field name | Type | Description | | :- | :- | :- | | userCashBalanceId | UUID! | Unique identifier for the cash balance account | | totalCashBalance | String! | Total cash balance in the account (starts at 0) | | availableCashBalance | String! | Available cash balance for immediate use (starts at 0) | | lifetimeCashBalance | String! | Cumulative total of all funds ever deposited (starts at 0) | | nickname | String | The custom name assigned to the account | | status | UserCashBalanceStatus! | Current status of the account (ACTIVE, CLOSED, or SUSPENDED) | | createdAt | DateTime! | Timestamp when the account was created | > ❗️ Authorization required > > This mutation requires the `MANAGE_PAYMENT` scope. Ensure your access token has been granted this scope before attempting to create a cash balance account. # Issue cards Source: https://docs.fluz.app/features/create-virtual-card Use the `createVirtualCard` mutation to create a virtual card with configurable funding sources, limits, and lifecycle controls. > [!NOTE] > > **Prerequisites:** a user access token with the `CREATE_VIRTUALCARD` scope, and a `CreateVirtualCardInput` object. To find an `offerId`, see [Get Virtual Card Offers](/features/get-card-offers). > [!NOTE] > > **Billing address must be verifiable.** The billing address — whether passed inline as `billingAddress` or referenced by `userAddressId` — must be a real, deliverable **US** address (`US`, `USA`, or `United States`) with a correct city, state, and ZIP; **PO boxes are not accepted**. It is validated against USPS data (via Smarty) as the cardholder is set up. If it can't be verified, the card is not created and the request fails with `VC-0025` (`UnableToCreateAuthUser`), returned on `extensions.code`. See [Address Formatting Requirements](/concepts/address-formatting-requirements). ## Sandbox test offers | Offer ID | Program Name | Reward Value | | - | - | - | | `ed669305-5e43-40a0-9a25-7a15ed174628` | Virtual Card | 1.5% | | `b23630f6-8d91-43df-84aa-a541e7691197` | Virtual Card - Mastercard Prepaid | 1.5% | | `592c394e-26cc-44ac-a145-a5f81301fe77` | Brand Locked Virtual Card - Mastercard Prepaid | 1.5% | ## Important considerations * **Funding Source:** You can fund your virtual cards using either your Fluz balance or a linked bank account as the primary funding source. * If you select a bank account as the `primaryFundingSource`, you must provide the `bankAccountId`. * **Spend Limits:** The `spendLimit` you set must adhere to the program's defined spend limits for the chosen `spendLimitDuration`. An error will occur if the limit is exceeded. * **Balance Composition:** By default, virtual cards may be funded by a combination of your specified spend account, prepaid (gift card) balance, and rewards balance when available. You can restrict the card to draw only from the spend account by setting `usePrepaymentBalance: false` and `useRewardsBalance: false` in the input. * **Transaction Annotations:** You may optionally attach a `memo` and/or `transactionCategory` at card creation time. These will be associated with the resulting transaction for tracking and organization purposes. * If `transactionCategory` is provided, a matching category will be found or created for the account. * `attachmentId` is not supported ## Arguments * `input` (`CreateVirtualCardInput!`): The input object containing details for the new virtual card. ## CreateVirtualCardInput fields | Field | Type | Description | Required | | - | - | - | - | | `idempotencyKey` | `UUID!` | A unique client-generated UUID to ensure a request is processed only once. | Yes | | `spendLimit` | `Float!` | The maximum amount you can charge to the card. You are only charged for the amount actually used. | Yes | | `spendLimitDuration` | `VirtualCardSpendLimitDuration` | Card limit duration type. Default is `LIFETIME`. | No | | `lockDate` | `String` | The date when the card will be locked. The default is 47 months from creation. Format: yyyy-mm-dd | No | | `lockCardNextUse` | `Boolean` | Setting to lock the card after its next use. The default is `false`. | No | | `cardNickname` | `String` | The card's nickname. | No | | `primaryFundingSource` | `VirtualCardFundingSource` | Primary Funding Source for Virtual Card. Default is `FLUZ_BALANCE`. | No | | `bankAccountId` | `UUID` | The unique identifier for the bank account. Required if `primaryFundingSource` is `BANK_ACCOUNT`. | No | | `offerId` | `UUID` | The Offer Id of Virtual Card Offer. Use `getVirtualCardOffers` to fetch a list of active offers. | No | | `userCashBalanceId` | `UUID` | The cash balance (spend account) to be used for that purchase. | No | | `usePrepaymentBalance` | `Boolean` | When `false`, the card will not draw from the prepaid (gift card) balance. Defaults to `true`, preserving the existing behavior where prepaid funds may be used in addition to the specified spend account. | No | | `useRewardsBalance` | `Boolean` | When `false`, the card will not draw from the rewards balance. Defaults to `true`, preserving the existing behavior where rewards may be used in addition to the specified spend account. | No | | `billingAddress` | `VirtualCardBillingAddressInput` | Billing address for the virtual card. If it doesn't match an existing address on the user's account (by street, city, state, and postal code), a new one is created. All fields are required except streetAddressLine2. Only U.S. addresses are supported. If userAddressId is provided, this field is ignored. | No | | `userAddressId` | `UUID` | The ID of an existing billing address on the user's account to use for the virtual card. If provided, it takes precedence over billingAddress. The ID must reference a UserAddress belonging to the calling user. | No | | `memo` | `String` | An optional note to attach to the transaction. | No | | `transactionCategory` | `String` | An optional category name. A matching category will be found or created for the account. | No | ## VirtualCardBillingAddressInput fields | Field | Type | Description | Required | | - | - | - | - | | `streetAddressLine1` | `String!` | The primary street address (e.g., "123 Main St"). PO boxes are not accepted by the card issuer. | Yes | | `streetAddressLine2` | `String` | Optional secondary address line (e.g., apartment, suite, or unit number). | No | | `country` | `String!` | The country name. Currently only "United States" is supported. | Yes | | `city` | `String!` | The city name. | Yes | | `state` | `String!` | The state name (e.g., "New York") or two-letter US state code (e.g., "NY"). | Yes | | `postalCode` | `String!` | A 5-digit US ZIP code. | Yes | Addresses that fail verification return `VC-0025` (`UnableToCreateAuthUser`). See [Address Formatting Requirements](/concepts/address-formatting-requirements) and [Virtual Card Error Codes](/features/virtual-card-error-codes). ## cURL Example ```bash theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation CreateVirtualCard { createVirtualCard( input: { spendLimit: 150.00, idempotencyKey: \"63b2c9e0-62d1-42ab-b1c2-1a7ee2f8c0a9\", offerId: \"b3355504-ad30-4b2f-873d-b8795277b918\", spendLimitDuration: MONTHLY, lockCardNextUse: false, cardNickname: \"My New Test Card\", primaryFundingSource: FLUZ_BALANCE, memo: \"Office supplies\", transactionCategory: \"Expenses\" } ) { virtualCardId userId cardholderName expiryMonth expiryYear virtualCardLast4 status cardType initialAmount usedAmount createdAt } }" }' ``` ## Sample Mutation ```graphql theme={null} mutation { createVirtualCard( input: { idempotencyKey: "07df5653-43a8-4532-9881-3ab5857bbe12" spendLimit: 123.45 spendLimitDuration: DAILY lockDate: "2030-10-10" lockCardNextUse: true cardNickname: "xyz789" primaryFundingSource: FLUZ_BALANCE offerId: "b3355504-ad30-4b2f-873d-b8795277b918" userCashBalanceId: "b1155504-ad30-4b2f-873d-b8795277b128" } ) { virtualCardId userId cardholderName expiryMonth expiryYear virtualCardLast4 status cardType initialAmount usedAmount createdAt } } ``` ## Sample Mutation — Cash-Only Balance Restrict a virtual card to draw only from the specified `userCashBalanceId`, excluding prepaid and rewards funds. ```graphql theme={null} mutation { createVirtualCard( input: { idempotencyKey: "07df5653-43a8-4532-9881-3ab5857bbe13" spendLimit: 150.00 offerId: "b3355504-ad30-4b2f-873d-b8795277b918" primaryFundingSource: FLUZ_BALANCE userCashBalanceId: "b1155504-ad30-4b2f-873d-b8795277b128" usePrepaymentBalance: false useRewardsBalance: false } ) { virtualCardId virtualCardLast4 status initialAmount } } ``` ## Sample Response ```json theme={null} { "data": { "createVirtualCard": { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "userId": "07df5653-43a8-4532-9881-3ab5857bbe11", "cardholderName": "xyz789", "expiryMonth": "12", "expiryYear": "27", "virtualCardLast4": "7890", "status": "ACTIVE", "cardType": "MULTI_USE", "initialAmount": 150.00, "usedAmount": 0, "createdAt": "2025-07-09T10:00:00Z" } } } ``` ### Response fields | Field | Type | Description | | - | - | - | | `virtualCardId` | `UUID` | The unique identifier assigned to the newly created virtual card. | | `userId` | `UUID` | The unique identifier of the user who created this virtual card. | | `cardholderName` | `String` | The name of the cardholder as it appears on the virtual card. | | `expiryMonth` | `String` | The two-digit expiration month of the virtual card (e.g., "12" for December). | | `expiryYear` | `String` | The two-digit expiration year of the virtual card (e.g., "27" for 2027). | | `virtualCardLast4` | `String` | The last four digits of the virtual card number. | | `status` | `VirtualCardStatus` | The current status of the virtual card (e.g., ACTIVE, PENDING). | | `cardType` | `VirtualCardType` | Indicates if the card is MULTI\_USE or SINGLE\_USE. | | `initialAmount` | `Float` | The original spendLimit set when the card was created. | | `usedAmount` | `Float` | The total amount that has been spent using this virtual card so far. | | `createdAt` | `DateTime` | Timestamp when the virtual card was created. | ## Next steps > **[Reveal the card](/recipes/reveal-virtual-card)** > > Retrieve the PAN, CVV, and expiry so the card can actually be used. > **[Get virtual card offers](/features/get-card-offers)** > > Enumerate every program available to your account to pick the right `offerId`. # Create virtual card for authorized user Source: https://docs.fluz.app/features/create-virtual-card-for-authorized-user Create a virtual card on behalf of an authorized user on the caller's account. Pass the authorized user's `authUserId` (the UAC role assignment ID returned by `addAuthorizedUser`) to create the card for that user's underlying cardholder record while keeping the card scoped to the caller's account. The `authUserId` must belong to the caller's account, must be ACTIVE, and cannot be an `OWNER` assignment. If `addAuthorizedUser` returns `PENDING`, the authorized user must accept the invite before this mutation can use that `authUserId`. Virtual card creation can use a saved billing address (`userAddressId`) or an inline `billingAddress`. For select offers, providing either address value triggers billing-address registration with the card issuer before the card is issued. If issuer approval is still pending after the server-side wait window, the mutation returns GraphQL error code `VC-0020` with `extensions.addressId`; retry with that value as `userAddressId`. 🔒 Restricted Access This mutation requires a Bearer token with the `CREATE_VIRTUALCARD` scope. ```graphql theme={null} mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId userId cardholderName expiryMonth expiryYear virtualCardLast4 status cardType initialAmount usedAmount createdAt authorizationSetting { lockDate dailySpendLimit weeklySpendLimit monthlySpendLimit } } } ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | input | CreateVirtualCardInput! | Yes | Wrapper object for the virtual card creation request. | | input.idempotencyKey | UUID! | Yes | Unique client-generated UUID. The same key prevents the same request from being processed more than once. | | input.spendLimit | Float! | Yes | Maximum amount that can be charged to the card. Minimum is `$5`; at most two decimal places are allowed. | | input.offerId | UUID | Yes | Virtual card offer ID. Use `getVirtualCardOffers` to fetch active offers. | | input.authUserId | UUID | No | Authorized user ID (UAC role assignment ID) to create the card on behalf of. Must be ACTIVE and non-owner. | | input.userAddressId | UUID | No | Saved billing address ID for the caller or authorized cardholder. Takes precedence over `billingAddress`. | | input.billingAddress | VirtualCardBillingAddressInput | No | New billing address to save and register with the card program before issuing the card. | | input.spendLimitDuration | VirtualCardSpendLimitDuration | No | Card limit duration. Defaults to `LIFETIME`. | | input.lockDate | String | No | Future lock date. Defaults to 47 months from the request date. | | input.lockCardNextUse | Boolean | No | Whether to lock the card after the next use. Defaults to `false`. | | input.cardNickname | String | No | Card nickname. Must be 1-50 characters when provided. | | input.primaryFundingSource | VirtualCardFundingSource | No | Primary funding source. Defaults to `FLUZ_BALANCE`. If `BANK_ACCOUNT`, `bankAccountId` is required. | | input.bankAccountId | UUID | No | Bank account ID used when `primaryFundingSource` is `BANK_ACCOUNT`. | | input.userCashBalanceId | UUID | No | Spend account ID to attach to the card. | | input.usePrepaymentBalance | Boolean | No | When `false`, the card will not draw from prepaid balance. Defaults to `true`. | | input.useRewardsBalance | Boolean | No | When `false`, the card will not draw from rewards balance. Defaults to `true`. | | input.memo | String | No | Optional note attached to the transaction. Maximum 255 characters. | | input.transactionCategory | String | No | Optional category name. A matching category is found or created for the account. Maximum 100 characters. | ### Response #### Success Response ```json theme={null} { "data": { "createVirtualCard": { "virtualCardId": "6d9f0d0f-21f0-4ad1-8fc7-2fb7dd58ce11", "userId": "f1320ac4-52dc-4c67-9e80-24e506b18450", "cardholderName": "Ada Lovelace", "expiryMonth": "08", "expiryYear": "2029", "virtualCardLast4": "4242", "status": "ACTIVE", "cardType": "MULTI_USE", "initialAmount": 100, "usedAmount": 0, "createdAt": "2026-05-15T15:18:00.000Z", "authorizationSetting": { "lockDate": "2029-08-15", "dailySpendLimit": null, "weeklySpendLimit": null, "monthlySpendLimit": null } } } } ``` ### Response Fields | Field | Type | Description | | :- | :- | :- | | `virtualCardId` | UUID | Created virtual card ID. | | `userId` | UUID | User ID of the cardholder. When `authUserId` is provided, this is the authorized user's user ID. | | `cardholderName` | String | Cardholder name shown on the virtual card. | | `expiryMonth` | String | Virtual card expiration month. | | `expiryYear` | String | Virtual card expiration year. | | `virtualCardLast4` | String | Last 4 digits of the virtual card number. | | `status` | String | Virtual card status, such as `ACTIVE`. | | `cardType` | String | Virtual card type. | | `initialAmount` | Float | Initial spend limit requested for the card. | | `usedAmount` | Float | Amount already spent on the card. | | `createdAt` | DateTime | Time when the card was created. | | `authorizationSetting` | Object | Authorization settings attached to the virtual card. | ### Example Requests Create a card for an authorized user with a saved address: ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId userId cardholderName virtualCardLast4 status cardType initialAmount usedAmount createdAt } }", "variables": { "input": { "idempotencyKey": "931e8841-cf23-4f36-8bf8-169384042ec2", "offerId": "09a9c8d1-9c4b-46fa-8a7a-508812a2a0d9", "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "userAddressId": "1f6b2c3d-4e5f-4a67-9a10-2b3c4d5e6f70", "spendLimit": 100, "spendLimitDuration": "LIFETIME", "cardNickname": "Ada Travel Card", "primaryFundingSource": "FLUZ_BALANCE" } } }' ``` Create a card for an authorized user with a new inline billing address: ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId userId cardholderName virtualCardLast4 status cardType initialAmount usedAmount createdAt } }", "variables": { "input": { "idempotencyKey": "c20341f0-47e3-4d52-8361-1d7c37736b65", "offerId": "09a9c8d1-9c4b-46fa-8a7a-508812a2a0d9", "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "spendLimit": 100, "billingAddress": { "streetAddressLine1": "456 Market St", "streetAddressLine2": "Suite 200", "country": "United States", "city": "San Francisco", "state": "CA", "postalCode": "94105" } } } }' ``` ```typescript theme={null} const response = await fetch('https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query: ` mutation CreateVirtualCard( $input: CreateVirtualCardInput! ) { createVirtualCard(input: $input) { virtualCardId userId cardholderName virtualCardLast4 status cardType initialAmount usedAmount createdAt } } `, variables: { input: { idempotencyKey: '931e8841-cf23-4f36-8bf8-169384042ec2', offerId: '09a9c8d1-9c4b-46fa-8a7a-508812a2a0d9', authUserId: '8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d', userAddressId: '1f6b2c3d-4e5f-4a67-9a10-2b3c4d5e6f70', spendLimit: 100, spendLimitDuration: 'LIFETIME', cardNickname: 'Ada Travel Card', primaryFundingSource: 'FLUZ_BALANCE', }, }, }), }); const data = await response.json(); if (data.errors?.[0]?.extensions?.code === 'VC-0020') { const userAddressId = data.errors[0].extensions.addressId; console.log('Billing address pending issuer approval. Retry with userAddressId:', userAddressId); } else { console.log('Virtual card created:', data.data.createVirtualCard); } ``` ### Full Flow: Register User, Add Authorized User, Add Address, Create Card This flow registers a new Fluz user, adds that user to the caller's account as an authorized user, saves a billing address for that authorized cardholder, then creates a virtual card on their behalf. Step 1: Register the user. This requires a Bearer token for the developer user of an ACTIVE application with registration enabled. ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation RegisterUser($firstName: String!, $lastName: String!, $phoneNumber: String!, $regionCode: String!, $emailAddress: String!, $dateOfBirth: String!, $billingAddress: VirtualCardBillingAddressInput!, $acceptCardholderAgreement: Boolean!) { registerUser(firstName: $firstName, lastName: $lastName, phoneNumber: $phoneNumber, regionCode: $regionCode, emailAddress: $emailAddress, dateOfBirth: $dateOfBirth, billingAddress: $billingAddress, acceptCardholderAgreement: $acceptCardholderAgreement) { success error { code message } } }", "variables": { "firstName": "Ada", "lastName": "Lovelace", "phoneNumber": "5555555555", "regionCode": "US", "emailAddress": "ada.lovelace@example.com", "dateOfBirth": "1990-01-31", "billingAddress": { "streetAddressLine1": "1600 Amphitheatre Pkwy", "city": "Mountain View", "state": "CA", "postalCode": "94043", "country": "United States" }, "acceptCardholderAgreement": true } }' ``` Step 2: Add the registered user as an authorized user on the caller's account. ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddAuthorizedUser($email: String, $roles: [UACRoleType!]!) { addAuthorizedUser(email: $email, roles: $roles) { success authUserId roles status pendingActionId error { code message } } }", "variables": { "email": "ada.lovelace@example.com", "roles": ["SPENDER"] } }' ``` Continue only after the returned `status` is `ACTIVE`. If the status is `PENDING`, the authorized user must accept the invite before `addVirtualCardAddress` or `createVirtualCard` can use the returned `authUserId`. Step 3: Save the authorized user's billing address. ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation AddVirtualCardAddress($input: AddVirtualCardAddressInput!) { addVirtualCardAddress(input: $input) { userAddressId streetAddressLine1 streetAddressLine2 country city state postalCode } }", "variables": { "input": { "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "billingAddress": { "streetAddressLine1": "456 Market St", "streetAddressLine2": "Suite 200", "country": "United States", "city": "San Francisco", "state": "CA", "postalCode": "94105" } } } }' ``` Step 4: Create the virtual card for the authorized user with the saved address. ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId userId cardholderName virtualCardLast4 status cardType initialAmount usedAmount createdAt } }", "variables": { "input": { "idempotencyKey": "931e8841-cf23-4f36-8bf8-169384042ec2", "offerId": "09a9c8d1-9c4b-46fa-8a7a-508812a2a0d9", "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "userAddressId": "1f6b2c3d-4e5f-4a67-9a10-2b3c4d5e6f70", "spendLimit": 100, "spendLimitDuration": "LIFETIME", "cardNickname": "Ada Travel Card", "primaryFundingSource": "FLUZ_BALANCE" } } }' ``` ### Error Codes | Code | Message | Description | | :- | :- | :- | | `ARG-0001` | Invalid arguments received | Required input is missing or invalid, `spendLimit` is below `$5`, `lockDate` is not in the future, `offerId` is invalid, `authUserId` is invalid, `userAddressId` does not belong to the account/cardholder, or `BANK_ACCOUNT` funding was selected without `bankAccountId`. | | `AUTH-0008` | Invalid user access | The Bearer token could not be resolved to a caller. Verify the token is valid. | | `AUTH-0031` | The requested scopes must be granted by the user first. | The token is missing the `CREATE_VIRTUALCARD` scope required to create virtual cards. | | `AUTH-0034` | No authorized user found with this id on the caller's account. | The `authUserId` does not exist on the caller's account, is not ACTIVE, or refers to an OWNER assignment. | | `VC-0001` | Please try another payment method. If you continue experiencing issues, please contact our support team. | The card could not be created or the issuer rejected the billing address. | | `VC-0019` | We are unable to get the virtual card offer. If you continue experiencing issues, please contact our support team. | The requested virtual card offer could not be resolved. | | `VC-0020` | Virtual card billing address is pending approval. Please retry shortly using the returned addressId. | The billing address was submitted to the issuer but was not approved before the server-side wait window ended. Retry using `extensions.addressId` as `userAddressId`. | # Delete bank card Source: https://docs.fluz.app/features/delete-bank-card You can delete (deactivate) an existing bank card using the `deleteBankCard` mutation. This mutation requires the `MANAGE_PAYMENT` scope. The `deleteBankCard` [mutation](/api-reference/overview) sets the status of a bank card to `INACTIVE`. Once deleted, the card cannot be used for transactions. ### Sample request ```json theme={null} { "query": "mutation deleteBankCard($input: DeleteBankCardInput!) { deleteBankCard(input: $input) { bankCardId cardStatus }}", "variables": { "input": { "bankCardId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19" } } } ``` This mutation requires the `DeleteBankCardInput` input type. Any field marked with an exclamation mark (`!`) in the schema is mandatory and must be included in the request. | Field name | Type | Description | | :- | :- | :- | | bankCardId | UUID! | The unique identifier of the bank card. | ### DeleteBankCardInput ```Text JSON theme={null} { "bankCardId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19" } ``` ### Sample response ```json theme={null} { "data": { "deleteBankCard": { "bankCardId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "cardStatus": "INACTIVE" } } } ``` > [!WARNING] > > Deleting a bank card will deactivate it immediately. The card will no longer be available for any transactions or funding operations. # Deposit from external accounts Source: https://docs.fluz.app/features/deposit-from-external-accounts depositCashBalance When you're ready to move money into your Fluz account, you can deposit funds into the following three balances: 1. **Cash balance**- A regular cash balance deposit. 2. **Gift card balance**- A non-withdrawable deposit for gift card balance. 3. **Reserve balance** - A deposit held in reserve balance. # Make a Deposit Deposits require the **`MAKE_DEPOSIT`** scope on the access token. To deposit funds into one of your Fluz balances, use the `depositCashBalance` [mutation](/api-reference/mutations/deposit-cash-balance). This mutation accepts an input object type of `DepositCashBalanceInput`, which allows you to specify the following details: 1. **Amount:** **Required** - The amount you wish to deposit. 2. **idempotencyKey:** **Required** - A unique client-generated UUID to ensure a request is processed only once. 3. **Funding Source:** The payment method from which the deposit will be made (e.g., bank account, credit or debit card, PayPal). In order to get your funding source's ID, use the `getWallet` [query](/api-reference/queries/get-wallet). 1. **`bankAccountId`** - If you want to pay with a bank account, define the bank account ID in the `depositCashBalance` mutation. 2. **`bankCardId`** - If you want to pay with a bank card, define the bank card ID in the `depositCashBalance` mutation. 3. **`paypalVaultID`** - If you want to pay with a PayPal account, define the PayPal account ID in the `depositCashBalance` mutation. 4. **Deposit Destination:** The balance where you want to deposit your funds. The available options for this are defined in the `CashBalanceDepositType` enumerator: 1. **`CASH_BALANCE`** 2. **`GIFT_CARD_BALANCE`** 3. **`RESERVE_BALANCE`** 5. **Merchant Category Code:** Only applies for **GIFT\_CARD\_BALANCE**. A four-digit number that classifies a business by the type of products or services it offers. Use `getMccList` to retrieve a list of valid MCCs. 6. **`userCashBalanceId`** - If `CASH_BALANCE`is selected, you can specify the cash balance (spend account) to deposit your funds 7. *`memo`*\* - If you want to attach a note to this transaction, provide a free-text memo here. Max 255 characters. 8. **`transactionCategory`** - If you want to categorize this transaction, provide a category name. Categories are created automatically on first use and reused if the same name is passed again. 9. **`attachmentId`** - If you want to attach a file to this transaction, provide the ID returned by the upload endpoint. See [Add Expense Details](/features/add-expense-details). 📘 See [Add Expense Details](/features/add-expense-details) for full details on uploading attachments and working with memos and categories. ## Sample Request ```graphql graphql theme={null} mutation depositCashBalance($input: DepositCashBalanceInput!) { depositCashBalance(input: $input) { cashBalanceDeposits { ...CashBalanceDepositFragment } balances { ...UserBalancesFragment } } } ``` ## DepositCashBalanceInput ```json theme={null} { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "amount": 987.65, "depositType": "GIFT_CARD_BALANCE", "merchantCategoryCode": 1234, "bankAccountId": "0285c162-fb2f-4c32-b076-29166471f570", "bankCardId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "paypalVaultId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "userCashBalanceId": "0347adef-2b69-45e6-8db0-5b5daddf45d2" } ``` ## Sample Response ```json theme={null} { "data": { "depositCashBalance": { "cashBalanceDeposits": [ { "cashBalanceDepositId": "5f5c5b5f-4c4b-4e4e-9e9e-4f4f4f4f4f4f", "depositDisplayId": "102370", "depositAmount": "100.00", "depositFee": "2.00", "bankAccountId": "b2c3d4e5-f6a7-890b-c12d-34ef5678gh90", "transactionDate": "2024-08-21T10:30:00Z", "clearedDate": "2024-08-22T12:00:00Z", "status": "COMPLETED", "expectedClearedDate": "2024-08-22T12:00:00Z", "cashBalanceDepositType": "CASH_BALANCE", "cashBalanceSettlements": [ { "cashBalanceSettlementId": "255f8245-02c7-4817-901e-15fe265f6968", "cashBalanceDepositId": "255f8245-02c7-4817-901e-15fe265f6968", "availabilityType": "INSTANT", "status": "AVAILABLE" } ] } ], "balances": { "cashBalance": { "availableBalance": "25.00", "totalBalance": "25.00", "pendingBalance": "25.00", "lifetimeBalance": "25.00", }, "rewardBalance": { "availableBalance": "25.00", "totalBalance": "25.00", "lifetimeBalance": "25.00", }, "giftCardCashBalance": { "availableBalance": "25.00", "totalBalance": "25.00", "pendingBalance": "25.00", "lifetimeBalance": "25.00", } } } } } ``` # Debit Card Deposits A deposit funded by a **debit card** may be processed as an **account funding transaction (AFT)** — a card-network transaction type designed for moving funds into an account. Fluz applies it automatically where it is available; there is no field to request it. What it changes for you is the fee. AFT applies only when all of the following hold: * The funding source is a `bankCardId`, and the card's `cardType` is `DEBIT`. * `depositType` is `CASH_BALANCE`. Deposits to `GIFT_CARD_BALANCE` and `RESERVE_BALANCE` never use AFT. * The card is AFT-eligible. `CREDIT` and `PREPAID` bank cards never use AFT, and neither do bank accounts (`bankAccountId`) or PayPal (`paypalVaultId`). They are charged the standard rate for their payment method — this is a normal outcome, not an error. ## Fees on Debit Card Deposits AFT-eligible cards are charged an AFT-specific rate. Cards that are not AFT-eligible are charged the standard debit-card rate instead — this is a normal outcome, not an error. Because the two rates differ, read `depositFee` from the response instead of computing the fee client-side. ## Card Eligibility Whether a card supports AFT is a property of the card itself rather than something you configure. Eligibility is evaluated when the deposit is submitted. > [!NOTE] > > **Eligibility is not available in advance, and does not carry over to withdrawals.** > > There is no query that reports whether a card supports AFT — it is reflected in the fee on the first deposit from that card. Deposit and withdrawal eligibility are also independent, so a card a user can deposit from is not necessarily a card you can send a withdrawal to. See [Withdraw to External Account](/features/withdraw-to-external-account). Handle these decline codes on debit-card deposits: | Code | Meaning | What to do | | - | - | - | | `HN-0125` | This card is not eligible for account funding (AFT) transfers. | Ask the user for a different funding source. | | `PG-0003` | The deposit could not be processed for this card right now. | Retry later, or fall back to another funding source. | | `BC-0004` | The card was declined by the issuer. | Surface the message; the user should contact their bank or try another card. | Keep an alternative funding source reachable in your UI so an ineligible or declined card does not dead-end the flow. # Deposit Response Once you initiate a deposit with the `depositCashBalance` [mutation](/api-reference/mutations/deposit-cash-balance), you will get a response providing details about the deposit. | Field Name | Type | Description | | :- | :- | :- | | cashBalanceDeposits | \[CashBalanceDeposit] | List of cash balance deposits made as part of the mutation. | | balances | UserBalances | The current balances for the user, including cash balance, rewards balance, and Fluz prepayment balance. | The `CashBalanceDeposit` object will contain important information about the deposit you initiated. | Field Name | Type | Description | | :- | :- | :- | | cashBalanceDepositId | UUID! | Unique identifier for the cash balance deposit. | | depositDisplayId | String! | Display identifier for the deposit, intended for user-facing purposes. | | depositAmount | String! | The amount of the deposit. | | depositFee | String | Fee associated with the deposit, if applicable. | | bankCardId | UUID | Identifier of the bank card used for the deposit. | | bankAccountId | UUID | Identifier of the bank account used for the deposit. | | paypalVaultId | UUID | Identifier of the PayPal vault used for the deposit. | | transactionDate | DateTime! | Date and time when the transaction was made. | | clearedDate | DateTime | Date and time when the deposit cleared, if applicable. | | status | CashBalanceDepositStatus! | Current status of the deposit. | | expectedClearedDate | DateTime! | Expected date and time for the deposit to clear. | | cashBalanceDepositType | CashBalanceDepositType! | Type of the cash balance deposit: `CASH_BALANCE`, `GIFT_CARD_BALANCE`, or `RESERVE_BALANCE`. | | cashBalanceSettlements | \[CashBalanceSettlement] | Settlement time details for the deposit, including status and type. | > 📘 Please note > > Deposits may settle instantly or within 2-5 business days. To learn how settlement times are calculated, [read this article.](https://help.fluz.app/en/articles/5773275-instant-access) # Edit spend accounts Source: https://docs.fluz.app/features/edit-spend-accounts # Overview Each cash balance account has it's own nickname so user could easily distinguish between them, if there are a few of them. # Edit User Cash Balance Account ## Sample request You can update a user cash balance account with the `updateUserCashBalance` mutation. This mutation allows you edit spend account's nickname to help organize your funds. The query takes a `UpdateUserCashBalanceInput` input object. ```json theme={null} mutation updateUserCashBalance($input: UpdateUserCashBalanceInput!) { updateUserCashBalance(input: $input) { userCashBalanceId totalCashBalance availableCashBalance lifetimeCashBalance nickname status createdAt } } ``` This mutation requires the `UpdateUserCashBalanceInput` input type. Any field marked with an exclamation mark (`!`) in the schema is mandatory and must be included in the request. | Field name | Type | Description | | :- | :- | :- | | userCashBalanceId | UUID! | An unique identifier for the cash balance account. | | nickname | String! | A custom name for the cash balance account. This helps identify the purpose of the account. Has to be at least 2 characters long, but no longer than 100. | ## UpdateUserCashBalanceInput ```json theme={null} { "userCashBalanceId": "f8a3c9e1-7b2d-4f5e-9c8a-1d2e3f4g5h6i", "nickname": "New Team Travel" } ``` ## Sample response The response from the `updateUserCashBalance` mutation will include the newly created cash balance account details, including its unique identifier and initial balance information. ```json theme={null} { "data": { "updateUserCashBalance": { "userCashBalanceId": "f8a3c9e1-7b2d-4f5e-9c8a-1d2e3f4g5h6i", "totalCashBalance": "0", "availableCashBalance": "0", "lifetimeCashBalance": "0", "nickname": "New Team Travel", "status": "ACTIVE", "createdAt": "2025-01-15T10:30:00.000Z" } } } ``` ### Response Fields | Field name | Type | Description | | :- | :- | :- | | userCashBalanceId | UUID! | Unique identifier for the cash balance account | | totalCashBalance | String! | Total cash balance in the account (starts at 0) | | availableCashBalance | String! | Available cash balance for immediate use (starts at 0) | | lifetimeCashBalance | String! | Cumulative total of all funds ever deposited (starts at 0) | | nickname | String | The custom name assigned to the account | | status | UserCashBalanceStatus! | Current status of the account (ACTIVE, CLOSED, or SUSPENDED) | | createdAt | DateTime! | Timestamp when the account was created | > ❗️ Authorization required > > This mutation requires the `MANAGE_PAYMENT` scope. Ensure your access token has been granted this scope before attempting to create a cash balance account. # Edit card overview Source: https://docs.fluz.app/features/edit-virtual-card The `editVirtualCard` mutation allows you to update various details of an existing virtual card, such as spend limits, lock date, and nickname. > [!NOTE] > > **Prerequisites:** a user access token with the `EDIT_VIRTUALCARD` scope, and the `virtualCardId` of the card to update. ## Arguments * **`input`** (`EditVirtualCardInput!`): The input object containing the virtual card ID and the fields to update. ## EditVirtualCardInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `virtualCardId` | `UUID!` | The virtual card to update. | Yes | | `spendLimit` | `Float` | The maximum amount that you can charge to the card. | No | | `spendLimitDuration` | `VirtualCardSpendLimitDuration` | Card limit duration type. | No | | `lockDate` | `String` | The date when the card will be locked. Format: yyyy-mm-dd | No | | `lockCardNextUse` | `Boolean` | The setting to lock the card after next use. | No | | `cardNickname` | `String` | The card's nickname. | No | | `primaryFundingSource` | `VirtualCardFundingSource` | Primary Funding Source for Virtual Card. | No | | `bankAccountId` | `UUID` | The unique identifier for the bank account. | No | | `userCashBalanceId` | `UUID` | The unique identifier for the user cash balance. | No | ## cURL example ```curl theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation { editVirtualCard( input: { virtualCardId: \"07df5653-43a8-4532-9881-3ab5857bbe11\", spendLimit: 200.00, spendLimitDuration: MONTHLY, cardNickname: \"Edited Card Nickname\", lockCardNextUse: false } ) { virtualCardId cardholderName expiryMonth expiryYear virtualCardLast4 status cardType initialAmount usedAmount createdAt authorizationSetting { virtualCardAsaSettingsId lockDate dailySpendLimit monthlySpendLimit cardNickname lockCardNextUse } } }" }' ``` ## Sample response ```json theme={null} { "data": { "editVirtualCard": { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "userId": "00a2ec0a-255f-4fe7-85ad-2958dc8d3c72", "cardholderName": "John Doe", "expiryMonth": "12", "expiryYear": "27", "virtualCardLast4": "7890", "status": "ACTIVE", "cardType": "MULTI_USE", "initialAmount": 200, "usedAmount": 0, "createdAt": "2023-10-26T10:00:00Z", "authorizationSetting": { "virtualCardAsaSettingsId": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "lockDate": "1875743999999", "dailySpendLimit": null, "monthlySpendLimit": "200.00", "cardNickname": "Edited Card Nickname", "lockCardNextUse": false } } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `virtualCardId` | `UUID` | The unique identifier of the virtual card that was edited. | | `cardholderName` | `String` | The name of the cardholder associated with the virtual card. | | `expiryMonth` | `String` | The expiration month of the virtual card. | | `expiryYear` | `String` | The expiration year of the virtual card. | | `virtualCardLast4` | `String` | The last four digits of the virtual card number. | | `status` | `VirtualCardStatus` | The current status of the virtual card. | | `cardType` | `VirtualCardType` | The type of the virtual card (e.g., `MULTI_USE`). | | `initialAmount` | `Float` | The initial amount (spend limit) set on the card when it was created. | | `usedAmount` | `Float` | The total amount spent on the virtual card. | | `createdAt` | `DateTime` | The timestamp when the virtual card was originally created. | | `authorizationSetting` | `VirtualCardAuthorizationSetting` | An object containing the authorization settings for the virtual card, reflecting any updates. | | `authorizationSetting.virtualCardAsaSettingsId` | `UUID` | The unique identifier for this specific authorization setting. | | `authorizationSetting.lockDate` | `String` | The date the card will be locked, if set. | | `authorizationSetting.dailySpendLimit` | `String` | The daily spending limit, if applicable. | | `authorizationSetting.monthlySpendLimit` | `String` | The monthly spending limit, if applicable. | | `authorizationSetting.cardNickname` | `String` | The updated nickname for the card. | | `authorizationSetting.lockCardNextUse` | `Boolean` | Indicates if the card will be locked after its next use. | ## Next steps > **[Lock a virtual card](/features/lock-virtual-card)** > > Temporarily block all spend on a card without closing it. > **[Unlock a virtual card](/features/unlock-virtual-card)** > > Re-enable a locked card so authorizations succeed again. # Fund account transfers with rewards Source: https://docs.fluz.app/features/fund-account-transfers-with-rewards Pass useRewardsBalance on createTransfer and requestAccountTransfer to fund a send from rewards, or from rewards plus the sender's spend account. Set `useRewardsBalance` on the GraphQL account-to-account mutations to fund a transfer from the sender's **rewards balance**. You can combine rewards with the sender's resolved spend account in the same request. Prepayment / gift-card balance is not a field on these inputs and cannot fund the transfer. This page covers the TGS GraphQL surface only: * [`createTransfer`](/api-reference/mutations/create-transfer) (`CreateTransferInput`) * [`requestAccountTransfer`](/api-reference/mutations/request-account-transfer) (`RequestAccountTransferInput`) Destination, auth, scopes, and idempotency are unchanged from [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet). > [!NOTE] > > **Not `withdrawCashBalance`.** > > [Withdraw to an external account](/features/withdraw-to-external-account) uses `source: CASH_BALANCE | REWARDS_BALANCE` and is XOR (one source per request). Account-to-account send uses `useRewardsBalance` on `createTransfer` / `requestAccountTransfer` and can combine rewards with one spend account. *** ## Mutations and scopes | Mutation | Input | Scope | Auth | | - | - | - | - | | `createTransfer` | `CreateTransferInput` | `MAKE_PAYOUT_TRANSFER_SEND` | Bearer, or Basic Auth on a public `ACTIVE` application | | `requestAccountTransfer` | `RequestAccountTransferInput` | `REQUEST_ACCOUNT_TRANSFER` | Same auth rules as `createTransfer` | `useRewardsBalance` is optional on both inputs. Omit it or pass `false` for cash-only — the default, so existing clients do not change behavior. *** ## Input field ### `useRewardsBalance: Boolean` When `true`, fund the transfer from the sender's rewards balance **in addition to** the spend account TGS already resolves from the access token (or application operator for Basic Auth). | Value | What TGS does | | - | - | | omitted / `false` | Cash-only. Full `amount` from the sender's spend account. | | `true` | Take available cash first (up to `amount`); remainder from rewards. If available cash is `0`, the full `amount` is rewards-only. | The recipient is still credited on a spend account (`destination.userCashBalanceId`, or the automatically resolved cash balance). There is no GraphQL field to send rewards into another account's rewards balance. `CreateTransferInput` / `RequestAccountTransferInput` still have **no** `usePrepaymentBalance` (or equivalent). Do not copy FluzPay purchase flags onto these mutations. *** ## createTransfer ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440010" amount: 25.00 useRewardsBalance: true destination: { accountId: "b2c3d4e5-f6a7-8901-bcde-f23456789012" } }) { success message transferId } } ``` ```text theme={null} Authorization: Bearer ``` Same shape as a cash-only send — the only addition is `useRewardsBalance: true`. ### With an external funding source `bankCardId`, `bankAccountId`, and `paypalVaultId` remain Bearer-token only, at most one per request. They deposit into the sender's spend account first; `useRewardsBalance` still applies to the transfer itself. *** ## requestAccountTransfer Mirrors `createTransfer`. Pass `useRewardsBalance` on the request; it is included in the payload that executes after `approveApprovalRequest`. ```graphql theme={null} mutation { requestAccountTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440011" amount: 25.00 useRewardsBalance: true destination: { accountId: "b2c3d4e5-f6a7-8901-bcde-f23456789012" } }) { success messageId error { code message } } } ``` Approve and decline are unchanged: `approveApprovalRequest` / `declineApprovalRequest` with `MANAGE_APPROVALS`. See [Request Account Transfer Approval](/features/request-account-transfer-approval). *** ## Errors Failures use the standard GraphQL error envelope. `createTransfer` returns `createTransfer: null` when the send cannot complete. ```json theme={null} { "data": { "createTransfer": null }, "errors": [ { "message": "Insufficient funds", "extensions": { "code": "WDR-0001", "name": "InsufficientBalanceError", "status_code": 400 } } ] } ``` | Code | When | | - | - | | `WDR-0001` | Combined cash + rewards (or rewards alone) cannot cover `amount`. No transfer is created. | | Existing `createTransfer` errors | Destination, auth, idempotency, and single-external-funding-source rules still apply. | See [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet) for the rest of `CreateTransferInput` and [createTransfer](/api-reference/mutations/create-transfer) / [CreateTransferInput](/api-reference/types/create-transfer-input) in the API reference. # Manage linked funding sources Source: https://docs.fluz.app/features/funding-sources A **funding source** (also called a payment method) is how users pay for transactions on Fluz — gift card purchases, virtual card top-ups, wallet deposits, and more. Before any transaction can be completed, a user must have at least one funding source on file. A **bank card must always be added as a backup payment method**, even when the user intends to pay with their bank account or another method. *** ## Supported Funding Sources Fluz supports four funding source types: | Type | Add via API | Add via App / Web | Fees | Best Cashback | | - | - | - | - | - | | **Bank Account (ACH)** | ✗ | ✓ | None | ✓ Full rate | | **Bank Card** (debit/credit/prepaid) | ✓ | ✓ | Debit: –1% · Credit/PayPal: –3% | Debit > Credit | | **PayPal / Apple Pay / Google Pay** | ✗ | ✓ | –3% | Lower | | **Fluz Balance** | — | — | None | Standard | > 💡 **Bank accounts carry no processing fees and earn the highest cashback rate.** Debit cards are the next best option. Credit cards, PayPal, Apple Pay, and Google Pay all carry a processing fee that reduces the effective cashback rate. *** ## Cashback Rate by Funding Source Using a **4% merchant cashback rate** as an example on a \$200 gift card purchase: ```text theme={null} ACH / Bank Account → 4.0% → $8.00 Debit Card → 3.0% → $6.00 (–1% processing fee) Credit Card → 1.0% → $2.00 (–3% processing fee) PayPal / Apple Pay → 1.0% → $2.00 (–3% processing fee) ``` *** ## What Can Be Done via API vs. App / Web ```mermaid theme={null} flowchart LR subgraph API["🔌 API"] A1[Add Bank Card\naddBankCard mutation] A2[Update Bank Card\nnickname / MCC] A3[Delete Bank Card] A4[Read all funding sources\ngetWallet query] end subgraph Web["🌐 App / Web Portal Only"] W1[Add Bank Account\nvia Plaid Link] W2[Remove Bank Account] W3[Add PayPal] W4[Add Apple Pay / Google Pay] W5[Set primary / preferred /\nbackup payment method] end ``` > 🚧 **Bank cards are the only funding source addable via the API.** All other funding source management — bank accounts, PayPal, Apple Pay, Google Pay, and payment method preferences — must be done through the Fluz app or web portal. *** ## Bank Account Bank accounts are linked via **Plaid**, and settle via ACH. They carry no fees and earn the full merchant cashback rate. **Added via: App or web portal only.** ![Adding a bank account on the Fluz app](https://downloads.intercomcdn.com/i/o/836799999/6f3ab32e045b7011dd3bd0fb/How+to+manage+and+select+a+funding+source.gif) *To add: Menu → Funding Sources → Add New → Bank Account → follow Plaid prompts.* > 💡 All accounts under a linked bank login are connected, including savings accounts. Users should remove savings accounts after linking — most banks do not permit payments from savings. *** ## Bank Card Bank cards (debit, credit, or prepaid) can be added via the **API** or through the app and web portal. **Added via:** `addBankCard `**mutation (API) or app / web portal.** ![Adding a bank card on the Fluz app](https://downloads.intercomcdn.com/i/o/836809654/93aa3bc1b082cff44ef2a70c/How+to+manage+and+select+a+funding+source.gif) *To add on web: Menu → Accounts and Cards → Bank Cards → Add New → enter card details and billing address.* Bank cards support the following operations through the API: * **Add** — `addBankCard` (requires `MANAGE_PAYMENT` scope) * **Update nickname** — `updateBankCardNickname` * **Update preferred MCC** — `updateBankCardPreferredMerchantCategoryCode` * **Delete** — `deleteBankCard` (sets status to `INACTIVE`) > ❗️ **A bank card must be added as a backup payment method before any transaction can complete.** When a user pays via ACH, a temporary hold may be placed on the backup card for the transaction amount. If the ACH payment clears, the hold releases within 1–7 business days. If it does not clear, the backup card is charged instead. *** ## PayPal / Apple Pay / Google Pay Digital wallets can be added through the app or web portal only. They carry a –3% processing fee applied to the effective cashback rate — the same as credit cards. **Added via: App or web portal only. Not available via API.** > 💡 Some merchants restrict which payment methods they accept. A merchant may accept ACH and debit only, and block credit cards and digital wallets. This is reflected in the `blockedPaymentTypes` field returned by `getWallet`. *** ## Reading Funding Sources via API The `getWallet` query returns all funding sources linked to a user's account, their current statuses, and any blocked payment types. ```graphql theme={null} query getWallet { getWallet { bankCards { bankCardId cardType lastFourDigits cardStatus } bankAccounts { bankAccountId type status lastFour } paypalAccounts { paypalVaultId status email } blockedPaymentTypes balances { rewardsBalance { availableBalance } cashBalance { availableBalance } } } } ``` See [View Funding Sources](/features/view-funding-sources) for the full field reference. # Get card offers Source: https://docs.fluz.app/features/get-card-offers The `getVirtualCardOffers` query allows you to retrieve a list of active virtual card offers — each offer is a card program you can issue against. > [!NOTE] > > **Prerequisites:** a user access token with the `CREATE_VIRTUALCARD` scope. ## Arguments * **`input`** (`GetVirtualCardOffersInput`): Optional input to filter the virtual card offers. ## GetVirtualCardOffersInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `cardBrandLocked` | `Boolean` | Specify if virtual card offers are brand locked. | No | | `cardType` | `VirtualCardOfferType` | Specify virtual card network type (`DEBIT` or `PREPAID`). | No | | `cardNetwork` | `VirtualCardNetwork` | Specify virtual card network type (`MASTERCARD` or `VISA`). | No | ## Sample query ```graphql theme={null} query GetVirtualCardOffers { getVirtualCardOffers { offerId bin bankName rewardValue programLimits { dailyLimit weeklyLimit monthlyLimit } programName } } ``` ## cURL example ```curl theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "query { getVirtualCardOffers(input: { cardType: DEBIT, cardNetwork: MASTERCARD }) { offerId programName bin bankName programLimits { dailyLimit weeklyLimit monthlyLimit } rewardValue } }" }' ``` ## Sample response Offers are sorted by rewardValue. ```json theme={null} { "data": { "getVirtualCardOffers": [ { "offerId": "offer-id-1", "programName": "Example Card Program A", "bin": "543210", "bankName": "Bank of Examples", "programLimits": { "dailyLimit": "500.00", "weeklyLimit": "2000.00", "monthlyLimit": "5000.00" }, "rewardValue": "1.5%" }, { "offerId": "offer-id-2", "programName": "Example Card Program B", "bin": "456789", "bankName": "Another Bank", "programLimits": { "dailyLimit": "1000.00", "weeklyLimit": "4000.00", "monthlyLimit": "10000.00" }, "rewardValue": "1%" } ] } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `offerId` | `String` | The unique identifier for the virtual card offer. | | `programName` | `String` | The name of the virtual card program or merchant. | | `bin` | `String` | The Bank Identification Number (BIN) associated with the virtual card offer. | | `bankName` | `String` | The name of the issuing bank for this virtual card offer. | | `programLimits` | `ProgramLimits` | An object detailing the spending limits for this program. | | `programLimits.dailyLimit` | `String` | The maximum amount that can be spent daily on cards from this program. | | `programLimits.weeklyLimit` | `String` | The maximum amount that can be spent weekly on cards from this program. | | `programLimits.monthlyLimit` | `String` | The maximum amount that can be spent monthly on cards from this program. | | `rewardValue` | `String` | The earn rate or reward percentage offered by this virtual card program. | ## Next steps > **[Create a virtual card](/features/create-card)** > > Pass the `offerId` you picked into `createVirtualCard`. > **[Card linked offers](/features/card-linked-offers)** > > Merchant-specific offers that layer additional rewards on top of the base program. # Get spend accounts Source: https://docs.fluz.app/features/get-spend-accounts After creating a spend account, you can access it through the following queries: 1. `getUserCashBalances` - retrieves a list of all your spend accounts. 2. `getUserCashBalanceById` - retrieves a specific spend account based on it's id value. > [!WARNING] > > **Authorization required** > > This mutation requires the `LIST_PAYMENT` scope. Ensure your access token has been granted this scope before attempting to transfer funds internally. ## Spend Account details The `UserCashBalanceDetails` object will contain important information about the spend account. | Field name | Type | Description | | :- | :- | :- | | userCashBalanceId | UUID! | Unique identifier for the cash balance account | | totalCashBalance | String! | Total cash balance in the account (starts at 0) | | availableCashBalance | String! | Available cash balance for immediate use (starts at 0) | | lifetimeCashBalance | String! | Cumulative total of all funds ever deposited (starts at 0) | | nickname | String | The custom name assigned to the account | | status | UserCashBalanceStatus! | Current status of the account (ACTIVE, CLOSED) | | isDefault | Boolean! | Whether spend account is the default one | | createdAt | DateTime! | Timestamp when the account was created | | updatedAt | DateTime! | Timestamp when the account was updated | ## Retrieve the Spend Account List To obtain a list of spend accounts created on your account, use the `getUserCashBalances` query. This call returns a list of spend accounts with basic details, including the `userCashBalanceId`. You will need this ID to retrieve a specific spend account through `getuserCashBalanceById`. ### Variables | Field | Type | Required | Description | | - | - | - | - | | filter | UserCashBalanceFilterInput | No | Filtering criteria for spend accounts | | paginate | OffsetInput | No | Pagination parameters (default: limit=20, offset=0) | ### Sample Request ```graphql theme={null} query GetUserCashBalances( $filter: UserCashBalanceFilterInput, $paginate: OffsetInput, ) { getUserCashBalances( filter: $filter paginate: $paginate ) { userCashBalances { userCashBalanceId totalCashBalance availableCashBalance lifetimeCashBalance nickname status isDefault createdAt updatedAt } totalCount hasNextPage } } ``` > [!NOTE] > > Please note that due to rate-limiting, you might need to paginate and call the spend account list a few times. ### Sample Response The response will contain a list of spend accounts ```json JSON theme={null} { "data": { "getUserCashBalances": { "userCashBalances": [ { "userCashBalanceId": "c115604e-5d47-473c-a955-41a820cdcaf8", "totalCashBalance": "0.00", "availableCashBalance": "0.00", "lifetimeCashBalance": "768.00", "nickname": "Virtual Prepaid Account", "status": "ACTIVE", "isDefault": false, "createdAt": "2025-10-10T15:30:02.535Z", "updatedAt": "2026-04-22T16:07:18.618Z" }, { "userCashBalanceId": "1c1b5fcc-eb21-44c5-b678-9c41f3fa21b4", "totalCashBalance": "36.00", "availableCashBalance": "36.00", "lifetimeCashBalance": "177.00", "nickname": "austin1", "status": "ACTIVE", "isDefault": false, "createdAt": "2026-02-27T22:18:28.814Z", "updatedAt": "2026-04-22T15:43:52.031Z" }, { "userCashBalanceId": "6d1b4b19-deef-42f5-80d7-ec34804ce090", "totalCashBalance": "425108.80", "availableCashBalance": "425108.80", "lifetimeCashBalance": "444096.44", "nickname": "Main account", "status": "ACTIVE", "isDefault": true, "createdAt": "2025-08-01T19:25:54.392Z", "updatedAt": "2026-04-21T19:19:22.777Z" } ], "totalCount": 3, "hasNextPage": false } } } ``` ### Filter Options | Field | Type | Description | | :- | :- | :- | | userCashBalanceId | \[UUID!] | Filter by specific spend account IDs | | nickname | \[String!] | Filter by spend accounts nicknames | | status | \[UserCashBalanceStatus!] | Filter by spend account status | | isDefault | Boolean | Filter by whether spend account is the default one | | createdGte | DateTime | Filter by creation date (greater than or equal) | | createdLte | DateTime | Filter by creation date (less than or equal) | | updatedGte | DateTime | Filter by update date (greater than or equal) | | updatedLte | DateTime | Filter by update date (less than or equal) | **Example - Get only `ACTIVE` spend accounts** ```json theme={null} { "filter": { "status": [ACTIVE] } } ``` ### Pagination | Field | Type | Default | Max | Description | | - | - | - | - | - | | limit | Int | 20 | 20 | Number of transactions to return per page | | offset | Int | 0 | - | Number of transactions to skip | **Example - Page 1**: ```json theme={null} { "paginate": { "limit": 20, "offset": 0 } } ``` **Example - Page 2**: ```json theme={null} { "paginate": { "limit": 20, "offset": 20, } } ``` ## Retrieve a specific Spend Account To obtain a specific spend account created on your account, use the `getUserCashBalanceById` query. This call returns this spend account's details, including the `userCashBalanceId` & it's balances. ### Variables | Field | Type | Required | Description | | - | - | - | - | | userCashBalanceId | UUID! | Yes | Spend account's ID | ### Sample Request ```graphql theme={null} query GetUserCashBalanceById( $userCashBalanceId: UUID! ) { getUserCashBalanceById( userCashBalanceId: $userCashBalanceId ) { userCashBalanceId totalCashBalance availableCashBalance lifetimeCashBalance nickname status isDefault createdAt updatedAt } } ``` ### Sample Response The response will contain a specific spend account's details ```json JSON theme={null} { "data": { "getUserCashBalanceById": { "userCashBalanceId": "6d1b4b19-deef-42f5-80d7-ec34804ce090", "totalCashBalance": "425108.80", "availableCashBalance": "425108.80", "lifetimeCashBalance": "444096.44", "nickname": "Main account", "status": "ACTIVE", "isDefault": true, "createdAt": "2025-08-01T19:25:54.392Z", "updatedAt": "2026-04-21T19:19:22.777Z" } } } ``` # Getting bank account transaction data Source: https://docs.fluz.app/features/getting-bank-data Read historical bank transactions for a linked Plaid account. These transactions are ingested and maintained by identity-service; TGS exposes them read-only. ## Auth Call `/api/v1/graphql` with a Fluz user Bearer access token that includes `MANAGE_PAYMENT`. Basic auth is not allowed on these fields. ```http theme={null} Authorization: Bearer ``` ## Get transactions ```graphql theme={null} query GetPlaidBankTransactions($input: PlaidBankTransactionFilterInput) { getPlaidBankTransactions(input: $input) { totalCount transactions { historicalBankTransactionId platformItemId bankInstitutionAuthId bankAccountId transactionId transactionStatus transactionType transactionDate amount currencyCode description merchantName category pending } } } ``` ```json theme={null} { "input": { "bankAccountId": "bank-account-id", "startDate": "2026-01-01T00:00:00.000Z", "endDate": "2026-06-04T23:59:59.999Z", "paginate": { "limit": 50, "offset": 0 } } } ``` **Notes** * Filter by `bankAccountId` and a `startDate`/`endDate` window; page through results with `paginate.limit` and `paginate.offset`. * `totalCount` reflects the total matching the filter, for building pagination. * `pending: true` marks transactions that have not fully settled yet — the same recent, unsettled outflows that reduce a user's available spend power (see *Managing Bank Account Spend Power*). # Manage linked bank accounts Source: https://docs.fluz.app/features/link-external-bank-accounts A **bank account** is the lowest-cost, highest-reward funding source on Fluz. Bank accounts are linked through **Plaid** and settle via **ACH** — they carry no processing fees and earn the full merchant cashback rate. Bank account linking and management is exposed through a public GraphQL wrapper. Identity-service remains the system of record for Plaid tokens, account ingestion, balances, historical transactions, and Plaid webhooks. > [!NOTE] > > **Bank accounts carry no processing fees and earn the highest cashback rate.** > > On a 4% merchant offer, an ACH payment returns the full 4%, while a debit card returns 3% and a credit card 1% after processing fees. Where a user has the choice, a linked bank account is always the best-earning option. *** ## What You Can Do Everything below runs through the Plaid integration. See [Linking Bank Account via Plaid](/features/link-via-plaid) for full request and response details. ### Linking | Capability | Operation | Description | | - | - | - | | Create a Link token | `createPlaidLinkToken` | Generate the token used to open Plaid Link. | | Complete a new link | `completePlaidLink` | Exchange Plaid's `public_token` and ingest the connected accounts. | | Repair a broken connection | `createPlaidLinkToken` + `completePlaidLink` (with `platformItemId`) | Relink a previously connected institution that has disconnected. | | Attach a required address | `createPlaidLinkAddress` | Add an address when `completePlaidLink` returns `requiresAddress: true`. | ### Balances & Spend Power | Capability | Operation | Description | | - | - | - | | List linked accounts | `getPlaidBankAccounts` | Return the safe metadata for all connected bank accounts. | | Read stored balances | `getPlaidBankBalances` · `getPlaidBankAccountBalance` | Get the latest stored balance across accounts or for one account. | | Read spend power | `getPlaidBankAccountSpendPower` | Return available spend power, last recorded balance, and pending activity. | | Refresh one balance | `refreshPlaidBankAccountBalance` | Request a rate-limited realtime balance refresh for one account. | | Refresh all connections | `refreshPlaidBankConnections` | Request a cached refresh across all connected Plaid data. | ### Transactions | Capability | Operation | Description | | - | - | - | | Read transaction history | `getPlaidBankTransactions` | Return historical, paginated bank transactions with merchant and category detail. | ### Management | Capability | Operation | Description | | - | - | - | | Remove an institution | `removePlaidBankInstitution` | Disconnect a linked bank institution and its accounts. | *** ## Requirements All bank account operations require a Fluz user access token that includes the `MANAGE_PAYMENT` scope. **Bearer auth only.** The Plaid fields do not accept Basic auth. Obtain a user access token through the standard authentication flow first, then call the Plaid fields with `Authorization: Bearer `. *** ## Balance Refresh Limits Balance refreshes are cost-controlled per Plaid institution: * **One** realtime refresh per hour * **Six** realtime refreshes per day Calls made inside the limit window return the latest stored balance instead of triggering a new realtime request. *** ## Savings Accounts > [!NOTE] > > **Linking a login connects every account under it, including savings.** > > Most banks do not permit payments from savings accounts. After linking, users should remove any savings accounts they don't intend to pay from to avoid failed transactions. *** ## Unsupported Flows Micro-deposit linking is intentionally unsupported. New links and relinks must use the standard Plaid Link flow, which supports balances and historical transactions. *** ## Security We do not store Plaid access tokens, Plaid public tokens, identity-match details, bank account numbers, or routing numbers. Only safe account metadata — such as institution name, last four digits, type, and user-entered labels — is exposed through the API. *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Manage linked bank cards Source: https://docs.fluz.app/features/link-external-cards A **bank card** — debit, credit, or prepaid — is the most flexible funding source on Fluz and the only one that can be added programmatically. Bank cards are used to pay for gift card purchases, fund virtual card transactions, deposit to your Fluz balance, and serve as the required backup for other payment methods. Bank cards can be added and managed through the **API** or through the Fluz app and web portal. > [!NOTE] > > **A bank card must be added as a backup payment method before any transaction can complete.** > > When a user pays via ACH, a temporary hold may be placed on the backup card for the transaction amount. If the ACH payment clears, the hold releases within 1–7 business days. If it does not clear, the backup card is charged instead. *** ## What You Can Do | Action | Mutation | Description | | - | - | - | | [Add Bank Card](/features/add-bank-card) | `addBankCard` | Add a debit, credit, or prepaid card to a user's account with a billing address. | | [Update Bank Card](/features/add-bank-card) | `updateBankCardNickname` · `updateBankCardPreferredMerchantCategoryCode` | Rename a card or set its preferred merchant category code (MCC). | | [Delete Bank Card](/features/delete-bank-card) | `deleteBankCard` | Remove a card by setting its status to `INACTIVE`. | All bank card mutations require the `MANAGE_PAYMENT` scope. *** ## Fees & CashbackCard type affects the effective cashback rate a user earns on a purchase: | Card Type | Processing Fee | Relative Cashback | | - | - | - | | **Debit** | –1% | Higher | | **Credit** | –3% | Lower | | **Prepaid** | Varies by card | Varies | > [!NOTE] > > **Debit cards earn more cashback than credit cards.** > > Bank accounts (ACH) carry no processing fees and earn the full merchant cashback rate. Where a user has the choice, ACH earns the most, debit is next, and credit carries the highest fee. See [Bank Accounts](/features/link-external-bank-accounts) for the fee-free option. *** ## Requirements**ope.** Every bank card mutation requires the `MANAGE_PAYMENT` scope on the user access token. **Billing address.** Each card must be associated with an address. Provide either a new `billingAddress` object or an existing `userAddressId` — not both. Use the `getUserAddresses` query to look up a stored `userAddressId`. > [!NOTE] > > **Only personal applications can add bank cards via the API.** > > Publicly available applications that want to add bank cards will need to complete PCI certification and an attestation of PCI compliance. If your application is not PCI-certified, direct users to add bank cards through the Fluz app or web portal. *** > Setting a Primary, Preferred, or Backup Card, updating, and deleting cards is available via the API. **Choosing which funding source is primary, preferred, or backup is managed in the Fluz dashboard**, not through the API. *** ## Reading Bank Cardse `getWallet` query to return all bank cards on a user's account, along with their type, last four digits, and current status. ```graphql theme={null} query getWallet { getWallet { bankCards { bankCardId cardType lastFourDigits cardStatus } } } ``` See [View Funding Sources](/features/view-funding-sources) for the full field reference. *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Linking bank account via Plaid Source: https://docs.fluz.app/features/link-via-plaid 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: ```http theme={null} Authorization: Bearer ``` ## The link flow at a glance 1. Create a Link token (`createPlaidLinkToken`). 2. Open Plaid Link with that token. 3. On Plaid's `onSuccess`, send the returned `public_token` back to TGS (`completePlaidLink`). 4. Store the `platformItemId` from the completion response — it's the safe public identifier you'll use later to relink. ### 1. Create a Link token ```graphql theme={null} mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) { createPlaidLinkToken(input: $input) { linkToken expiration requestId mode } } ``` ```json theme={null} { "input": {} } ``` For **native** Link, pass `deviceOs` as `IOS` or `ANDROID` so identity-service includes the correct Plaid OAuth redirect option. ```json theme={null} { "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. ```graphql theme={null} mutation CompletePlaidLink($input: CompletePlaidLinkInput!) { completePlaidLink(input: $input) { requiresAddress bankAccountId bankInstitutionAuthId newlyLinkedBankInstitutionAuthId bankInstitutionName platformItemId bankAccounts { bankInstitutionAuthId bankAccountId bankName lastFour type subtype } } } ``` ```json theme={null} { "input": { "publicToken": "public-sandbox-..." } } ``` ### 4. Store the `platformItemId` Save `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. ```html theme={null} ``` 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*). ```javascript theme={null} const FLUZ_GRAPHQL_URL = '/api/v1/graphql'; const fluzUserAccessToken = ''; 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. ```graphql theme={null} query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) { getPlaidBankAccounts(input: $input) { platformItemId bankInstitutionAuthId bankInstitutionName bankAccountId accountName lastFour type subtype status } } ``` ```json theme={null} { "input": { "platformItemId": "plaid-item-id" } } ``` ## Attach an address If `completePlaidLink` returned `requiresAddress: true`, attach an address to the linked bank institution. ```graphql theme={null} mutation CreatePlaidLinkAddress($input: CreatePlaidLinkAddressInput!) { createPlaidLinkAddress(input: $input) { addressId } } ``` ```json theme={null} { "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. ```graphql theme={null} mutation RemovePlaidBankInstitution($input: RemovePlaidBankInstitutionInput!) { removePlaidBankInstitution(input: $input) { removed bankInstitutionAuthId } } ``` ```json theme={null} { "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. # Lock virtual card Source: https://docs.fluz.app/features/lock-virtual-card The `lockVirtualCard` mutation allows you to lock an existing virtual card, preventing further transactions. > [!NOTE] > > **Prerequisites:** a user access token with the `EDIT_VIRTUALCARD` scope, and the `virtualCardId` of the card to lock. ## Arguments * **`input`** (`LockVirtualCardInput!`): The input object containing the ID of the virtual card to lock. ## LockVirtualCardInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `virtualCardId` | `UUID!` | The virtual card to lock. | Yes | ## Sample mutation ```graphql theme={null} mutation { lockVirtualCard(input: { virtualCardId: "07df5653-43a8-4532-9881-3ab5857bbe11" }) { virtualCardId locked } } ``` ## cURL example ```curl theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation { lockVirtualCard(input: { virtualCardId: \"07df5653-43a8-4532-9881-3ab5857bbe11\" }) { virtualCardId locked } }" }' ``` ## Sample response ```json theme={null} { "data": { "lockVirtualCard": { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "locked": true } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `virtualCardId` | `UUID` | The unique identifier of the virtual card that was targeted for locking. | | `locked` | `Boolean` | `true` if the card was successfully locked, `false` otherwise. | > [!TIP] > > To lock a card automatically after its first successful authorization, set `lockCardNextUse: true` at creation instead — see [Create Virtual Card](/features/create-card). ## Next steps > **[Unlock a virtual card](/features/unlock-virtual-card)** > > Re-enable a locked card so authorizations succeed again. # Managing spend power Source: https://docs.fluz.app/features/manage-spend-power Once a bank account is linked (see *Linking a Plaid Bank Account*), you can read its balance and spend power and, when you need a fresher reading, trigger a realtime balance refresh. ## What spend power means **Spend power** is how much a user can currently spend from a linked bank account. It is based on the account's most recent known balance, reduced by recent ACH outflows that have not fully settled yet. When a user spends from the account, those amounts are held for roughly three business days before they clear and stop counting against spend power. (The exact clearing window is set per bank and can be shorter.) Fluz keeps the stored balance for each linked account up to date automatically. When you need a more current reading — for example, right after a user moves money in or out of their bank — request a **realtime refresh** (rate limited; see below). Refreshing the balance updates the balance-derived spend-power figure. ## Auth Call `/api/v1/graphql` with a Fluz user Bearer access token that includes `MANAGE_PAYMENT`. Basic auth is not allowed on these fields. ```http theme={null} Authorization: Bearer ``` ## Get spend power for one bank account ```graphql theme={null} query GetPlaidBankAccountSpendPower($input: PlaidBankAccountInput!) { getPlaidBankAccountSpendPower(input: $input) { bankAccountId spendPower availableSpendPower lastRecordedBalance pendingTransactions updatedAt } } ``` ```json theme={null} { "input": { "bankAccountId": "bank-account-id" } } ``` **Response fields** | Field | Description | | - | - | | `lastRecordedBalance` | The most recent known bank balance for the account. | | `pendingTransactions` | Recent ACH outflows (debits from roughly the last three business days) that have not yet settled and are still being held against the balance. | | `availableSpendPower` | What the user can spend right now, derived from the latest balance less pending. Moves immediately after a realtime balance refresh. | | `spendPower` | The account's decisioned spend-power figure. May update on a slower cadence than the balance, so it can briefly differ from `availableSpendPower` after a refresh. | | `updatedAt` | When the figure was last computed. | ## Get the latest stored balance For one bank account: ```graphql theme={null} query GetPlaidBankAccountBalance($input: PlaidBankAccountInput!) { getPlaidBankAccountBalance(input: $input) { platformItemId bankInstitutionAuthId bankAccountId amount current source trigger startedAt } } ``` ```json theme={null} { "input": { "bankAccountId": "bank-account-id" } } ``` For all of the user's connected accounts: ```graphql theme={null} query GetPlaidBankBalances($input: PlaidBankBalanceFilterInput) { getPlaidBankBalances(input: $input) { platformItemId bankInstitutionAuthId bankAccountId amount current source trigger startedAt } } ``` ## Refresh one account in realtime Request a rate-limited realtime balance refresh for a single bank account. This pulls a fresh balance from Plaid and updates the balance-derived spend power. ```graphql theme={null} mutation RefreshPlaidBankAccountBalance($input: PlaidBankAccountInput!) { refreshPlaidBankAccountBalance(input: $input) { status balances { bankAccountId amount current source trigger startedAt } } } ``` ```json theme={null} { "input": { "bankAccountId": "bank-account-id" } } ``` ### Refresh rate limit To control cost, realtime refreshes are rate limited **per Plaid institution**: one realtime refresh per hour and six realtime refreshes per day. A refresh requested inside a limit window does not call Plaid again — it returns the latest stored balance instead, so the call still succeeds and returns data. Design your UI so that a returned balance is not always assumed to be a brand-new realtime reading. ## Refresh all connections (cached) Request a cached refresh of all connected Plaid bank data for the authenticated account. ```graphql theme={null} mutation RefreshPlaidBankConnections { refreshPlaidBankConnections { status balances { bankAccountId amount current } verifyMembers { platformItemId bankInstitutionAuthId } } } ``` Use `refreshPlaidBankConnections` when you want identity-service to perform its cached all-connection refresh. > If `verifyMembers` is returned, one or more connections need repair. Use the `platformItemId` to start the relink flow — see **Relinking Disconnected Bank Accounts**. Note that the single-account `refreshPlaidBankAccountBalance` mutation does **not** return this signal, so use `refreshPlaidBankConnections` when you need to detect disconnected connections. # Overview Source: https://docs.fluz.app/features/move-funds-with-external-accounts Move money between a user's **Fluz wallet** and their **external accounts** — a bank account, bank card, or digital wallet (PayPal, Venmo). Deposits bring money *into* the Fluz wallet from an external source to fund purchases; withdrawals send money *out* of the Fluz wallet to a linked external destination. This differs from moving money *within* Fluz: to shift funds between a user's own spend accounts use [Transfer Funds Between Spend Accounts](/features/transfer-between-spend-accounts), and to send funds to a different Fluz account use [Account to Account Transfers](/features/account-to-account-transfers). | Direction | Page | What it does | | - | - | - | | **In** | [Deposit Funds From External Accounts](/features/deposit-from-external-accounts) | Add money to a Fluz balance from a linked funding source. | | **In** | [Redeem Fluz Gift Card to Gift Card Balance](/features/redeem-fluz-gift-card) | Credit a Fluz Gift Card code to the gift card balance. | | **Out** | [Withdraw Funds To External Account](/features/withdraw-to-external-account) | Send money from a Fluz balance to a linked external account. | *** ## Fluz Balances Deposits and withdrawals interact with a few distinct balances. Knowing which balance a movement touches is important, because not every balance can be withdrawn. | Balance | Funded by | Withdrawable | | - | - | - | | **Cash balance** | Deposits from a funding source | ✓ | | **Rewards balance** | Cashback earned on purchases | ✓ | | **Gift card balance** | Deposits to `GIFT_CARD_BALANCE` and redeemed Fluz Gift Cards | ✗ (spend only) | | **Reserve balance** | Deposits to `RESERVE_BALANCE` | ✗ (held in reserve) | > [!NOTE] > > **The gift card balance is spend-only.** > > Funds credited to the gift card balance — whether by depositing to `GIFT_CARD_BALANCE` or redeeming a Fluz Gift Card — can be used for purchases but cannot be withdrawn to an external account. *** ## Depositing Funds Use the `depositCashBalance` mutation to move money into Fluz from a linked funding source. You choose the **destination balance** (`CASH_BALANCE`, `GIFT_CARD_BALANCE`, or `RESERVE_BALANCE`) and the **funding source** — a `bankAccountId`, `bankCardId`, or `paypalVaultId` retrieved from `getWallet`. When depositing to a cash balance, you can target a specific spend account with `userCashBalanceId`. Deposits may settle instantly or within 2–5 business days depending on the funding source. Requires the `MAKE_DEPOSIT` scope. Deposits funded by a **debit card** into `CASH_BALANCE` may be processed as account funding transactions (AFT), which carry their own fee rate. Fluz applies this automatically where it is available, so read `depositFee` from the response rather than computing it — see [Deposit Funds From External Accounts](/features/deposit-from-external-accounts). See [Deposit Funds From External Accounts](/features/deposit-from-external-accounts) for the full input and response reference. *** ## Redeeming a Fluz Gift Card Fluz Gift Card is a special kind of deposit. The `redeemFluzGiftCard` mutation credits a gift card **code** directly to the user's gift card balance (`giftCardCashBalance`). Redemptions are instant, and any activation fee is reflected in `depositFee`. Requires the `MAKE_DEPOSIT` scope. See [Redeem Fluz Gift Card to Gift Card Balance](/features/redeem-fluz-gift-card) for details. *** ## Withdrawing Funds Use the `withdrawCashBalance` mutation to send money out of Fluz to a linked external account. You choose the **source balance** (`CASH_BALANCE` or `REWARDS_BALANCE`) and a **withdrawal method**, providing the matching destination ID: | Method | Required field | Notes | | - | - | - | | `BANK_ACH` | `bankAccountId` | ACH transfer to a linked bank account. Typically settles in 1–3 business days. No fees. | | `BANK_CARD` | `bankCardId` | Push-to-card transfer to an OCT-eligible linked debit card. Set `isExpedited: true` for immediate delivery. May have fees. | | `PAYPAL` | `paypalVaultId` | Transfer to a linked PayPal account. May have fees. | | `VENMO` | `venmoAccountId` | Transfer to a linked Venmo account. May have fees. | When withdrawing from a cash balance, specify which spend account to draw from with `cashBalanceId`. Withdrawals require the `MAKE_WITHDRAWAL` scope. Fees are deducted from the withdrawal, so the destination receives less than `amount`. Quote the exact fee, net amount, and settlement timing first with [`getWithdrawFeeEstimate`](/api-reference/queries/get-withdraw-fee-estimate). See [Withdraw Funds To External Account](/features/withdraw-to-external-account) for the full input reference, error handling, and multi-seat behavior. *** ## Idempotency Deposit, redemption, and withdrawal must include a unique, client-generated `idempotencyKey`. If the same key is submitted more than once, the API returns the result of the original request instead of processing it again — preventing duplicate money movement. Always generate a fresh UUID per request. *** ## Requirements at a Glance | tion | Mutation | Scope | | - | - | - | | Deposit funds | `depositCashBalance` | `MAKE_DEPOSIT` | | Redeem a Fluz Gift Card | `redeemFluzGiftCard` | `MAKE_DEPOSIT` | | Withdraw funds | `withdrawCashBalance` | `MAKE_WITHDRAWAL` | Before moving funds, use [Check Account Balance](/check-account-balance) or `getWallet` to confirm the user has sufficient available balance and to retrieve the relevant funding source IDs. *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Delivery methods Source: https://docs.fluz.app/features/open-loop-cards/open-loop-card-delivery-methods Four ways to get an open-loop card into a recipient's hands — and how much you need to know about them to do it. > [!NOTE] > > **The short version: you do not need to know anything about your recipient to generate a card.** > > Recipient contact details are only required when you want *Fluz* to do the delivering. If you deliver the link yourself, you send us a card limit, an offer, and a funding account — nothing else. The recipient fills in their own details on the hosted page. ## Pick your method **1. Do you want Fluz to deliver the link, or will you?** If you deliver it yourself — through your own email, your own SMS, in-app, in a portal, or by handing the URL to a downstream client — use **`GENERATE_URL`**. No recipient data required. **2. If Fluz delivers, how?** Email (`EMAIL`) or SMS (`PHONE_NUMBER`). You supply one address or number per card. **3. Do you already hold the recipient's full identity details?** If you do — and you want the recipient to skip data entry entirely — ask your Fluz rep about **pre-filled enrollment**. This is a gated option, not part of the standard `generateVCShareLinks` flow. ## The four options at a glance | # | Option | `shareMethod` | What you send Fluz | Who delivers the link | What the recipient enters | | - | - | - | - | - | - | | 1 | **Generate a link** | `GENERATE_URL` | Nothing about the recipient | **You** | Their own details | | 2 | **Fluz emails it** | `EMAIL` | One email address per card | Fluz | Their own details | | 3 | **Fluz texts it** | `PHONE_NUMBER` | One phone number per card | Fluz | Their own details | | 4 | **Pre-filled enrollment** | Gated — contact your rep | Full recipient identity details | You or Fluz | Nothing — they just claim | > [!NOTE] > > Options 1–3 are three values of the same `shareMethod` field on a single mutation. Switching between them is a one-line change — you are not integrating three different APIs. ## Option 1 — Generate a link, you deliver it **Most partners want this one.** You call the API, you get back an array of URLs, you do whatever you want with them: email them from your own system, text them, drop them into a customer portal, or hand them to a downstream client who distributes them to their own end users. Fluz sends **nothing** to anyone. We have no recipient contact details on file for these links, because you never gave us any. ```json Generate URLs theme={null} { "input": { "cardLimit": 100, "offerId": "7c4a1d92-3fb8-4e05-9a61-2d8ef50b7c33", "quantity": 1, "daysUntilExpiration": 30, "shareMethod": "GENERATE_URL", "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` ```json Response theme={null} { "data": { "generateVCShareLinks": { "shareLinks": [ "https://fluz.app/virtual-prepaid-card/3f8a...c1" ] } } } ``` > [!WARNING] > > With `GENERATE_URL`, both `recipientListEmail` and `recipientListPhone` must be **empty or omitted**. Sending a recipient list alongside `GENERATE_URL` is a validation error and creates no records. > [!TIP] > > Set `quantity` above 1 to mint a batch in a single call. You get one distinct URL per unit, and each URL is claimable exactly once. Distribute the URLs exactly as returned — do not rewrite or re-shorten them. ## Option 2 — Fluz emails the link You supply one email address per card and Fluz sends the email. The recipient clicks through to the same hosted page as in Option 1 and enters their own details there. Use this when you already hold recipient emails and would rather not build delivery yourself. ```json Email theme={null} { "input": { "cardLimit": 100, "offerId": "7c4a1d92-3fb8-4e05-9a61-2d8ef50b7c33", "quantity": 2, "daysUntilExpiration": 30, "shareMethod": "EMAIL", "recipientListEmail": ["mike.bennett@example.com", "dana.ruiz@example.com"], "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` > [!WARNING] > > `recipientListEmail` length **must equal** `quantity`. A mismatch returns a validation error and creates no records — no partial batches. ## Option 3 — Fluz texts the link Same as Option 2, over SMS. You supply one phone number per card in E.164 format. Fluz sends the SMS; Fluz does **not** also send an email on this path. ```json SMS theme={null} { "input": { "cardLimit": 100, "offerId": "7c4a1d92-3fb8-4e05-9a61-2d8ef50b7c33", "quantity": 2, "daysUntilExpiration": 30, "shareMethod": "PHONE_NUMBER", "recipientListPhone": ["+12125550101", "+12125550102"], "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` > [!WARNING] > > `recipientListPhone` length must equal `quantity`, and both lists are mutually exclusive — send the one that matches your `shareMethod` and leave the other empty. ## Option 4 — Pre-filled enrollment Some partners want a white-glove experience where the recipient enters nothing at all. In that model you pass Fluz the identity details the card enrollment needs, and the recipient's only action is opening the link and claiming the card. This is a **gated option** and is not part of the standard `generateVCShareLinks` input. If your program needs it, talk to your Fluz account team — it carries additional data-handling and compliance obligations on your side, since you are supplying personal information on behalf of a person who has not interacted with Fluz yet. > [!NOTE] > > Choose this only if you genuinely hold verified recipient identity data. If you are tempted toward Option 4 simply to avoid asking recipients for information, Option 1 is almost certainly what you want instead. ## What the recipient does Identical across Options 1–3. The link is the same hosted page no matter who delivered it: **1. Open the link** No app download, no Fluz account, no password. **2. Verify by phone** A one-time code confirms the person holding the link. **3. Enter their card details** The recipient supplies the details the card needs. No PIN prompt occurs at this stage. **4. Claim, reveal, and spend** The card is funded from your spend account **at claim time**, not when the link was generated. The recipient becomes an authorized user of that one card object — nothing else on your account. The card is not auto-revealed on claim: revealing it prompts the recipient to enter their PIN, or create one if they haven't set one yet. See [Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience) for the full walkthrough and the states a recipient sees when a link is expired, revoked, or already claimed. ## Common points of confusion > **Do we have to pre-populate recipient information?** > > No. That is a common misreading of the API reference. Recipient fields exist so that **Fluz can deliver on your behalf** — they are not inputs to card issuance. With `GENERATE_URL` you send no recipient data at all. > **Does an email address have to be in every request?** > > No. `recipientListEmail` is required **only** when `shareMethod = EMAIL`. With `GENERATE_URL` and `PHONE_NUMBER` it must be empty. > **We pass the link to our client, who passes it to their end user. Does that work?** > > Yes — that is exactly the `GENERATE_URL` pattern. The URL is bearer-style: whoever opens it first and completes verification claims the card. Treat links as sensitive and deliver them over a channel you trust. > **Can I generate a test link from the portal?** > > Not today. Link generation is API-only. Test against the staging environment with a staging token carrying the `CREATE_SHARE_LINK` scope — see [Staging vs. Live Environment](/docs/staging-vs-live-environment). > **Can I switch methods later?** > > Yes. `shareMethod` is set per call, not per account. Nothing stops you from generating URLs for one batch and having Fluz email the next. ## Requirements common to all methods | Requirement | Detail | | - | - | | Auth | Bearer access token with the `CREATE_SHARE_LINK` scope. Basic auth is rejected. | | Funding | `userCashBalanceId` — a spend account on your own account. Effectively required despite being marked optional in the schema. | | Offer | `offerId` must be an active offer whose merchant is shareable. | | Expiration | `daysUntilExpiration` defaults to 30. This date is also the card's freeze date. | ## Next steps > **[Open Loop Cards Overview](/features/open-loop-cards/send-open-loop-cards)** > > Full operation reference for generating, listing, and deactivating links. > **[Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience)** > > What your recipients see, and the rules that govern their card. # Recipient experience Source: https://docs.fluz.app/features/open-loop-cards/open-loop-cards-recipient-experience What a recipient sees when they open a hosted virtual card link — sign-in, two-factor verification, PIN, claim, and the program rules that govern how the card can be spent. Every hosted link generated with [`generateVCShareLinks`](/features/open-loop-cards/send-open-loop-cards#generatevcsharelinks) resolves to a Fluz-hosted activation page at `https://fluz.app/virtual-prepaid-card/{share_request_id}`. The recipient claims their card there — no app download, no password. The recipient becomes an authorized user of that virtual card object only. They do not gain access to your account, your balances, or any other cards. See it in action: [desktop flow](https://test.fluz.app/wp-content/uploads/2026/04/Web-Share-V6.mp4) · [mobile flow](https://test.fluz.app/wp-content/uploads/2026/04/Mob-Share-F.mp4). [Demo open loop cards](https://demos.fluz.app/card-issuing/?launch-card=1) ## Claim flow **1. Landing & sign-in** The recipient sees the activation page branded with the sender's business. They sign in through the Fluz auth portal (new recipients onboard here). **2. Two-factor authentication** On initial load, existing users are taken to the 2FA screen. 2FA is required before the card can be viewed or claimed. **3. Billing address (if needed)** If the recipient has no billing address on file, they're prompted to add one. *Billing address is required for online purchases.* **4. Card issued & claimed** A single-load virtual card is created and assigned to the recipient, funded from the sender's account, with a lock date equal to the link's expiration date. No PIN prompt occurs during activation. **5. Reveal & PIN** The card is not auto-revealed on claim. When the recipient chooses to reveal card details, they're prompted to enter their PIN — or create one, if they haven't set one yet. **6. Use the card** Once revealed, the recipient can see card details, transactions, spend online, and (where supported) add the card to Apple Pay or Google Pay in one tap. > [!NOTE] > > **Already claimed?** If the same user opens a link they already claimed, they land on their card and are prompted for their PIN to reveal it. If a *different* user opens a link that someone else already claimed, they're shown an access-denied state after 2FA. > [!NOTE] > > **Funding timing.** The card limit is drawn against the sender's spend account **at claim time**, not when the link is generated. ## What the recipient sees for expiration The link's expiration date — set by the sender via `daysUntilExpiration`, defaulting to 30 days — is surfaced to the recipient, typically as a "Valid until" date. * Before claim, that date is the **last day the link can be claimed**. * After claim, that date is the card's **freeze / lock date** (end of day). After it, the card can no longer be spent. * The printed **card expiry** is aligned to the end of that month (e.g., a freeze date of 6/15/2026 yields a card expiry of 6/30/2026). ## Recipient-facing link errors | Condition | What the recipient sees | | - | - | | Link expired before being claimed | "Expired before issued" — link can no longer be claimed. | | Sender deactivated the link early | "Frozen by sender" — link was revoked before expiry. | | Card was claimed, then its lock date passed | "Expired after issued" — card is frozen. | | A different user opens an already-claimed link | "Access denied". | ## Program rules to communicate to recipients These are program-level rules for hosted (open-loop) virtual cards. Confirm the exact values for **your** program with your Fluz representative — several are partner-negotiated. | Rule | Default | Notes | | - | - | - | | Daily account spend limit | **\$250,000/day** | Some partners have a custom limit. | | Restaurant transaction buffer | **25%** | A 25% buffer applies to all programs (to accommodate tips/holds at restaurants). | | Restricted merchants / categories | Program-specific | Certain merchant categories are restricted. | | Funding | Sender's spend account | Cards are funded from the sender's account at claim time. | **Customer support for recipients:** 1-888-360-6660 · [humans@fluz.app](mailto:humans@fluz.app) > [!NOTE] > > The full partner-facing reference (terminology, recipient walkthrough with screenshots, funding instructions, restricted categories, and support) lives in the **Partner Guide — Hosted URL Virtual Cards**. Ask your Fluz contact for the latest copy for your program. ## Next steps > **[Send open-loop cards](/features/open-loop-cards/send-open-loop-cards)** > > Generate, list, and deactivate hosted virtual card links from the API. > **[Create a bulk order](/features/create-bulk-order)** > > Issue many cards at once for programmatic distribution. # Register & send Source: https://docs.fluz.app/features/open-loop-cards/register-and-send Register a recipient's identity and billing address up front with registerUser, then generate a hosted virtual card link bound to that known user with generateVCShareLinks (shareMethod: EXISTING_USER). Unlike the standard flow, the card is created at link-generation time, not at claim. The [standard hosted-link flow](/features/open-loop-cards/send-open-loop-cards) defers everything to the recipient: you mint a link, and Fluz doesn't create the virtual card until the recipient opens it, verifies themselves, and claims it. This flow inverts that for recipients you've already identified. You register the recipient's identity and billing address yourself with `registerUser`, then generate a share link with `generateVCShareLinks` using `shareMethod: EXISTING_USER`. Fluz creates the virtual card **immediately**, at generation time — not at claim — and binds it to that one recipient. The link is still delivered and claimed the normal way; only card creation moves earlier. Funding is still drawn at claim time, same as the standard flow — from `userCashBalanceId`, falling back to your prepayment or rewards balance if enabled and the spend account runs short. > [!NOTE] > > **When to use this instead of a plain share link** > > * You already know exactly who the recipient is (by user ID) and want the card created and ready before you notify them, rather than waiting on them to claim it. > * You want a hard guarantee that only the intended recipient can ever view the link — not "first person to click it." > * You're sending to a batch of known recipients and want deterministic 1:1 mapping between recipient and card. ## Before you start You'll need a Bearer access token. Basic auth is not accepted for either operation. | Operation | Scope | Also required | | - | - | - | | `registerUser` | — | Registration permission enabled on your application | | `generateVCShareLinks` | `CREATE_SHARE_LINK` | An active virtual card offer, a spend account to fund from (optionally with prepayment/rewards balance as fallback) | > [!WARNING] > > **User registration is not enabled by default.** `registerUser` is a restricted mutation — your application must be explicitly approved by Fluz before it can create users. **Contact your Fluz sales rep or account manager to have it enabled.** Calls from an unapproved application fail with `AUTH-0022`. > > If the recipient already has a Fluz account, you can skip registration and go straight to `generateVCShareLinks` with their existing `recipientUserIds`. See [Authentication](/concepts/authentication) for how to mint a scoped token. [Demo open loop cards](https://demos.fluz.app/card-issuing/?launch-card=1) ## How this differs from the standard flow | | Standard (`GENERATE_URL` / `EMAIL` / `PHONE_NUMBER`) | This flow (`EXISTING_USER`) | | - | - | - | | Recipient identified by | Email / phone in `recipientListEmail` / `recipientListPhone` | Known Fluz `userId` in `recipientUserIds` | | Card created | At claim, when the recipient completes onboarding | At `generateVCShareLinks` call time | | Card funded | At claim time | At claim time (same as standard) | | Who can claim the link | Whoever opens it first, if unclaimed | Only the assigned recipient — enforced from the moment the link is created | | Recipient onboarding at claim | Sign-in, 2FA, billing address (if needed) | Sign-in, 2FA — no billing address prompt, since it was collected at registration | | Revealing the card | Prompts for PIN (or PIN creation, if none set yet) | Prompts for PIN (or PIN creation, if none set yet) | See [Send Open Loop Cards](/features/open-loop-cards/send-open-loop-cards) for the standard flow and [Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience) for the full claim walkthrough. ## Notes and limitations * **`recipientUserIds` must reference existing Fluz users.** If the recipient isn't registered yet, register them first with `registerUser` (this flow), or use the standard hosted-link flow and let Fluz onboard them at claim time. * **`recipientUserIds` length must equal `quantity`.** A mismatch returns a validation error and creates no records. * **Card creation moves to generation time — funding does not.** The virtual card object and recipient binding are created when you call `generateVCShareLinks`, but funds are still drawn at claim time, same as the standard flow: from `userCashBalanceId` first, then your prepayment or rewards balance as a fallback if `usePrepaymentBalance` / `useRewardsBalance` are set and the spend account is insufficient. * **Registration is per-application and per-environment.** Permission granted for staging does not carry to production. See [Deploying to Production](/deploying-to-production). * **Never register real people in staging.** See [Staging vs. Live](/concepts/environments). ## Next steps > **[Send Open Loop Cards](/features/open-loop-cards/send-open-loop-cards)** > > The standard flow — generate links and let Fluz issue the card at claim time. > **[Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience)** > > What the recipient sees when they open and claim a hosted link. > **[Register Customers](/user-registration)** > > Full reference for `registerUser`, including error handling and fallback patterns. # Open loop cards overview Source: https://docs.fluz.app/features/open-loop-cards/send-open-loop-cards Generate hosted virtual card links ("open-loop" Send Cards) from your platform. You call one API to mint one or more links, then deliver those links to recipients however you like — by email, by SMS, or by handing back the raw URLs to embed in your own flows. When a recipient opens the link, they land on a Fluz-hosted page, verify themselves, and claim a single-load virtual card funded from your account. > [!NOTE] > > **Prerequisites:** a Bearer access token carrying the `CREATE_SHARE_LINK` scope. Basic auth is rejected. Contact your sales rep to enable access. See [Authentication](/concepts/authentication). > [!NOTE] > > **What "hosted" / "open-loop" means.** A *hosted* link points to a Fluz-hosted activation page. *Open-loop* means the resulting virtual card is a network card (Visa/Mastercard-style) that can be spent at many merchants, subject to your program's rules — not a single-brand closed-loop gift card. [Demo open loop cards](https://demos.fluz.app/card-issuing/?launch-card=1) ## How it works **1. You generate links** Call `generateVCShareLinks` with the offer, card limit, quantity, funding source, and a delivery method. Each link represents one card with its own limit, funded from the spend account you specify. **2. Fluz creates a share request per link** Each link maps to one share request (`PENDING`) and one hosted URL. **3. The link is delivered** With `GENERATE_URL` you get the URLs back to distribute yourself. With `EMAIL` or `PHONE_NUMBER`, Fluz delivers a link to each recipient for you. **4. The recipient activates and claims the card** The recipient opens the link and verifies their phone number with a one-time code — no app download, no password. The card limit is drawn against your spend account at claim time, not when the link is generated. The card isn't auto-revealed on claim; revealing it prompts the recipient to enter their PIN, or create one if they haven't set one yet. See [Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience) for the full walkthrough. The recipient becomes an authorized user of that virtual card object only — they do not gain access to your account, balances, or any other cards. ![Send cards flow diagram](https://files.readme.io/c19029bfeaca196f7eadc2f020ab56f2aad893261f561307bec543e700f0d74b-diagram.svg) ## Availability & scope | Capability | Status | | - | - | | Single-load virtual cards | ✅ Supported | | Single-use virtual cards | ✅ Supported | | Reloadable cards | ❌ Not supported | | Generate links via API | ✅ Supported | | Generate links via CSV Import | ❌ Coming Soon | | Hosted **gift-card** links | ❌ Not in scope (virtual cards only) | The card share-link object type is `VIRTUAL_CARD` and the card type is `SINGLE_LOAD`. > [!WARNING] > > **Gift cards:** Despite the "virtual cards **and** gift cards" framing of the broader initiative, there is no hosted gift-card claim flow today. Gift-card balances appear in this area only as a *potential funding source* for hosted virtual cards (planned, not yet enabled). Document and build against virtual cards only. ## Operation reference There are three public operations, all gated by the `CREATE_SHARE_LINK` scope: | Operation | Type | Purpose | | - | - | - | | `generateVCShareLinks` | Mutation | Create one or more hosted virtual card links | | `getVCShareLinks` | Query | List/inspect previously generated links | | `deactivateVCShareLinks` | Mutation | Deactivate (expire) links you generated | All Send Cards operations are on the Fluz GraphQL API at `POST https:///api/v1/graphql` with an `Authorization: Bearer ` header. The token must carry the `CREATE_SHARE_LINK` scope — without it, every operation returns *"Missing permissions! Please contact your sales rep to get access to generate VC share links."* ## generateVCShareLinks Creates `quantity` share requests and returns one hosted link per request. ```graphql theme={null} mutation GenerateVCShareLinks($input: GenerateVCShareLinksInput!) { generateVCShareLinks(input: $input) { shareLinks } } ``` ### Input fields | Field | Type | Required | Description | | - | - | - | - | | `cardLimit` | `Int!` | Yes | Spend limit (and load amount) for each card, in whole currency units. Must be a whole number ≥ the program minimum. | | `offerId` | `String!` | Yes | UUID v4 of the merchant offer the card is tied to. The offer must be active and its merchant must be shareable. | | `quantity` | `Int!` | Yes | Number of links to generate. One distinct hosted URL is created per unit. | | `shareMethod` | `ShareMethodType!` | Yes | How recipients are identified and links are delivered: `GENERATE_URL`, `EMAIL`, `PHONE_NUMBER`, `EXISTING_USER`, or `REGISTER_USER`. | | `userCashBalanceId` | `UUID` | Yes\* | The spend account used to fund the cards. *Marked optional in the schema but required in practice — omitting it fails validation.* | | `daysUntilExpiration` | `Int` | No | Days until the link expires. Minimum 1. Defaults to the program default (30 days) if omitted. **This date also becomes the card's lock/freeze date** — see [Expiration & freeze](#expiration--freeze). | | `recipientListEmail` | `[String]` | Conditional | Required and non-empty when `shareMethod = EMAIL`. Length must equal `quantity`. Must be empty otherwise. | | `recipientListPhone` | `[String]` | Conditional | Required and non-empty when `shareMethod = PHONE_NUMBER`. Length must equal `quantity`. Must be empty otherwise. | | `recipientUserIds` | `[UUID]` | Conditional | Known Fluz user IDs to bind as recipients when `shareMethod = EXISTING_USER`. Length must equal `quantity`. Mutually exclusive with `recipientRegistrations`. | | `recipientRegistrations` | `[ShareLinkRecipientRegistrationInput]` | Conditional | Inline pre-register payloads when `shareMethod = REGISTER_USER` — Fluz creates or reuses a placeholder user (no seat) per entry before generating links. Length must equal `quantity`. Mutually exclusive with `recipientUserIds`. | | `usePrepaymentBalance` | `Boolean` | No | Set whether to use your Prepayment Balance as an additional funding source. The default is false. More information about how this is used: [How funding works](/features/virtual-cards#how-funding-works). | | `useRewardsBalance` | `Boolean` | No | Set whether to use your Fluz Rewards Balance as an additional funding source. The default is false. More information about how this is used: [How funding works](/features/virtual-cards#how-funding-works). | > [!NOTE] > > **Funding source.** `userCashBalanceId` (a spend account belonging to your, the sender's, account) is the primary and required funding source. Optionally set `usePrepaymentBalance` and/or `useRewardsBalance` to `true` to let Fluz fall back to your prepayment or rewards balance if the spend account doesn't cover the full amount at claim time. Bank accounts and bank cards are not supported as funding sources. ### Recipient identification & delivery (`shareMethod`) | Value | Behavior | Recipient field | Card created | | - | - | - | - | | `GENERATE_URL` | Fluz returns the hosted URLs in the response. You distribute them yourself. | Both lists must be empty/omitted. | At claim | | `EMAIL` | Fluz emails a link to each recipient. | `recipientListEmail` required; length must equal `quantity`. | At claim | | `PHONE_NUMBER` | Fluz texts a link to each recipient. | `recipientListPhone` required; length must equal `quantity`. | At claim | | `EXISTING_USER` | Link is bound to a known Fluz user from the start; only that user can claim it. Delivered via the contact channel(s) already on file for that user. | `recipientUserIds` required; length must equal `quantity`. | **Immediately**, at generation time | | `REGISTER_USER` | Fluz creates or reuses a placeholder user per entry, then binds the link to that user, same as `EXISTING_USER`. | `recipientRegistrations` required; length must equal `quantity`. | **Immediately**, at generation time | > [!NOTE] > > `EXISTING_USER` and `REGISTER_USER` both create the virtual card as part of the `generateVCShareLinks` call, rather than deferring card creation to claim time. See [Register & Send](/features/open-loop-cards/register-and-send) for the full `EXISTING_USER` flow, including how to register a recipient first with `registerUser`. ### Validation rules * `cardLimit` must be a whole number and at least the program minimum. * `offerId` must be a valid UUID v4 for an **active** offer whose **merchant is shareable**. * `quantity` must be a whole number. * The recipient field matching `shareMethod` (`recipientListEmail`, `recipientListPhone`, `recipientUserIds`, or `recipientRegistrations`) must have length **equal to** `quantity`. Mismatches return a clear error and create **no** records. * `recipientUserIds` and `recipientRegistrations` are mutually exclusive with each other and with the delivery-list fields. * Each ID in `recipientUserIds` must be a valid, existing Fluz user. * `userCashBalanceId` is required and must be a valid UUID v4 owned by the sender's account. `usePrepaymentBalance` and `useRewardsBalance` are optional fallback sources and may both be enabled alongside it. * Invalid card types or malformed inputs return clear errors and create no records. ### Examples ```json Generate URLs theme={null} { "input": { "cardLimit": 25, "offerId": "11111111-2222-3333-4444-555555555555", "daysUntilExpiration": 30, "quantity": 3, "shareMethod": "GENERATE_URL", "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` ```json Email theme={null} { "input": { "cardLimit": 25, "offerId": "11111111-2222-3333-4444-555555555555", "daysUntilExpiration": 30, "quantity": 2, "shareMethod": "EMAIL", "recipientListEmail": ["recipient1@example.com", "recipient2@example.com"], "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` ```json SMS theme={null} { "input": { "cardLimit": 25, "offerId": "11111111-2222-3333-4444-555555555555", "daysUntilExpiration": 30, "quantity": 2, "shareMethod": "PHONE_NUMBER", "recipientListPhone": ["+12125550101", "+12125550102"], "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` ```json Existing user theme={null} { "input": { "cardLimit": 25, "offerId": "11111111-2222-3333-4444-555555555555", "daysUntilExpiration": 30, "quantity": 1, "shareMethod": "EXISTING_USER", "recipientUserIds": ["f1320ac4-52dc-4c67-9e80-24e506b18450"], "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` > [!TIP] > > For `EXISTING_USER`, register the recipient first (or use an existing user's ID directly) — see [Register & Send](/features/open-loop-cards/register-and-send) for the full walkthrough, including the `registerUser` call and response handling. ### Response ```json theme={null} { "data": { "generateVCShareLinks": { "shareLinks": [ "https://fluz.app/virtual-prepaid-card/3f8a...c1", "https://fluz.app/virtual-prepaid-card/9b2d...77", "https://fluz.app/virtual-prepaid-card/0c41...e3" ] } } } ``` `shareLinks` is an array of hosted URLs, one per `quantity`, each of the form `https://fluz.app/virtual-prepaid-card/{share_request_id}`. > [!TIP] > > The response returns only the URLs. To retrieve the **batch ID** and **display IDs** for the links you just created (needed for listing and deactivation), use `getVCShareLinks` filtered by status. ## getVCShareLinks Lists previously generated share links so you can inspect status, recipients, expiration, and the issued card. ```graphql theme={null} query GetVCShareLinks($input: GetVCShareLinksInput!) { getVCShareLinks(input: $input) { senderAppId shareRequestBatchId shareRequestDisplayId shareObjectStatus recipientPhone recipientEmail linkExpirationDate virtualCardId linkUrl shareRequestDetails { cardLimit offerId daysUntilExpiration quantity shareMethod recipientListEmail recipientListPhone userCashBalanceId } } } ``` ### Input fields | Field | Type | Description | | - | - | - | | `shareObjectStatuses` | `[ShareObjectStatus]` | Filter by status: `PENDING`, `ISSUED`, `USED`, `EXPIRED`. | | `shareRequestBatchIds` | `[String]` | Return only links in these batches. | | `shareRequestDisplayIds` | `[String]` | Return only the links with these display IDs. | > [!TIP] > > **Recommended flow.** On the first call, filter by `shareObjectStatuses` only. The response gives you `shareRequestBatchId` and `shareRequestDisplayId` values; use those to filter precisely on subsequent calls (and to deactivate). ### Response fields (`GeneratedShareLink`) | Field | Type | Description | | - | - | - | | `senderAppId` | `String` | The app/developer application that generated the link. | | `shareRequestBatchId` | `String` | Batch identifier shared by all links generated in one call. | | `shareRequestDisplayId` | `String` | Human-friendly per-link identifier. | | `shareObjectStatus` | `ShareObjectStatus` | `PENDING`, `ISSUED`, `USED`, or `EXPIRED`. | | `recipientEmail` | `String` | Recipient email, if delivered by email. | | `recipientPhone` | `String` | Recipient phone, if delivered by SMS. | | `linkExpirationDate` | `DateTime` | When the link expires / card freezes. | | `virtualCardId` | `String` | The issued virtual card ID, once claimed. | | `linkUrl` | `String` | The hosted URL for the link. | | `shareRequestDetails` | `ShareRequestDetails` | The original configuration (card limit, offer, quantity, delivery, funding). | ### Examples ```json By status theme={null} { "input": { "shareObjectStatuses": ["PENDING", "ISSUED"] } } ``` ```json By batch theme={null} { "input": { "shareRequestBatchIds": ["ABC123", "XYZ789"] } } ``` ```json By display ID theme={null} { "input": { "shareRequestDisplayIds": ["SR-000001", "SR-000002"] } } ``` ## deactivateVCShareLinks Deactivates (expires) links you generated — for example, if a batch was sent in error or you need to revoke unclaimed links. Deactivating a link sets it to `EXPIRED`; an unclaimed link can no longer be claimed. ```graphql theme={null} mutation DeactivateVCShareLinks($input: DeactivateVCShareLinksInput!) { deactivateVCShareLinks(input: $input) } ``` ### Input fields | Field | Type | Description | | - | - | - | | `shareRequestBatchIds` | `[String]` | Deactivate every link in these batches. | | `shareRequestDisplayIds` | `[String]` | Deactivate only the links with these display IDs. | Get the batch IDs from `getVCShareLinks`. ```json theme={null} { "input": { "shareRequestBatchIds": ["ABC123"] } } ``` Returns a human-readable confirmation string, e.g. `"3 share requests successfully deactivated!"`. > [!WARNING] > > If a recipient has already **claimed** a link (status `ISSUED`/`USED`), deactivating the link does not claw back the issued card. To stop spend on an already-issued card, use the relevant card lifecycle/freeze controls. ## Expiration & freeze The link's expiration date does double duty: * **Link expiration** — after this date, an **unclaimed** link can no longer be claimed. * **Card freeze / lock date** — for an **issued** card, this is the lock date (end of that day). After it, the card is frozen and cannot be spent. * **Card expiry** is aligned to the end of the month of the freeze date (e.g., a freeze date of 6/15/2026 yields a card expiry of 6/30/2026). Set the window with `daysUntilExpiration` at generation time. If omitted, the program default (30 days) is used. This date is shown to the recipient (typically as a "Valid until" date) — see [Recipient Experience](/features/open-loop-cards/open-loop-cards-recipient-experience). ## Status & error reference ### Share object statuses | Status | Meaning | | - | - | | `PENDING` | Link generated, not yet claimed. | | `ISSUED` | Recipient claimed the link; a virtual card was issued. | | `USED` | The issued card has been used. | | `EXPIRED` | Link expired or was deactivated; no longer claimable. | For the states a recipient sees when a link is expired, revoked, or already claimed, see [Recipient-facing link errors](/features/open-loop-cards/open-loop-cards-recipient-experience#recipient-facing-link-errors). ### Common API errors | Cause | Result | | - | - | | Missing/invalid Bearer token or missing `CREATE_SHARE_LINK` scope | Request rejected (unauthorized). | | Recipient field (`recipientListEmail`, `recipientListPhone`, `recipientUserIds`, or `recipientRegistrations`) length ≠ `quantity` | Clear validation error; no records created. | | `recipientUserIds` references a non-existent Fluz user | Validation error; no records created. | | Inactive offer, non-shareable merchant, or invalid `offerId` | Validation error; no records created. | | Missing/invalid `userCashBalanceId` | Validation error; no records created. | ## Notes & limitations * **Returned URL is the hosted destination, not a short link.** Internally, links are also wrapped by a short-link provider, but the API returns the canonical hosted URL (`/virtual-prepaid-card/{share_request_id}`). Distribute the URL exactly as returned. * `userCashBalanceId` is effectively required even though the schema marks it optional. * **Hidden/internal fields are not part of this API.** Object type and card type are fixed (`VIRTUAL_CARD` / `SINGLE_LOAD`). Bank account and bank card funding are not yet enabled; do not send them. `usePrepaymentBalance` and `useRewardsBalance` are the only supported additional funding sources today. * **Gift-card hosted links are not supported.** This API is for virtual cards only. ## Next steps > **[Recipient experience](/features/open-loop-cards/open-loop-cards-recipient-experience)** > > What your recipients see when they open a hosted link, and the rules that govern their card. > **[Register & Send](/features/open-loop-cards/register-and-send)** > > Use `EXISTING_USER` to register a recipient and create their card up front, instead of at claim. > **[Create a bulk order](/features/create-bulk-order)** > > Issue many cards at once for programmatic distribution. # Primary & backup funding Source: https://docs.fluz.app/features/primary-and-backup-funding Every Fluz account funds transactions from a **default funding source** built on two roles — a **primary** bank account and a **backup** card. The primary is charged first; the backup steps in when the primary can't be. This page covers how funding works and how to read and change both roles, over the API or in the app. | Role | Shown in app as | Must be a | Purpose | | - | - | - | - | | **Primary** | Preferred account | Bank account (ACH) | The default funding source, charged first for every transaction. | | **Backup** | Backup card | Bank card (debit / credit / prepaid) | Charged if the primary funding source can't be. | Each account has exactly **one primary and one backup** at a time. Selecting a new funding source for a role replaces the previous one. *** ## How funding works When a user makes a purchase, funds a virtual card, or deposits to their Fluz balance, Fluz charges the **primary** funding source first — by default, the preferred bank account over ACH. Because ACH can take several days to settle (and can fail), the **backup card** acts as a safety net: 1. When a user pays via ACH, a temporary **hold** may be placed on the backup card for the transaction amount. 2. If the ACH payment **clears**, the hold is released within **1–7 business days**. 3. If the ACH payment **does not clear**, the **backup card is charged** instead. ```mermaid theme={null} flowchart TD A([Transaction initiated]) --> B{Charge primary
bank account via ACH} B -->|ACH accepted| C[Temporary hold placed
on backup card] C --> D{ACH clears?} D -->|Yes| E([Hold released in
1–7 business days]) D -->|No| F([Backup card charged]) B -->|Primary can't be charged| F ``` > ❗️ A backup card must be on file before a transaction can complete > > Fluz requires a valid bank card as the backup so a transaction can still settle if the primary funding source fails. Without one, transactions that depend on ACH **cannot be started**. *** ## Configuring primary & backup Primary and backup selection can be managed **via the API** or **in the Fluz app / web portal**. Selecting a new funding source for a role replaces the previous one for that account. ### Via the API > 📘 Authentication & scopes > > These operations require a **user access token**. Reads (`getDefaultFundingSource`) use the `LIST_PAYMENT` scope; changes (`setPrimaryFundingSource`, `setBackupFundingSource`) use `MANAGE_PAYMENT`. #### Read the current default funding source Returns both roles for the account. `primary` is `null` when no primary bank account is set; `backup` is `null` when no backup card is set. ```graphql Query theme={null} query { getDefaultFundingSource { primary { bankAccountId accountName status lastFour } backup { bankCardId lastFourDigits cardType cardStatus } } } ``` ```json Response theme={null} { "data": { "getDefaultFundingSource": { "primary": { "bankAccountId": "d290f1ee-6c54-4b01-90e6-d701748f0851", "accountName": "Chase Total Checking", "status": "ENABLED", "lastFour": "6789" }, "backup": { "bankCardId": "16fd2706-8baf-433b-82eb-8c7fada847da", "lastFourDigits": "4242", "cardType": "CREDIT", "cardStatus": "ACTIVE" } } } } ``` **Response fields** | Field | Type | Description | | - | - | - | | `primary` | object \| null | The primary bank account, or `null` if none is set. | | `primary.bankAccountId` | ID | UUID of the linked bank account. | | `primary.accountName` | string | Display name of the bank account. | | `primary.status` | enum | Bank account status (e.g. `ENABLED`). | | `primary.lastFour` | string | Last four digits of the account number. | | `backup` | object \| null | The backup bank card, or `null` if none is set. | | `backup.bankCardId` | ID | UUID of the bank card. | | `backup.lastFourDigits` | string | Last four digits of the card. | | `backup.cardType` | enum | Card type — `DEBIT`, `CREDIT`, or `PREPAID`. | | `backup.cardStatus` | enum | Card status (e.g. `ACTIVE`). | > 👍 One call, two reads > > The same `DefaultFundingSource` object is also returned inside `getWallet { defaultFundingSource { … } }` — so you can read the current selection in the same call you use to list wallet contents. #### Set the primary funding source (a bank account) ```graphql Mutation theme={null} mutation { setPrimaryFundingSource(input: { bankAccountId: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) { primary { bankAccountId accountName } backup { bankCardId lastFourDigits } } } ``` ```json Response theme={null} { "data": { "setPrimaryFundingSource": { "primary": { "bankAccountId": "d290f1ee-6c54-4b01-90e6-d701748f0851", "accountName": "Chase Total Checking" }, "backup": { "bankCardId": "16fd2706-8baf-433b-82eb-8c7fada847da", "lastFourDigits": "4242" } } } } ``` | Argument | Type | Required | Description | | - | - | - | - | | `input.bankAccountId` | ID (UUID) | Yes | A bank account linked to the account and **enabled**. Becomes the funding source charged first. | #### Set the backup funding source (a bank card) ```graphql Mutation theme={null} mutation { setBackupFundingSource(input: { bankCardId: "16fd2706-8baf-433b-82eb-8c7fada847da" }) { primary { bankAccountId accountName } backup { bankCardId lastFourDigits } } } ``` ```json Response theme={null} { "data": { "setBackupFundingSource": { "primary": { "bankAccountId": "d290f1ee-6c54-4b01-90e6-d701748f0851", "accountName": "Chase Total Checking" }, "backup": { "bankCardId": "16fd2706-8baf-433b-82eb-8c7fada847da", "lastFourDigits": "4242" } } } } ``` | Argument | Type | Required | Description | | - | - | - | - | | `input.bankCardId` | ID (UUID) | Yes | A bank card on the account that is **active**. Becomes the fallback funding source. | > 📘 Both mutations return the account's full `DefaultFundingSource` > > Each mutation returns **primary + backup** after the change — so a single call updates one role and confirms the resulting state. #### Validation rules * The `bankAccountId` must be a bank account **linked to the account and enabled**; the `bankCardId` must be a bank card **on the account and active**. * The primary must be a **bank account** and the backup a **bank card** — a mismatch is rejected. * Setting a role **replaces** the account's previous selection (one primary, one backup at a time). #### Errors | Situation | `errorName` | `code` | `statusCode` | `message` | | - | - | - | - | - | | Bank account not found / not owned / not enabled | `InvalidBankAccount` | `BA-0001` | 400 | Please verify that your account is active and up-to-date and try again. | | Bank card not found / not owned / not active | `InvalidBankCard` | `BC-0001` | 400 | Looks like this bank card is not a valid card, please select a new card and try again | | Malformed input (e.g. not a UUID) | `InvalidArguments` | `ARG-0001` | 422 | Invalid arguments received *(not user-friendly)* | ```json Example error theme={null} { "errors": [ { "message": "Please verify that your account is active and up-to-date and try again.", "extensions": { "errorName": "InvalidBankAccount", "code": "BA-0001", "statusCode": 400, "userFriendly": true } } ], "data": { "setPrimaryFundingSource": null } } ``` The structured error lives under `extensions`, keyed off `code` / `errorName`. `statusCode` is a *semantic* status (e.g. `400`, `422`) carried inside `extensions` — the GraphQL transport itself returns **HTTP 200 even for errors**, with the failure in the `errors` array and `data.` set to `null`. **Detect failures by checking for an `errors` array, not the HTTP status.** Some errors also carry optional `codeNumber` and `notificationDetails` keys, and `userFriendly` indicates whether `message` is safe to surface directly to end users. ### In the app / web **On the web** 1. Open **Accounts and Cards** from the account menu (or go to `/accounts-and-cards`). 2. In the **Default funding source** section at the top of the page: * Select **Manage preferred account** to choose the bank account used as the default funding source. * Select **Manage backup card** to choose the bank card charged if the preferred account can't be. 3. Your selection is saved immediately. **On mobile** 1. Open the menu and tap **Accounts and Cards**. 2. Under **Default funding source**, tap **Manage preferred account** or **Manage backup card**. 3. Choose from your linked funding sources. Your selection is saved immediately. If you don't yet have an eligible funding source, the app prompts you to add one first — a bank account for the preferred account, or a bank card for the backup. *** ## What can be done via API vs. app / web | Task | API | App / Web | | - | :-: | :-: | | Add, update, or delete a bank card | ✓ | ✓ | | Link or remove a bank account (Plaid) | ✓ | ✓ | | Set which funding source is primary / backup | ✓ | ✓ | See [Bank Cards](doc:bank-cards) and [Bank Accounts](doc:bank-accounts) for the underlying funding-source operations available through the API. *** ## Eligibility & notes * The primary payment method must be a **linked bank account**; the backup must be a **bank card**. * Business accounts must complete **KYB verification** before funding sources — and therefore the default funding source — can be set. * Some funding sources may be restricted for specific merchants or by account limits and won't be selectable as a default. > 📘 Want to learn more? [Speak with our experts](doc:contact) for more info or to request a demo. # Query authorized user Source: https://docs.fluz.app/features/query-authorized-user List authorized users on the caller's account. Results are always scoped to the caller's account — Bearer tokens use the token's account; Basic (API key) callers use the application's configured operator account. Role assignments for `OWNER` are excluded. Both filters are optional and narrow the results further by `email` and/or `phone`. 🔒 Restricted Access This query requires the `VIEW_SUBUSERS` scope. It supports both Bearer (user access token) and Basic (``) authentication. ```graphql theme={null} query AuthorizedUsers( $email: String $phone: String ) { authorizedUsers( email: $email phone: $phone ) { authUserId roles status email phone firstName lastName } } ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | email | String | No | Filter by the email address of the authorized user. | | phone | String | No | Filter by the phone number of the authorized user. | ### Response #### Success Response ```json theme={null} { "data": { "authorizedUsers": [ { "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "roles": ["MANAGER", "VIEWER"], "status": "ACTIVE", "email": "teammate@example.com", "phone": "+15555555555", "firstName": "Ada", "lastName": "Lovelace" } ] } } ``` ### Response Fields | Field | Type | Description | | :- | :- | :- | | `authUserId` | UUID | The authorized user ID (UAC role assignment ID). Pass this value to `removeAuthorizedUser`. | | `roles` | \[UACRoleType] | Roles assigned to the user on the account. | | `status` | UACRoleStatusType | Status of the role assignment: `PENDING`, `ACTIVE`, `INACTIVE`, or `DECLINED`. | | `email` | String | Email address of the authorized user. | | `phone` | String | Phone number of the authorized user. | | `firstName` | String | First name of the authorized user. | | `lastName` | String | Last name of the authorized user. | ### Example Request ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "query AuthorizedUsers($email: String, $phone: String) { authorizedUsers(email: $email, phone: $phone) { authUserId roles status email phone firstName lastName } }", "variables": { "email": "teammate@example.com" } }' ``` ```typescript theme={null} const response = await fetch('https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ query: ` query AuthorizedUsers( $email: String $phone: String ) { authorizedUsers( email: $email phone: $phone ) { authUserId roles status email phone firstName lastName } } `, variables: { email: "teammate@example.com" } }) }); const data = await response.json(); console.log('Authorized users:', data.data.authorizedUsers); ``` ### Error Codes | Code | Message | Description | | :- | :- | :- | | `AUTH-0008` | Invalid user access | The caller could not be resolved from the access token or API key, or the Basic-auth application has no operator account configured. Verify your authentication credentials. | | `AUTH-0031` | The requested scopes must be granted by the user first. | The token is missing the `VIEW_SUBUSERS` scope required to list authorized users. | # Wallet transfer recipient lookup Source: https://docs.fluz.app/features/recipient-lookup Used to retrieve the `account_id` of the account for transfering funds. ### Overview Two queries for looking up recipients for transfer operations: * `lookupUser` - Find a user by phone or email * `lookupBusiness` - Find businesses by company name *** ### lookupUser Look up an individual user recipient. #### Query (UserLookupInput) ```graphql theme={null} lookupUser(input: UserLookupInput!): UserLookupResult! ``` **Requires**: `QUERY_RECIPIENT` scope. #### Input | Field | Type | Required | Description | | :- | :- | :- | :- | | phoneNumber | String | No\* | The phone number of the recipient in E.164 format (e.g., +14155551234) | | email | String | No\* | The email address of the recipient | *Provide exactly one field* #### Response (UserLookupResult) | Field | Type | Description | | :- | :- | :- | | recipientId | UUID! | Recipient's account ID | | name | String! | Display name ("First Last") | #### Examples **By phone:** ```graphql theme={null} query { lookupUser(input: { phoneNumber: "+14155551234" }) { recipientId name } } ``` **By email:** ```graphql theme={null} query { lookupUser(input: { email: "jane@example.com" }) { recipientId name } } ``` **Success:** ```json theme={null} { "data": { "lookupUser": { "recipientId": "a2c02ec6-357b-41cf-9229-4da34e727019", "name": "John Doe" } } } ``` **Not found:** ```json theme={null} { "errors": [{ "message": "No recipient found with the provided information.", "extensions": { "code": "RC-0001" } }] } ``` #### Error Codes | Code | Description | | :- | :- | | RC-0001 | Recipient not found | | RC-0002 | Multiple matches (rare) | | AR-0001 | Invalid/missing input | | GE-9999 | Server error | *** ### lookupBusiness Look up business recipients by company name. #### Query ```graphql theme={null} lookupBusiness(input: BusinessLookupInput!): [BusinessLookupResult!]! ``` **Requires**: `QUERY_RECIPIENT` scope. #### Input | Field | Type | Required | Description | | :- | :- | :- | :- | | companyName | String | Yes | Company name (case-insensitive, exact match) | **Note:** Searches both registered business name and DBA name. Must match exactly (case-insensitive). #### Response | Field | Type | Description | | :- | :- | :- | | recipientId | UUID! | Recipient's account ID | | companyName | String! | Formatted name: "Company Name (DBA Name)" | | state | String | State from business legal address | #### Examples ```graphql theme={null} query { lookupBusiness(input: { companyName: "acme corporation" }) { recipientId companyName state } } ``` **Single match:** ```json theme={null} { "data": { "lookupBusiness": [ { "recipientId": "c4e24ge8-579d-63eh-b451-6fc56g949241", "companyName": "Acme Corporation", "state": "CA" } ] } } ``` **Multiple matches (same name, different DBAs):** ```json theme={null} { "data": { "lookupBusiness": [ { "recipientId": "c4e24ge8-579d-63eh-b451-6fc56g949241", "companyName": "Acme Corporation (Acme Retail)", "state": "CA" }, { "recipientId": "d5f35hf9-680e-74fi-c562-7gd67h050352", "companyName": "Acme Corporation (Acme Wholesale)", "state": "NY" } ] } } ``` **Not found:** ```json theme={null} { "errors": [{ "message": "No recipient found with the provided information.", "extensions": { "code": "RC-0001" } }] } ``` #### Error Codes | Code | Description | | :- | :- | | RC-0001 | Recipient not found | | AR-0001 | Invalid/missing input | | GE-9999 | Server error | *** ### Important Notes #### lookupUser * Phone numbers must be in E.164 format (e.g., +14155551234) * 10-digit US numbers are auto-normalized * Email is case-insensitive * Throws error if not found ### lookupBusiness * Case-insensitive exact match - "acme corporation" matches "ACME CORPORATION" or "Acme Corporation" * Searches both business name and DBA name * Returns array (may contain multiple businesses with the same name) * Throws error if no matches found * `companyName` is formatted as "Business Name (DBA Name)" when DBA differs from business name # Redeem Fluz gift card Source: https://docs.fluz.app/features/redeem-fluz-gift-card The `redeemFluzGiftCard` mutation allows you to redeem a Fluz Gift Card to the authenticated user’s account using the gift card code. Successful redemption credits the user’s **gift card balance** (`giftCardCashBalance`). This mutation requires the `MAKE_DEPOSIT` scope. *** ### Arguments * **`input`** (`RedeemFluzGiftCardInput!`): The input object containing the idempotency key and gift card code. *** ### RedeemFluzGiftCardInput Fields | Field | Type | Description | Required | | - | - | - | - | | `idempotencyKey` | `UUID!` | Unique client-generated key to ensure the request is processed only once. | Yes | | `code` | `String!` | The Fluz Gift Card code. Leading/trailing whitespace is automatically trimmed. | Yes | *** ### Response Returns a `DepositCashBalanceResponse`, including: * **`cashBalanceDeposits`** — Deposit records created for this redemption * **`balances`** — Updated user balances after redemption > The redeemed value is credited to `balances.giftCardCashBalance` (not `cashBalance`). *** ### Sample Mutation ```graphql theme={null} mutation RedeemGC($input: RedeemFluzGiftCardInput!) { redeemFluzGiftCard(input: $input) { cashBalanceDeposits { cashBalanceDepositId depositDisplayId depositAmount depositFee status cashBalanceDepositType transactionDate clearedDate cashBalanceSettlements { cashBalanceSettlementId availabilityType status } } balances { cashBalance { availableBalance totalBalance pendingBalance lifetimeBalance } giftCardCashBalance { availableBalance totalBalance lifetimeBalance } rewardsBalance { availableBalance totalBalance lifetimeBalance } } } } ``` *** ### Variables ```json theme={null} { "input": { "idempotencyKey": "ee940ccc-2d24-4e1f-8c81-1a3a8b59fa01", "code": "FLUZ-GC-CODE-EXAMPLE" } } ``` *** ### cURL Example ```bash theme={null} curl -X POST \ https://transactional-graph-service.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation RedeemGC($input: RedeemFluzGiftCardInput!) { redeemFluzGiftCard(input: $input) { cashBalanceDeposits { cashBalanceDepositId depositDisplayId depositAmount depositFee status cashBalanceDepositType transactionDate clearedDate cashBalanceSettlements { cashBalanceSettlementId availabilityType status } } balances { cashBalance { availableBalance totalBalance pendingBalance lifetimeBalance } giftCardCashBalance { availableBalance totalBalance lifetimeBalance } rewardsBalance { availableBalance totalBalance lifetimeBalance } } } }", "variables": { "input": { "idempotencyKey": "ee940ccc-2d24-4e1f-8c81-1a3a8b59fa01", "code": "FLUZ-GC-CODE-EXAMPLE" } } }' ``` *** ### Behavior * **Balance destination** — Funds are credited to `giftCardCashBalance`. * **Idempotency** — Duplicate requests with the same `idempotencyKey` are rejected while in-flight. * **Whitespace handling** — Leading/trailing spaces in `code` are trimmed automatically. * **Fees** — Any activation fee is reflected in `depositFee`. * **Settlement** — Redemptions are **instant** (`INSTANT`, `AVAILABLE`). *** ### Errors | Code | Description | | - | - | | `ARG-XXXX` | Missing or invalid `idempotencyKey` or `code`. | | `GC-0003` | Invalid, inactive, or already redeemed gift card. | | `G-0409` | Duplicate request in progress for the same `idempotencyKey`. | | `G-0004` | General failure (e.g., feature access, internal error). | # Relinking bank accounts Source: https://docs.fluz.app/features/relink-accounts A previously linked Plaid connection can drop — for example, when the user changes their bank credentials. While it's disconnected, Fluz can't fetch updated balances, so it must be repaired with a Plaid **update-mode** Link flow before balances and spend power will refresh again. ## Auth Call `/api/v1/graphql` with a Fluz user Bearer access token that includes `MANAGE_PAYMENT`. Basic auth is not allowed on these fields. ```http theme={null} Authorization: Bearer ``` ## Detecting that a relink is needed There are two reliable signals: 1. `refreshPlaidBankConnections `**returns** `verifyMembers`**.** Each entry identifies a connection that needs repair; use its `platformItemId` to start the relink flow below. (See *Managing Bank Account Spend Power* for that mutation.) 2. `createPlaidLinkToken `**fails** with `No active Plaid connection found for the provided platformItemId.` — the stored connection is no longer relinkable; guide the user through a **new** bank link instead (see *Linking a Plaid Bank Account*). ## Relink flow You'll need the stored `platformItemId` for the disconnected connection. ### 1. Create a Link token with the stored `platformItemId` ```graphql theme={null} mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) { createPlaidLinkToken(input: $input) { linkToken expiration requestId mode } } ``` ```json theme={null} { "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`. ### 2. Open Plaid Link Initialize Plaid Link with the returned `linkToken`. ### 3. Complete the relink In Plaid Link's `onSuccess`, complete the flow with **both** `publicToken` and the same `platformItemId`. ```graphql theme={null} mutation CompletePlaidLink($input: CompletePlaidLinkInput!) { completePlaidLink(input: $input) { requiresAddress bankAccountId bankInstitutionAuthId newlyLinkedBankInstitutionAuthId bankInstitutionName platformItemId bankAccounts { bankInstitutionAuthId bankAccountId bankName lastFour type subtype } } } ``` ```json theme={null} { "input": { "publicToken": "public-sandbox-...", "platformItemId": "plaid-item-id" } } ``` ### 4. Update the stored `platformItemId` Update your stored `platformItemId` from the response if it changed. If it isn't returned, call `getPlaidBankAccounts` for the user and store the persisted `platformItemId` from that response. ## Using the Web SDK for relink The shared `startPlaidLink` helper in *Linking a Plaid Bank Account* handles both cases. Call `startPlaidLink(existingPlatformItemId)` to repair a disconnected connection; call `startPlaidLink()` with no argument for a brand-new link. ## Error handling If `createPlaidLinkToken` fails with `No active Plaid connection found for the provided platformItemId.`, treat the stored connection as no longer relinkable and guide the user through a new bank link. 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. # Remove authorized user Source: https://docs.fluz.app/features/remove-authorized-user Remove an authorized user from the caller's account by deactivating their role assignment. This mutation does not delete the user — it sets the role assignment on the caller's account to `INACTIVE`, revoking their access. The account owner (`OWNER` role) cannot be removed through this endpoint. The target account is always resolved from the caller's credentials — Bearer tokens use the token's account; Basic (API key) callers use the application's configured operator account. The `authUserId` must refer to a role assignment on that account; otherwise the request is rejected. 🔒 Restricted Access This mutation requires the `MANAGE_SUBUSERS` scope. It supports both Bearer (user access token) and Basic (``) authentication. ```graphql theme={null} mutation RemoveAuthorizedUser( $authUserId: UUID! ) { removeAuthorizedUser( authUserId: $authUserId ) { success authUserId status error { code message } } } ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | authUserId | UUID | Yes | The authorized user ID (UAC role assignment ID) to deactivate. Obtain this from `authorizedUsers` or `addAuthorizedUser`. | ### Response #### Success Response ```json theme={null} { "data": { "removeAuthorizedUser": { "success": true, "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d", "status": "INACTIVE", "error": null } } } ``` ### Response Fields | Field | Type | Description | | :- | :- | :- | | `success` | Boolean | `true` if the role assignment was successfully deactivated. | | `authUserId` | UUID | The authorized user ID (UAC role assignment ID) that was updated. | | `status` | UACRoleStatusType | Updated status of the role assignment. Will be `INACTIVE` on success. | | `error` | AuthorizedUserError | If `success` is false, an Error object containing `code` and `message`. | > [!NOTE] > > This mutation returns errors in the response data, not as GraphQL errors. Always check the `success` field and handle the `error` object when `success` is false. ### Example Request ```curl theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation RemoveAuthorizedUser($authUserId: UUID!) { removeAuthorizedUser(authUserId: $authUserId) { success authUserId status error { code message } } }", "variables": { "authUserId": "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d" } }' ``` ```typescript theme={null} const response = await fetch('https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ query: ` mutation RemoveAuthorizedUser( $authUserId: UUID! ) { removeAuthorizedUser( authUserId: $authUserId ) { success authUserId status error { code message } } } `, variables: { authUserId: "8b2c1e0a-7d4f-4a9b-9c2d-1f3e4a5b6c7d" } }) }); const data = await response.json(); if (data.data.removeAuthorizedUser.success) { console.log('Authorized user removed:', data.data.removeAuthorizedUser); } else { console.error('Remove authorized user failed:', data.data.removeAuthorizedUser.error); } ``` ### Error Codes | Code | Message | Description | | :- | :- | :- | | `ARG-0002` | Missing required arguments | `authUserId` was not provided. | | `AUTH-0008` | Invalid user access | The caller could not be resolved from the access token or API key, or the Basic-auth application has no operator account configured. Verify your authentication credentials. | | `AUTH-0031` | The requested scopes must be granted by the user first. | The token is missing the `MANAGE_SUBUSERS` scope required to manage authorized users. | | `AUTH-0034` | No role assignment found for the provided authorized user on the specified account. | The `authUserId` does not exist on the caller's account, or the assignment is already `INACTIVE`. | | `AUTH-0036` | The account owner cannot be removed. | The referenced role assignment holds the `OWNER` role and cannot be removed through this endpoint. | | `AUTH-0037` | Unable to manage authorized user. Please try again or contact support. | A general failure occurred while deactivating the role assignment. Please retry or contact support. | # Overview Source: https://docs.fluz.app/features/spend-accounts A **spend account** is a cash balance account inside Fluz that a user uses to hold, organize, and track funds. Each account has its own nickname, so a user can separate spending by purpose — for example "Team Travel," "Operations Wallet," or "Marketing Budget" — and every account keeps its own running balances. Spend accounts are the wallet that everything else draws from: they hold the funds used to purchase gift cards, fund virtual card transactions, and receive deposits and transfers. > [!NOTE] > > **"Spend account" and "cash balance account" are the same thing.** > > In the API, a spend account is represented by the `UserCashBalance` type, and the operations are named `...UserCashBalance` (e.g., `createUserCashBalance`). This page uses "spend account" throughout. *** ## Balances Every spend account tracks three balances, all returned as strings: | Balance | Field | Description | | - | - | - | | **Total** | `totalCashBalance` | The full balance currently held in the account. | | **Available** | `availableCashBalance` | The portion available to spend immediately. | | **Lifetime** | `lifetimeCashBalance` | The cumulative total of all funds ever deposited into the account. | A user can hold **multiple** spend accounts, and one is flagged as the **default** (`isDefault: true`). *** ## What You Can Do | Action | Query / Mutation | Scope | Description | | - | - | - | - | | [Create a spend account](/features/create-spend-accounts) | `createUserCashBalance` | `MANAGE_PAYMENT` | Open a new spend account with a custom nickname. | | [Get spend accounts](/features/get-spend-accounts) | `getUserCashBalances` · `getUserCashBalanceById` | `LIST_PAYMENT` | List all spend accounts (with filtering and pagination) or fetch one by ID. | | [Edit a spend account](/features/edit-spend-accounts) | `updateUserCashBalance` | `MANAGE_PAYMENT` | Rename a spend account (nickname, 2–100 characters). | | [Close a spend account](/features/close-spend-accounts) | `closeUserCashBalance` | `MANAGE_PAYMENT` | Close an account, move its remaining balance, and handle any dependent virtual cards. | *** ## Account Lifecycle A spend account moves through a small set of statuses (`UserCashBalanceStatus`): typically `ACTIVE`, and `CLOSED` once closed. Closing an account is more than a status change. When you close a spend account with a remaining available balance, that balance must be moved to another spend account. If any virtual cards are funded by the account being closed, you must either reassign them to a new funding source or lock them as part of the same request. > [!NOTE] > > **Closing an account requires you to resolve its balance and dependent cards.** > > If the account still holds an available balance, provide `transferUserCashBalanceId` to move it. If virtual cards are tied to the account, provide either `newFundingSource` to reassign them or `lockAllVirtualCards` to lock them. See [Close Spend Accounts](/features/close-spend-accounts) for the full input reference. *** ## How Spend Accounts Fit In Spend accounts sit at the center of a user's wallet: * **Fund them** with [Deposit Funds](/features/deposit-from-external-accounts) or move money between them with [Transfer Funds Internally](/features/transfer-between-spend-accounts). * **Spend from them** on [gift cards](/purchase-gift-card) and [virtual cards](/features/virtual-cards). * **Check balances** anytime with [Check Account Balance](/check-account-balance) or the spend account queries above. *** ## Requirements Listing spend accounts requires the `LIST_PAYMENT` scope. Creating, editing, and closing spend accounts require the `MANAGE_PAYMENT` scope. Make sure your user access token carries the correct scope before calling these operations. *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Transactions overview Source: https://docs.fluz.app/features/transactions-details-overview One ledger for every money movement on the account — which query to reach for, what a transaction record contains, and how to reconcile against your own system. Every movement of money on a Fluz account produces a transaction record: gift card purchases, virtual card authorizations, deposits, withdrawals, internal transfers, wallet-to-wallet sends, bill payments, and cashback. One feed, one shape, one query. Records are **account-level**. There is no user-level filtering — a query returns everything on the account your token is scoped to. *** ## Which query do I want? | You want | Use | Page | | :- | :- | :- | | Everything, filtered any way | `getTransactions` | [Get All Transactions](/features/get-all-transactions) | | Only authorizations that were declined | `getDeclinedTransactions` | [Get Declined Transactions](/features/get-decline-transactions) | | Activity on specific virtual cards | `getVirtualCardTransactions` | [Get Virtual Card Transactions](/features/get-virtual-card-transactions) | | Gift card orders as purchases | `getUserPurchases` | [Get Gift Card Purchases](/features/get-gift-card-purchases) | | Gift cards as assets, with remaining value | `getGiftCards` | [View Gift Cards](/view-gift-card) | > [!NOTE] > > **Declines are not a transaction status.** `status` is only ever `PENDING` or `SETTLED` — a declined authorization never becomes a settled transaction, so it won't appear in `getTransactions` at all. If you're debugging "the charge didn't go through," that's `getDeclinedTransactions` and [Decline Codes](/features/decline-codes), not this feed. *** > [!WARNING] > > **Field naming is mixed, and you have to match it exactly.** > > Most fields on the `Transaction` type are snake\_case — `record_id`, `transaction_type`, `created_at`, `cash_balance_available_balance`. But newer additions are camelCase — `memo`, `transactionCategory`, `attachmentUrl`, `connectedAppId`, `connectedAppName`, `expectedClearedDate`. > > Everything *around* the record is camelCase: the filter input (`createdGte`, `amountGte`, `virtualCardProgram`) and the connection fields (`totalCount`, `hasNextPage`). > > Introspect the schema before writing queries rather than assuming a convention. → [How the GraphQL API works](/concepts/graphql) *** ## What's in a record Around fifty fields, in six groups. Request only what you need — the response is large if you ask for everything. > **Identity and routing** > > `record_id`, `account_id`, `user_id`, `user`, `transaction_type`, `channel` (`WEB`, `MOBILE`, `API`), `connectedAppId` and `connectedAppName` — which of your applications initiated it. > **The money** > > `amount`, `fee`, `cashback`, `cashback_rate`, `bonus_cashback_rate`, plus two directional fields worth understanding: > > * `external_funding_source_activity` — the change to external funding sources (bank cards and accounts) > * `fluz_balance_activity` — the change to internal Fluz balances > > Together these tell you whether money entered Fluz, left Fluz, or just moved around inside it. > **Balance snapshots** > > See [below](#balance-snapshots) — every record carries the after-state of every balance. > **Context** > > `source` and `destination` as display strings ("Visa \*\*\*\*1234", "Amazon"), `description`, `merchant_id`, `logo_url`, `card_last_four`, `card_display_name`, `virtual_card_program`, `source_type`. > > Foreign transactions add `original_currency_amount`, `original_currency_code`, and `conversion_rate`. > **Your annotations** > > `memo`, `transactionCategory`, `attachmentUrl` — see [Annotating](#annotating-transactions). > **Linkage** > > `reference_id`, `transfer_id`, `liability_id`, `used_user_cash_balance_id`, `descriptor_id` — the fields you reconcile against. See [Reconciliation](#reconciling-against-your-own-system). ### Balance snapshots Every transaction carries the balance of **every** balance type as it stood *after* that transaction was applied. That makes the feed a replayable ledger — you can reconstruct the state of the account at any point in its history without a separate balance-history API. | Field prefix | Balance | Product name | | :- | :- | :- | | `cash_balance_*` | Cash | Cash balance | | `seat_balance_*` | Rewards | Rewards balance | | `gift_card_prepayment_balance_*` | Prepayment | Prepayment balance | | `reserve_balance_*` | Reserve | Reserve balance | | `other_cash_balance_*` | Other cash | — | > [!NOTE] > > `seat_balance_*` is the **rewards** balance. The naming is historical — don't go looking for a separate seat concept. Each comes in `_available_balance` and `_total_balance`. Paired `is_*_affected` booleans (`is_cash_balance_affected`, `is_seat_balance_affected`, `is_gift_card_balance_affected`, `is_reserve_balance_affected`) tell you which balances this transaction actually touched — cheaper to branch on than diffing snapshots. → [Wallet Overview](/features/move-funds-with-external-accounts) for what each balance is. *** ## Filtering `getTransactions` takes a rich `TransactionFilterInput`. The families: | Family | Fields | | :- | :- | | **Record & status** | `recordId`, `status` | | **Amount** | `amount`, `amountGte`, `amountLte`, and the same three for `finalAmount` | | **Cashback** | `cashbackAmount`, `cashbackPercentage`, each with `Gte`/`Lte` | | **Fees** | `feeAmount`, `feeAmountGte`, `feeAmountLte` | | **Dates** | `createdGte`, `createdLte`, `updatedGte`, `updatedLte` — ISO 8601, UTC | | **Merchant** | `merchantId`, `merchant` | | **Properties** | `transactionType`, `channel`, `category` | | **Virtual cards** | `virtualCard`, `virtualCardProgram` | | **Other** | `fundingSource`, `userCashBalanceId`, `referenceId`, `liabilityId` | `amount` is the base amount; `finalAmount` is amount plus fees — the total actually charged. Filter on `finalAmount` when reconciling against what a funding source was debited. > [!WARNING] > > **Confirm the accepted values for `transactionType` before relying on it.** The reference page lists human-readable strings (`Add Money`, `Gift Card Purchase`, `Transfer - Out`) in one place and enum-style constants (`GIFT_CARD_PURCHASE`, `DEPOSIT`) in its examples and sample responses. These are not interchangeable. Query a small unfiltered page first and read the actual `transaction_type` values your account returns. ### Pagination and throughput `limit` **maxes out at 20**, and `offset` walks forward. Check `hasNextPage` rather than inferring from a short page; `totalCount` gives the full size of the filtered set. The GraphQL API allows 20 requests per second per IP and per access token, and responses carry no rate-limit headers. See [Rate limits](/concepts/rate-limits). > [!NOTE] > > **Do the arithmetic before building a sync job.** 20 records per query at 20 requests per second is a ceiling of **400 transactions per second**, and one request at a time gets well below that. An account with 500,000 lifetime transactions takes 25,000 requests to walk end to end. > > Design for incremental sync: bound every job with `createdGte`/`updatedGte` against your last successful watermark, and never re-walk history you already hold. *** ## Annotating transactions Attach a free-text `memo` (max 255 characters), a `transactionCategory`, and a file to any transaction — either at transaction time on deposits, purchases, and transfers, or afterwards with `updateTransactionMetadata`. Categories are created on first use and reused when the same name comes back. > [!WARNING] > > **`attachmentUrl` is a signed URL that expires. Never store it.** Re-fetch the transaction when you need the file. > > This also breaks naive caching. Settled transactions look immutable, but `memo`, `transactionCategory`, and `attachmentUrl` are all mutable after settlement — so a cached `SETTLED` record will serve stale annotations and a dead attachment link. Cache the financial fields if you like; re-fetch the annotations. → [Add Expense Details](/features/add-expense-details) *** ## Reconciling against your own system Five fields do the joining: | Field | Joins to | | :- | :- | | `reference_id` | The purchase display ID — the short human-readable Fluz transaction ID (e.g. `1047283`) that also appears on gift card records and in exports, and that Fluz support references | | `transfer_id` | The transfer that produced this record, for wallet-to-wallet movements | | `used_user_cash_balance_id` | Which spend account the money came from — essential for per-budget reporting | | `liability_id` | The bill payment this settles | | `connectedAppId` | Which of your applications initiated it, when several share an account | A workable pattern: 1. **Store `record_id` and `reference_id`** against your own order at the time you create it. Don't try to match on amount and timestamp later. 2. **Sync incrementally on `updatedGte`,** not `createdGte` — a `PENDING` transaction that later settles changes `updated_at`, and a created-date sync will miss the transition. 3. **Expect settlement lag.** ACH withdrawals sit `PENDING` for 1–3 business days; card authorizations settle on their own timeline. `expectedClearedDate` tells you when to look again. 4. **Reconcile balances against snapshots,** not by summing amounts. The `*_available_balance` fields are authoritative and already account for fees, cashback, and pending holds. > [!NOTE] > > `externalReferenceId` does **not** appear on transaction records returned by these queries. If you need your own user ID on a movement, join through the account or carry it in `memo` at transaction time. → [Managing External Reference IDs](/managing-external-reference-ids) > > The transaction **webhooks** do carry it, resolved from the OAuth grant between the user and your application — currently in **staging only**, not yet in production. If reconciliation by your own user ID is what you're after, consume `TRANSACTION_CREATE` / `TRANSACTION_UPDATE` rather than polling this feed. → [Webhooks](/fluz-dashboard/webhooks#transaction-created-transaction_create) *** ## Scopes `getTransactions` requires **both** `LIST_PAYMENT` **and** `LIST_PURCHASES`. Missing either returns a `FORBIDDEN` error naming the required scopes. Enable both on your app's Permissions tab before you build — a requested scope that isn't enabled is silently dropped rather than rejected. → [Configure OAuth App](/configure-o-auth-app) *** ## Next steps > **[Get all transactions](/features/get-all-transactions)** > > Full filter, field, and pagination reference. > **[Declined transactions](/features/get-decline-transactions)** > > Authorizations that never became transactions. > **[Decline codes](/features/decline-codes)** > > What each decline reason means. > **[Virtual card transactions](/features/get-virtual-card-transactions)** > > Scoped to one or more cards. > **[Gift card purchases](/features/get-gift-card-purchases)** > > Orders rather than ledger entries. > **[Add expense details](/features/add-expense-details)** > > Memos, categories, and attachments. # Transfer between spend accounts Source: https://docs.fluz.app/features/transfer-between-spend-accounts Move money between two of your own Fluz **spend accounts** — withdrawing from a source spend account and depositing into a destination spend account in a single operation, using the `transferInternalBalance` mutation. > [!NOTE] > > **This transfers between your own spend accounts.** > > A spend account is a cash balance account (the `UserCashBalance` type). This mutation moves funds from one of your spend accounts to another. To send funds to a **different Fluz user**, use [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet) instead. *** ## Transfer Funds Between Spend Accounts To move funds from one spend account to another, use the `transferInternalBalance` mutation. It takes a `TransferInternalBalanceInput` input object identifying the source spend account, the destination spend account, and the amount. ### TransferInternalBalanceInput | Field | Type | Required | Description | | - | - | - | - | | amount | Float! | Yes | The amount to transfer from the source spend account to the destination spend account. | | idempotencyKey | String! | Yes | A unique client-generated string to ensure idempotent request processing. If the same key is submitted multiple times, only the first request is processed. | | sourceUserCashBalanceId | UUID! | Yes | The spend account to withdraw funds **from**. | | destinationUserCashBalanceId | UUID! | Yes | The spend account to deposit funds **into**. | ### Both IDs must be your own spend accounts. Use [Get Spend Accounts](/features/get-spend-accounts) to look up the `userCashBalanceId` for each spend account. The source and destination must be two different spend accounts, and the source must have enough available balance to cover the transfer. ### Sample Request GraphQL ```text theme={null} mutation transferInternalBalance($input: TransferInternalBalanceInput!) { transferInternalBalance(input: $input) { cashBalanceDeposit { cashBalanceDepositId depositDisplayId depositAmount depositFee bankAccountId transactionDate clearedDate status expectedClearedDate cashBalanceDepositType cashBalanceSettlements { cashBalanceSettlementId cashBalanceDepositId availabilityType status } } withdraw { withdrawId amount processingFee chargedFee status displayStatus withdrawSource submissionDate createdAt updatedAt transactionLogId externalTransactionId payoutId emailAddress bankAccountId userCashBalanceId seatId } } } ``` ### Example Input In this example, \$6.00 moves from the source spend account into the destination spend account. JSON ```text theme={null} { "idempotencyKey": "1f1df3e7-5d43-4e3d-83de-31922d4aefb7", "amount": 6.00, "sourceUserCashBalanceId": "6d1b4b19-deef-42f5-80d7-ec34804ce090", "destinationUserCashBalanceId": "bb584e49-d030-4a6e-a5a9-1c34368cbaed" } ``` ### Sample Response *** ## Understanding the Response An internal transfer is recorded as two linked movements: a **withdraw** from the source spend account and a **deposit** into the destination spend account. The response returns both. | Field Name | Type | Description | | - | - | - | | cashBalanceDeposit | CashBalanceDeposit | The deposit made into the destination spend account. | | withdraw | Withdraw | The withdrawal taken from the source spend account. | ### cashBalanceDeposit The `cashBalanceDeposit` object contains details about the deposit into the destination spend account. | Field Name | Type | Description | | - | - | - | | cashBalanceDepositId | UUID! | Unique identifier for the cash balance deposit. | | depositDisplayId | String! | Display identifier for the deposit, intended for user-facing purposes. | | depositAmount | String! | The amount of the deposit. | | depositFee | String | Fee associated with the deposit, if applicable. | | bankCardId | UUID | Identifier of the bank card used for the deposit. | | bankAccountId | UUID | Identifier of the bank account used for the deposit. | | paypalVaultId | UUID | Identifier of the PayPal vault used for the deposit. | | transactionDate | DateTime! | Date and time when the transaction was made. | | clearedDate | DateTime | Date and time when the deposit cleared, if applicable. | | status | CashBalanceDepositStatus! | Current status of the deposit. | | expectedClearedDate | DateTime! | Expected date and time for the deposit to clear. | | cashBalanceDepositType | CashBalanceDepositType! | Type of the cash balance deposit, including CASH\_BALANCE, GIFT\_CARD\_PREPAYMENT, RESERVE\_BALANCE. | | cashBalanceSettlements | \[CashBalanceSettlement] | Settlement time details for the deposit, including status and type. | ### withdraw The `withdraw` object contains details about the withdrawal from the source spend account. | Field Name | Type | Description | | - | - | - | | withdrawId | UUID! | Unique identifier for the withdrawal. | | amount | String! | The amount withdrawn. | | processingFee | String! | Fee charged for processing the withdrawal. | | chargedFee | String! | Fee charged to the user for the withdrawal. | | status | String! | Internal status of the withdrawal. | | displayStatus | String! | User-friendly display status of the withdrawal. | | withdrawSource | WithdrawSource! | The source balance from which funds were withdrawn. | | submissionDate | DateTime! | Date and time when the withdrawal was submitted. | | createdAt | DateTime! | Date and time when the withdrawal was created. | | updatedAt | DateTime! | Date and time when the withdrawal was last updated. | | transactionLogId | UUID | Identifier of the associated transaction log. | | externalTransactionId | String | External transaction identifier from payment gateway (for ACH). | | payoutId | String | Payout identifier from payment gateway (for PayPal/Venmo). | | emailAddress | String | Email address associated with the withdrawal. | | bankAccountId | UUID | Identifier of the bank account used (if applicable). | | userCashBalanceId | UUID | Identifier of the source spend account funds were withdrawn from. | | seatId | UUID | The seat identifier associated with the account. | > [!WARNING] > > **Authorization required** > > This mutation requires the `MAKE_INTERNAL_TRANSFER` scope. Ensure your access token has been granted this scope before attempting an internal transfer between spend accounts. # Transfer to another Fluz account Source: https://docs.fluz.app/features/transfer-to-another-fluz-wallet Move funds between your application's balance and your users' balances, or between two users. > "Requirement: Your application must be a public app." ## Quick Start Send \$10 from a user to your application: ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440000" amount: 10 }) { success message transferId } } ``` ```text theme={null} Authorization: Bearer ``` Send \$10 from your application to a user: ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440001" amount: 10.00 destination: { accountId: "c3d4e5f6-a7b8-9012-cdef-345678901234" } }) { success message transferId } } ``` ```text theme={null} Authorization: Basic ``` *** ## Authentication Transfers support two authentication methods. The method you choose determines who the sender is. | Method | Sender | Use case | | :- | :- | :- | | Bearer token | The user | Collecting payments from users (user → you) or moving funds between user | | Basic Auth | Your application | Disbursing funds to users (you → user) | ### Bearer Token Use a user OAuth token obtained via `generateUserAccessToken` or through the [OAuth refresh flow.](/refresh-o-auth-access-token) The token must include the `MAKE_PAYOUT_TRANSFER_SEND` scope. ### Basic Auth Use your application's Api Key. *** ## Input ### CreateTransferInput | Field | Type | Required | Description | | :- | :- | :- | :- | | idempotencyKey | String! | Yes | A unique UUID you generate to prevent duplicate transfers. Resubmitting the same key returns the original result. | | amount | Float! | Yes | Amount to transfer. Must be positive.. | | destination | TransferDestination | Depends | Who receives the funds. **Required for Basic Auth**. For Bearer token, defaults to your application if omitted. | | bankCardId | UUID | No | Fund the transfer from the user's linked bank card instead of their cash balance. Bearer token only.. | | bankAccountId | UUID | No | Fund the transfer via ACH from the user's bank account. Bearer token only.. | | paypalVaultId | UUID | No | Fund the transfer from the user's linked PayPal. Bearer token only.. | | memo | String | No | Free-text note to attach to this transaction. Max 255 characters. | | transactionCategory | String | No | Category label to attach to this transaction. Free-form — categories are created automatically on first use. | | attachmentId | UUID | No | ID of a previously uploaded file (PDF or PNG) to attach to this transaction. Upload the file first using the transaction memo attachment endpoint. | | useRewardsBalance | Boolean | No | When `true`, fund from the sender's rewards balance in addition to the resolved spend account. Cash is taken first; rewards cover the remainder. Defaults to `false` (cash-only). See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). | > "Only one of `bankCardId`, `bankAccountId`, or `paypalVaultId` can be specified per request. If none is provided, the transfer draws from the sender's spend account (and rewards when `useRewardsBalance` is `true`). > > External funding sources are not available with Basic Auth. Prepayment / gift-card balance is not an input on this mutation." 📘 See [Add Expense Details](/features/add-expense-details) for full details on uploading attachments and working with memos and categories. ### TransferDirection Identify the recipient by account ID or by the external reference ID you assigned when creating the user. Provide one or the other, not both. | Field | Type | Required | Description | | :- | :- | :- | :- | | accountId | UUID | Yes | The recipient's Fluz account ID.. | | externalReferenceId | String | Yes | The external reference ID you assigned to the user in your system.. | | userCashBalanceId | UUID | Depends | Optional. Target a specific cash balance on the recipient's account. If omitted, the system selects the correct one automatically. | > "The recipient must be a registered user of your application." *** ## Response | Field | Type | Description | | :- | :- | :- | | success | Boolean! | Whether the transfer completed. | | message | String! | Human-readable result description. | | transferId | UUID | The transfer's unique ID. Present when `success` is `true`. | *** ## Examples ### Collect payment from a user (Bearer) ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440000" amount: 100.00 }) { success message transferId } } ``` ```text theme={null} Authorization: Bearer ``` No destination needed -- defaults to your application. Optionally include `bankCardId`, `bankAccountId`, or `paypalVaultId` to fund from an external source instead of the user's cash balance. Pass `useRewardsBalance: true` to include the user's rewards balance; see [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). ### Disburse funds to a user (Basic Auth) ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440001" amount: 50.00 destination: { accountId: "c3d4e5f6-a7b8-9012-cdef-345678901234" } }) { success message transferId } } ``` ```text theme={null} Authorization: Basic ``` Use `externalReferenceId` instead of `accountId` to identify the recipient by the ID you assigned. ### Transfer between two users (Bearer) ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440002" amount: 25.00 destination: { accountId: "b2c3d4e5-f6a7-8901-bcde-f23456789012" } }) { success message transferId } } ``` ```text theme={null} Authorization: Bearer ``` ### Fund with rewards (Bearer) ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440010" amount: 25.00 useRewardsBalance: true destination: { accountId: "b2c3d4e5-f6a7-8901-bcde-f23456789012" } }) { success message transferId } } ``` ```text theme={null} Authorization: Bearer ``` Cash is taken from the sender's spend account first. Rewards cover the remainder. See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). ### Success response ```json theme={null} { "data": { "createTransfer": { "success": true, "message": "Transfer created successfully.", "transferId": "7c9e6679-7425-40de-944b-e07fc1f90ae7" } } } ``` *** ## Idempotency Every request must include a unique `idempotencyKey` (a string you generate). If the same key is sent more than once, the API returns the result of the original request without creating a duplicate transfer. Keys are valid for 10 minutes. *** ## Errors Errors follow the standard GraphQL error format: ```json theme={null} { "errors": [ { "message": "Amount must be positive.", "extensions": { "code": "ARG-0001", "name": "InvalidArguments", "status_code": 422 } } ] } ``` ### Common Error | Error | Cause | | :- | :- | | Missing idempotencyKey or amount | Required fields not provided. | | Amount must be positive | Amount is zero or negative. | | Destination is required when using Basic auth | Basic Auth requests must specify a destination. | | Funding sources are not supported when using Basic auth | Remove `bankCardId` / `bankAccountId` / `paypalVaultId` from Basic Auth requests. | | Only one funding source is allowed at a time | Multiple funding sources were provided. Pass only one. | | Provide either accountId or externalReferenceId, not both | Destination has both identifiers. Use one. | | Destination account has not authorized this application | The recipient is not a registered user of your application. | | No user found with this external reference ID | The `externalReferenceId` doesn't match any user in your application. | | User does not have a cash balance configured | The sender or recipient doesn't have a cash balance on the required sponsor bank. | | Basic auth is not supported for personal applications | Upgrade your application to `ACTIVE` status to use Basic Auth transfers. | | Insufficient balance (`WDR-0001`) | The sender cannot cover `amount` from spend account, or from spend account plus rewards when `useRewardsBalance` is `true`. | # Unlock virtual card Source: https://docs.fluz.app/features/unlock-virtual-card The `unlockVirtualCard` mutation allows you to unlock a previously locked virtual card, restoring its ability to process transactions. > [!NOTE] > > **Prerequisites:** a user access token with the `EDIT_VIRTUALCARD` scope, and the `virtualCardId` of the card to unlock. ## Arguments * **`input`** (`UnlockVirtualCardInput!`): The input object containing the ID of the virtual card to unlock. ## UnlockVirtualCardInput fields | Field | Type | Description | Required | | - | - | - | - | | `virtualCardId` | `UUID!` | The virtual card to unlock. | Yes | ## Sample mutation ```graphql theme={null} mutation { unlockVirtualCard(input: { virtualCardId: "07df5653-43a8-4532-9881-3ab5857bbe11" }) { virtualCardId unlocked } } ``` ## cURL example ```bash theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation { unlockVirtualCard(input: { virtualCardId: \"07df5653-43a8-4532-9881-3ab5857bbe11\" }) { virtualCardId unlocked } }" }' ``` ## Sample response ```json theme={null} { "data": { "unlockVirtualCard": { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "unlocked": true } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `virtualCardId` | `UUID` | The unique identifier of the virtual card that was targeted for unlocking. | | `unlocked` | `Boolean` | Indicates whether the card was successfully unlocked. | ## Next steps > **[Add a billing address](/features/billing-address)** > > Pre-save and manage the billing addresses your cards are issued against. > **[Get virtual card transactions](/features/get-virtual-card-transactions)** > > Confirm spend resumed by pulling the card's transaction history. # View funding sources Source: https://docs.fluz.app/features/view-funding-sources Once you've added funding sources to your Fluz account, you can retrieve a list of them with the `getWallet`[query](/api-reference/queries/get-wallet). This query will provide details of all funding sources linked to the user's wallet. ## Sample request ```graphql theme={null} query getWallet { getWallet { bankCards { bankCardId cardType cardholderName lastFourDigits expirationMonth expirationYear cardStatus billingAddressId } bankAccounts { bankAccountId type accountName status achRouting lastFour nickname } paypalAccounts { paypalVaultId status email } blockedPaymentTypes balances { rewardsBalance { availableBalance totalBalance lifetimeBalance } cashBalance { availableBalance totalBalance pendingBalance lifetimeBalance } giftCardCashBalance { availableBalance totalBalance pendingBalance lifetimeBalance } } } } ``` The query response will include user balances and funding source types such as `BankCard`,` BankAccount`and `PayPal`. Here is an example of the response object: ## Sample response ```json theme={null} { "data": { "getWallet": { "bankCards": [ { "bankCardId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "ownerAccountId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "addedUserId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "cardType": "DEBIT", "cardProcessor": "xyz789", "cardholderName": "John Doe", "lastFourDigits": "1234", "expirationMonth": "12", "expirationYear": "25", "cardStatus": "ACTIVE", "billingAddressId": "e7979eb7-1727-48ed-8c5a-c56532908c1e" } ], "bankAccounts": [ { "bankAccountId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "accountId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "type": "SAVING", "accountName": "John Doe", "status": "ENABLED", "achRouting": "021000021", "lastFour": "1234", "authChargeBackupPaymentMethod": true, "billingAddressId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "nickname": "Johnny" } ], "paypalAccounts": [ { "paypalVaultId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "userId": "e7979eb7-1727-48ed-8c5a-c56532908c1e", "status": "ACTIVE", "email": "abc123@example.com" } ], "blockedPaymentTypes": ["BANK_CARD"], "balances": { "rewardsBalance": "100.00", "cashBalance": "250.00", "giftCardCashBalance": "50.00" } } } } ``` ## Response Fields Explained ### bankCards - BankCardType: The `BankCard` object represents a bank card that has been added by a user to their account. Each object within this array represents a single bank card, including details like the card number (masked), expiration date, cardholder name, and billing information. | Field name | Type | Description | | :- | :- | :- | | bankCardId | UUID! | The unique identifier for the bank card. | | ownerAccountId | UUID! | The unique identifier of the account that owns this bank card. | | addedUserId | UUID! | The unique identifier of the user who added the bank card to the account. | | cardType | BankCardType | The type of the card, such as DEBIT or CREDIT. | | cardProcessor | String | The name or code of the card processor. | | cardholderName | String | The name of the cardholder as it appears on the bank card. | | lastFourDigits | String | The last four digits of the bank card number, used for identification. | | expirationMonth | String | The month the card expires, formatted as MM. | | expirationYear | String | The year the card expires, formatted as YYYY. | | cardStatus | BankCardStatus | The current status of the bank card, such as ACTIVE, INACTIVE, or BLOCKED. | | billingAddressId | UUID | The unique identifier for the billing address associated with this bank card. | ### bankAccounts - \[BankAccount]: The `BankAccount` object is a record of a bank account that a user has added to their account. It stores key information about the bank account, including the account's type, status, details, and associated billing address. | Field name | Type | Description | | :- | :- | :- | | bankAcountid | UUID! | The unique identifier for the bank account. | | accountId | UUID! | The unique identifier of the account associated with this bank account. | | type | BankAccountType! | The type of bank account, such as CHECKING or SAVING. | | accountName | String! | The name associated with the bank account. | | status | String! | The current status of the bank account, such as ENABLED, DISABLED. | | achRouting | String! | The ACH (Automated Clearing House) routing number for the bank account. | | lastFour | String! | The last four digits of the bank account number, used for identification. | | authChargeBackupPaymentMethod | Boolean! | Indicates whether this bank account is set as a backup payment method for charges. | | billingAddressId | UUID! | The unique identifier for the billing address associated with this bank account. | | nickname | String | An optional nickname given to the bank account for easier identification. | ### paypalAccounts - \[Paypal]: The `Paypal` object represents a PayPal account that a user has added to their account. This object contains essential details about the PayPal account, including its unique identifier, associated user, status, and email address. | Field name | Type | Description | | :- | :- | :- | | paypalVaultId | UUID! | The unique identifier for the PayPal account. | | userId | UUID! | The unique identifier of the user who added the PayPal account. | | status | PayPalAccountStatus! | The current status of the PayPal account, such as ACTIVE, INACTIVE, etc. | | email | String! | The email address associated with the PayPal account. | ### blockedPaymentTypes - \[PaymentMethodType]: **Description:** This array lists payment types that are currently blocked for the user.\ **Details:** If a payment type is blocked, it means the user cannot use this method for transactions on the platform. For example, "BANK\_CARD" indicates that the use of bank cards is currently restricted. **PaymentMethodType Enum values:** * **`"BANK_CARD"`**: Indicates that bank cards are blocked. * **`"BANK_ACCOUNT"`**: Indicates that bank accounts are blocked. * **`"PAYPAL"`**: Indicates that PayPal is blocked. * **`"FLUZPAY"`**: Indicated that your Fluz balance is blocked. * **`"APPLE_PAY"`**: Indicated that your Apple Pay wallet is blocked. * **`"GOOGLE_PAY"`**: Indicated that your Google Pay wallet is blocked. ### balances - UserBalances: **Description:** This object contains the user's current balances, including Cash Balance, Rewards Balance, and Fluz prepayment balance. To learn more, you can view ['Check Account Balances'](/check-account-balance). # Virtual account documents Source: https://docs.fluz.app/features/virtual-account-documents Generate payment instructions, paycheck direct-deposit forms, and account status letters as PDFs for a spend account's virtual account number. Three queries return ready-to-share PDF documents for a spend account's [virtual account number](/features/virtual-account-numbers). Each one is generated on demand, returned as a **base64-encoded string**, and is meant to be decoded and either rendered in your UI or offered as a download. All three: * Require the `LIST_PAYMENT` scope. * Take a `userCashBalanceId` identifying the spend account. * Take an optional `virtualAccountNumberId`. **When omitted, the spend account's primary VAN is used.** * Return `SpendAccountPdfDocument!`. *** ## Decoding the Response ```graphql theme={null} query getSpendAccountPaymentInstructions($userCashBalanceId: UUID!) { getSpendAccountPaymentInstructions(userCashBalanceId: $userCashBalanceId) { fileName pdfBase64 } } ``` ```javascript theme={null} const { fileName, pdfBase64 } = data.getSpendAccountPaymentInstructions; // Browser: turn the base64 payload into a downloadable file const bytes = Uint8Array.from(atob(pdfBase64), (c) => c.charCodeAt(0)); const blob = new Blob([bytes], { type: "application/pdf" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); ``` > [!WARNING] > > These PDFs contain the full, unmasked routing and account number. Do not cache them, log them, or store them outside the user's session. Regenerate on demand instead. *** ## Payment Instructions `getSpendAccountPaymentInstructions` produces a PDF containing the routing and account details needed to fund the virtual account number. Use it when a user needs to tell a customer, vendor, or their own outside bank where to send money. ```graphql theme={null} query paymentInstructions( $userCashBalanceId: UUID! $virtualAccountNumberId: UUID ) { getSpendAccountPaymentInstructions( userCashBalanceId: $userCashBalanceId virtualAccountNumberId: $virtualAccountNumberId ) { fileName pdfBase64 } } ``` | Argument | Type | Required | Description | | - | - | - | - | | `userCashBalanceId` | `UUID!` | Yes | The spend account the funds should land in. | | `virtualAccountNumberId` | `UUID` | No | Specific VAN to document. Defaults to the account's primary VAN. | [API reference](/api-reference/queries/get-spend-account-payment-instructions) *** ## Paycheck Deposit Form `getSpendAccountPaycheckDepositForm` produces a pre-filled direct-deposit authorization form a user can submit to their employer's payroll department. This is the intended path for routing all or part of a paycheck into a Fluz spend account. ```graphql theme={null} query paycheckDepositForm( $userCashBalanceId: UUID! $virtualAccountNumberId: UUID $depositType: PaycheckDepositType! $depositAmount: Float $depositPercentage: Float ) { getSpendAccountPaycheckDepositForm( userCashBalanceId: $userCashBalanceId virtualAccountNumberId: $virtualAccountNumberId depositType: $depositType depositAmount: $depositAmount depositPercentage: $depositPercentage ) { fileName pdfBase64 } } ``` | Argument | Type | Required | Description | | - | - | - | - | | `userCashBalanceId` | `UUID!` | Yes | The spend account the paycheck should land in. | | `virtualAccountNumberId` | `UUID` | No | Defaults to the account's primary VAN. | | `depositType` | `PaycheckDepositType!` | Yes | How much of each paycheck the employer should send: `FULL` for the entire check, `FIXED` for a set dollar amount, or `PERCENTAGE` for a share of it. | | `depositAmount` | `Float` | Conditional | **Required when `depositType` is `FIXED`.** | | `depositPercentage` | `Float` | Conditional | **Required when `depositType` is `PERCENTAGE`.** Value from 1–100. | > [!WARNING] > > **Validation is enforced server-side.** Sending `depositType: FIXED` without `depositAmount`, or `depositType: PERCENTAGE` without a `depositPercentage` between 1 and 100, returns an error. Validate in your UI before calling. ```json Variables theme={null} { "userCashBalanceId": "9c1f6b2e-4d7a-4c3b-9f11-2a5e8b0d6c74", "depositType": "PERCENTAGE", "depositPercentage": 25 } ``` [API reference](/api-reference/queries/get-spend-account-paycheck-deposit-form) *** ## Account Status Letter `getSpendAccountStatusLetter` produces a letter confirming the account exists and is in good standing — the equivalent of a bank letter. Set `displayBalance` to `true` to include the current balance on the letter; leave it off when the user only needs to prove the account exists. ```graphql theme={null} query statusLetter( $userCashBalanceId: UUID! $virtualAccountNumberId: UUID $displayBalance: Boolean ) { getSpendAccountStatusLetter( userCashBalanceId: $userCashBalanceId virtualAccountNumberId: $virtualAccountNumberId displayBalance: $displayBalance ) { fileName pdfBase64 } } ``` | Argument | Type | Required | Description | | - | - | - | - | | `userCashBalanceId` | `UUID!` | Yes | The spend account to document. | | `virtualAccountNumberId` | `UUID` | No | Defaults to the account's primary VAN. | | `displayBalance` | `Boolean` | No | Include the account's balance on the letter. Default off. | [API reference](/api-reference/queries/get-spend-account-status-letter) *** ## Choosing the Right Document ```mermaid theme={null} flowchart TD Q{"Who is the user\ngiving this to?"} Q -->|Their employer| A["Paycheck Deposit Form\ngetSpendAccountPaycheckDepositForm"] Q -->|A customer, vendor,\nor outside bank| B["Payment Instructions\ngetSpendAccountPaymentInstructions"] Q -->|A landlord, lender,\nor auditor| C["Account Status Letter\ngetSpendAccountStatusLetter"] ``` *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Virtual account numbers Source: https://docs.fluz.app/features/virtual-account-numbers Give each spend account a real routing and account number so external senders can push funds in over RTP, FedNow, Wire, or ACH. A **virtual account number (VAN)** is a real, bank-issued routing number and account number that points at one of a user's [spend accounts](/features/spend-accounts). Anyone outside Fluz — an employer, a bank, a customer, a marketplace — can send money to that routing and account pair, and the funds land in the spend account as a deposit. The VAN does not hold money. The **spend account holds the balance.** A VAN is simply another address that routes into it. > [!NOTE] > > **One balance, many addresses — eventually.** > > A spend account credits a single balance no matter which address the money arrives at. The VAN data model is built to support more than one active virtual account number per spend account, with exactly one marked **primary**, so that senders could eventually be separated — payroll versus a specific customer — without splitting the funds. > > Today, every spend account is provisioned with exactly one VAN, and it is always the primary. There is no API, dashboard, or account-manager path to a second one yet. See [Multiple Virtual Account Numbers](#multiple-virtual-account-numbers). *** ## How It Fits Together ```mermaid theme={null} flowchart LR subgraph EXT["External Senders"] E1[Employer payroll] E2[User's outside bank] E3[Customer or partner] end subgraph VANS["Virtual Account Numbers"] V1["VAN — primary\nrouting + account"] V2["VAN — secondary\nrouting + account"] end SA[("Spend Account\nUserCashBalance\nholds the balance")] OUT["Gift cards · Virtual cards\nInternal transfers · Withdrawals"] E1 -->|ACH credit| V1 E2 -->|RTP / FedNow| V1 E3 -->|Wire| V2 V1 --> SA V2 --> SA SA --> OUT ``` Money in arrives through a VAN. Money out still leaves the spend account the same way it always has — gift card and virtual card funding, internal transfers, and withdrawals to a linked external account. *** ## Provisioning Virtual account numbers are provisioned by Fluz. There is no mutation to create one, and no control in the dashboard to request one. Provisioning happens on Fluz's side once a spend account becomes eligible; your integration's job is to read the result, not to trigger it. ### Eligibility A spend account is eligible for a virtual account number when all of the following are true: | Condition | Detail | | - | - | | **Program entitlement** | VANs are enabled on your program at the sponsor bank. This is a per-program setting agreed during onboarding, not a per-user one. If VANs are not enabled on your program, no spend account under it will receive one. Today VANs are only available for Spend Accounts sponsored by Austin Capital Bank (ACB). | | **Spend account status** | The spend account exists and is active. | | **Identity verification** | The underlying accountholder has cleared CIP — KYC for a consumer, KYB plus beneficial owner verification for a business. A spend account that exists but is pending or under manual review will not carry a VAN. | | **Accountholder standing** | The accountholder is not frozen, restricted, or closed. | > [!WARNING] > > **Spend account creation is not the trigger.** > > An eligible spend account and a provisioned VAN are two different states. A newly created spend account is usable for gift cards, virtual cards, and internal transfers immediately, but it may not have a VAN yet. Surfacing deposit instructions off the back of spend account creation will show your users an empty screen. ### Trigger and timing Provisioning is **automatic and asynchronous.** Fluz initiates it once the conditions above are met; the VAN is issued by the sponsor bank and written back to the spend account when the bank returns it. The VAN is not available in the same request that creates the spend account. Treat availability as eventual, and read the VAN at the point the user needs it rather than at account creation. Until the VAN is written back, `getSpendAccountVirtualAccountNumbers` returns an **empty array**. It does not error, and it does not return `null`. ```json Pending — VAN not yet provisioned theme={null} { "data": { "getSpendAccountVirtualAccountNumbers": [] } } ``` An empty array is ambiguous on its own: it means either *not yet provisioned* or *program not entitled*. Resolve it against the spend account's own status rather than retrying indefinitely. ### Recommended integration pattern **1. Create the spend account** Proceed with your normal flow. Do not block the user on VAN availability. **2. Read the VAN lazily** Call `getSpendAccountVirtualAccountNumbers` when the user navigates to a deposit, direct deposit, or account details screen — not eagerly at account creation. **3. Handle the empty case explicitly** Render a "deposit details are being set up" state rather than an error or a blank field. Re-check on next view. **4. Cache by ID, not by position** Store `virtualAccountNumberId` as the stable key. Never key off array index. ### API and dashboard behave the same way Both surfaces read the same underlying object, and neither can create, modify, or remove one. | Operation | API | Dashboard | | - | - | - | | **Create a VAN** | Not available | Not available | | **Read VANs** | `getSpendAccountVirtualAccountNumbers` | Spend account detail view | | **Rename, set primary, deactivate** | Not available | Not available | | **Generate deposit artifacts** | `getSpendAccountPaymentInstructions`, `getSpendAccountPaycheckDepositForm`, `getSpendAccountStatusLetter` | — | If the API returns an empty array for a spend account, the dashboard will show no deposit details for that same account. There is no dashboard-only path to a VAN. *** ## Supported Rails Credits sent to a virtual account number are accepted over four rails: | Rail | Direction | Typical availability | Notes | | - | - | - | - | | **RTP** | Credit (in) | Near-instant, 24/7 | Sender's bank must participate in The Clearing House RTP. | | **FedNow** | Credit (in) | Near-instant, 24/7 | Sender's bank must be a FedNow participant. | | **Wire** | Credit (in) | Same business day | Domestic wires. Sending bank fees are set by the sender. | | **ACH** | Credit (in) | 1–2 business days | Standard ACH credit, including payroll direct deposit. | > [!WARNING] > > **ACH debit (pull) is not supported yet.** > > A virtual account number can currently only **receive** funds. You cannot use the VAN's routing and account number to originate an ACH debit — a third party cannot pull money out of a spend account using these credentials. Support for ACH debit is **coming soon**. > > To move money out today, use [Withdraw Funds to an External Account](/features/withdraw-to-external-account) or an [internal transfer](/features/transfer-between-spend-accounts). *** ## What a Credit Looks Like When funds arrive at a virtual account number, Fluz records a standard **deposit** against the destination spend account. The deposit's **funding source is the virtual account** — not a bank card, bank account, or PayPal — because the money originated outside Fluz and was pushed in rather than pulled from a linked payment method. This means: * The credit appears in the same transaction and deposit history as any other deposit. * There is no linked funding source object to reconcile against, and no backup card hold, because nothing was debited from the user. * The deposit is attributable to the specific VAN that received it, so you can distinguish a payroll credit from a customer payment when a spend account has multiple VANs. ```mermaid theme={null} sequenceDiagram participant S as External sender participant B as Receiving bank participant F as Fluz participant SA as Spend account S->>B: Push funds to VAN routing + account B->>F: Credit received (RTP / FedNow / Wire / ACH) F->>F: Match VAN to spend account F->>SA: Post deposit (funding source: virtual account) F-->>S: Funds available to spend ``` *** ## Retrieve a User's Virtual Account Numbers Use `getSpendAccountVirtualAccountNumbers` to list the active VANs on a spend account. It returns every active VAN, one of which is flagged as primary. **Scope required:** `LIST_PAYMENT` ```graphql theme={null} query getSpendAccountVirtualAccountNumbers($userCashBalanceId: UUID!) { getSpendAccountVirtualAccountNumbers(userCashBalanceId: $userCashBalanceId) { virtualAccountNumberId achRoutingNumber accountNumber last4 nickname isPrimary status createdAt } } ``` ```json Variables theme={null} { "userCashBalanceId": "9c1f6b2e-4d7a-4c3b-9f11-2a5e8b0d6c74" } ``` | Argument | Type | Required | Description | | - | - | - | - | | `userCashBalanceId` | `UUID!` | Yes | The spend account whose virtual account numbers you want. | Returns `[SpendAccountVirtualAccountNumber!]!`. See the [type reference](/api-reference/types/spend-account-virtual-account-number) for the full field list. > [!NOTE] > > **Displaying account numbers.** > > A VAN's full account number is sensitive. Mask it in list views and reveal the full value only on an explicit user action, the same way you would treat a card PAN. When a user needs to hand the details to a third party, prefer the generated PDF artifacts described in [Virtual Account Documents](/features/virtual-account-documents) over free-form copy. *** ## Multiple Virtual Account Numbers The query returns a list, and the type carries an `isPrimary` flag, because the underlying VAN data model is built to support more than one active virtual account number per spend account — for example, a dedicated payroll address and a per-customer address, all crediting the same balance. **This is not available today.** Every spend account is currently limited to exactly one VAN, and there is no API mutation, dashboard flow, or account-manager request that creates, views, or manages a second one. For Austin Capital Bank (Fluz's current VAN sponsor), the one-VAN-per-spend-account cap is a global setting on ACB's side, not a per-program one — so it isn't something Fluz can configure or enable for an individual integration. > [!NOTE] > > **Read-only today, for every program.** > > The read query above (`getSpendAccountVirtualAccountNumbers`) is the only VAN operation exposed, to every integration, with no exceptions. Support for additional VANs is a future capability, not something you can request or turn on today. ### Build for the list anyway Every spend account has exactly one VAN today, but code against the list shape rather than a single object. The API is already structured this way so that support for additional VANs, if it ships later, won't be a breaking change for you. * **Never index into the array.** Do not read `result[0]`. Select the VAN where `isPrimary` is `true`. * **Handle `0` explicitly** (see [Provisioning](#provisioning)), even though the non-zero case today is always length `1`. * **Pass `virtualAccountNumberId` explicitly** to the document queries rather than relying on the primary-VAN default, so your integration won't need a code change if additional VANs are supported down the line. *** ## Handing the Details to a Third Party Rather than asking a user to transcribe a routing and account number into a payroll portal or send it to a counterparty by email, Fluz generates PDF artifacts on demand: | Document | Query | Use case | | - | - | - | | **Payment instructions** | `getSpendAccountPaymentInstructions` | Give a customer or partner the details needed to send a wire or ACH credit. | | **Paycheck deposit form** | `getSpendAccountPaycheckDepositForm` | Pre-filled direct deposit form for an employer's payroll department. | | **Account status letter** | `getSpendAccountStatusLetter` | Proof of account, optionally showing the current balance. | All three default to the spend account's **primary** VAN when `virtualAccountNumberId` is omitted, and all three require the `LIST_PAYMENT` scope. See [Virtual Account Documents](/features/virtual-account-documents) for the full reference. *** ## Requirements * The user must have at least one **active spend account** whose accountholder has cleared CIP. * Your program must be **entitled to VANs** at the sponsor bank. This is agreed during onboarding and is not something the API can enable. * Reading virtual account numbers and generating documents both require the `LIST_PAYMENT` scope on the user access token. * Virtual account numbers are **provisioned by Fluz, asynchronously, and cannot be created, modified, or deleted** through the API or the dashboard. See [Provisioning](#provisioning). * Assume a spend account may have **zero or one** VAN at read time today. Code against the list shape (don't index into it) since additional VANs may be supported in the future. *** ## Related > **[Spend Accounts](/features/spend-accounts)** > > The account that actually holds the balance. > **[Virtual Account Documents](/features/virtual-account-documents)** > > Payment instructions, deposit forms, and status letters. > **[Deposit Funds](/features/deposit-from-external-accounts)** > > Pull funds in from a linked bank account or card. > **[Funding Sources](/features/funding-sources)** > > How Fluz classifies the money moving in. *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Virtual card error codes Source: https://docs.fluz.app/features/virtual-card-error-codes Every virtual card operation surfaces failures through a `VC-` prefixed code on `extensions.code`. Use this reference to branch on the specific failure rather than the human-readable message. | Error | Code | Message | | :- | :- | :- | | UNABLE\_TO\_CREATE | VC-0001 | Please try another payment method. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_MERCHANT\_OFFERS | VC-0002 | We are unable to load virtual card merchant offers. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_CANCEL | VC-0008 | We are unable to cancel your virtual card. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_REVEAL | VC-0009 | We are unable to reveal your virtual card. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_VIRTUAL\_CARDS | VC-0013 | We are unable to get your virtual card history. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_VIRTUAL\_CARD\_BY\_ID | VC-0014 | We are unable to get virtual card by ID. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_VIRTUAL\_CARD\_TRANSACTIONS | VC-0015 | We are unable to get your virtual card transaction history. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_VIRTUAL\_CARD\_PROGRAM\_LIMITS | VC-0016 | We are unable to get your virtual card program limits. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_VIRTUAL\_CARD\_VENDOR\_BALANCE | VC-0017 | We are unable to get the balance of your virtual card's vendor. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_GET\_BIN | VC-0018 | This BIN does not exist, please try another BIN. | | UNABLE\_TO\_GET\_VIRTUAL\_CARD\_OFFER | VC-0019 | We are unable to get the virtual card offer. If you continue experiencing issues, please contact our support team. | > [!NOTE] > > Address verification failures on card creation return `VC-0025` (`UnableToCreateAuthUser`), which isn't in the table above. See [Address Formatting Requirements](/concepts/address-formatting-requirements) for how to avoid it. ## Next steps > **[Decline codes](/features/decline-codes)** > > Why an individual card transaction was declined at authorization time. > **[General API error codes](/get-started/general-api-error-codes)** > > Auth, argument, and system errors shared across every endpoint. # Overview Source: https://docs.fluz.app/features/virtual-cards Network-branded virtual cards issued instantly over the API — card types, lifecycle, the scopes each operation needs, funding, spend controls, and how to reveal card details. **Pre-Funded Virtual Cards** are network-branded (Visa / Mastercard) cards issued instantly via the Fluz API. They earn cashback at every merchant that accepts the network — and can be layered with gift-card routing at 400+ brands for even higher rewards. ## What Is a Fluz Virtual Card? A Fluz **Pre-Funded Virtual Card** is a network-branded, instantly issued card that lives entirely in software. Unlike a physical card: * **No plastic, no shipping** — the card is available the moment `createVirtualCard` resolves. * **Pre-funded** — the spend limit is reserved at creation time from a Fluz Wallet, gift-card balance, rewards balance, or a linked bank account. * **Scoped** — each card has its own spend limit, duration, lock date, and optional single-use flag, so you control exactly how much can be charged and for how long. * **Full network acceptance** — issued on Mastercard (debit or prepaid) or Visa rails, usable anywhere those networks are accepted online or in-store. Cards earn a base **cashback** on all spend via interchange economics. At supported merchants, card-linked offers can layer on additional returns. ## Card Types | Type | Description | | - | - | | **Standard Virtual Card** | Works at all merchants on the card network | | **Brand-Locked Virtual Card** | Restricted to a specific merchant — useful for targeted incentive programs or spend controls | Use [`getVirtualCardOffers`](/features/get-card-offers) to enumerate all programs available to your account, including network type (Mastercard / Visa), card type (debit / prepaid), issuing bank, and per-program spend limits. ## Card Lifecycle A card moves through the following states: ```mermaid theme={null} stateDiagram-v2 [*] --> ACTIVE: createVirtualCard ACTIVE --> LOCKED: lockVirtualCard LOCKED --> ACTIVE: unlockVirtualCard ACTIVE --> CLOSED_EXPIRED: lockDate reached, or manually cancelled CLOSED_EXPIRED --> [*] ``` A card set with `lockCardNextUse: true` transitions automatically from `ACTIVE` to `LOCKED` after its first successful authorization — no additional API call required. See [Edit Virtual Card](/features/edit-virtual-card) for how to modify this setting post-creation. `LOCKED` is not a value of `status`: a locked card still reports `status: ACTIVE`. Read `locked` from [Get Virtual Card Balance](/recipes/get-virtual-card-balance) to tell whether a card can spend. ## Required Scopes All virtual card operations require a **User Access Token** generated with the appropriate scopes. The three core scopes are: | Scope | Used By | | - | - | | `CREATE_VIRTUALCARD` | Create cards, fetch offers, check balances, get transactions | | `REVEAL_VIRTUALCARD` | Reveal PAN, CVV, and expiry; also required for `getVirtualCardBalance`. Returning raw card data also requires `PCI_COMPLIANCE` on your application — see [Revealing card details](#revealing-card-details) | | `EDIT_VIRTUALCARD` | Edit, lock, unlock, set PIN | Generate a token with all three scopes before calling any virtual card endpoint: ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "query": "mutation generateUserAccessToken($userId: UUID!, $accountId: UUID!, $scopes: [ScopeType!]!) { generateUserAccessToken(userId: $userId, accountId: $accountId, scopes: $scopes) { token scopes } }", "variables": { "userId": "", "accountId": "", "scopes": ["CREATE_VIRTUALCARD", "REVEAL_VIRTUALCARD", "EDIT_VIRTUALCARD"] } }' ``` Pass the returned `token` as `Authorization: Bearer ` on all subsequent calls. See [Refresh an Expired User Access Token](/get-started/refresh-expired-access-token) when you receive a `401`. ### Revealing card details There are two ways to get a card's PAN, expiry, and CVV in front of a user, and they have very different compliance footprints. | Path | Where the data is readable | What it takes | | :- | :- | :- | | [Secure Elements Card Reveal](/build-a-platform/card-reveal) | A Fluz-hosted frame, mounted inline in your own page | A client token minted from your existing access token. No `PCI_COMPLIANCE` grant — your servers and your page's JavaScript never see the data | | [`revealVirtualCardByVirtualCardId`](/recipes/reveal-virtual-card) | Your own server, in the API response | `REVEAL_VIRTUALCARD` + `PCI_COMPLIANCE`. Granted on request on your own account; requires a PCI DSS certification on accounts belonging to other people | Reach for the API mutation when you genuinely need the numbers server-side — pushing them to another processor, for example. If a human is going to read them off a screen, Secure Elements is the shorter path. ## How Funding Works When a card is charged, Fluz draws funds in this priority order by default: 1. The designated **spend account** (`userCashBalanceId`) 2. **Prepaid (gift card) balance** — unless `usePrepaymentBalance: false` 3. **Rewards balance** — unless `useRewardsBalance: false` You can also fund directly from a **linked bank account** by setting `primaryFundingSource: BANK_ACCOUNT`. Full input details and examples are on [Create Virtual Card](/features/create-card). ## Spend Limit Durations The `spendLimit` you set applies per the chosen `spendLimitDuration`: | Duration | Behavior | | - | - | | `LIFETIME` | Applies across the card's entire lifespan (default) | | `DAILY` | Resets each calendar day | | `WEEKLY` | Resets weekly | | `MONTHLY` | Resets on the first of each month | You are **only charged for the amount actually spent** — the spend limit is an authorization ceiling, not a pre-charge. Unused balance stays in your wallet. ## Single-Use Cards Set `lockCardNextUse: true` at creation (or update it via `editVirtualCard`) to lock the card automatically after its first authorization. This is the recommended pattern for: * One-time vendor payments * Single-transaction disbursements * Agentic payment flows where a card should be consumed after one use ## All Virtual Card Operations | Operation | Type | Scope(s) | Page | | - | - | - | - | | Discover available programs | Query | `CREATE_VIRTUALCARD` | [Get Virtual Card Offers](/features/get-card-offers) | | Pre-save a billing address | Mutation | `CREATE_VIRTUALCARD` | [Add Virtual Card Address](/features/add-billing-address) | | Issue a card | Mutation | `CREATE_VIRTUALCARD` | [Create Virtual Card](/features/create-card) | | Retrieve PAN, CVV, expiry (server-side) | Mutation | `REVEAL_VIRTUALCARD` + `PCI_COMPLIANCE` | [Reveal Virtual Card](/recipes/reveal-virtual-card) | | Show PAN, CVV, expiry to a user | SDK | — | [Secure Elements Card Reveal](/build-a-platform/card-reveal) | | Update limit, nickname, lock date | Mutation | `EDIT_VIRTUALCARD` | [Edit Virtual Card](/features/edit-virtual-card) | | Temporarily block spend | Mutation | `EDIT_VIRTUALCARD` | [Lock Virtual Card](/features/lock-virtual-card) | | Re-enable a locked card | Mutation | `EDIT_VIRTUALCARD` | [Unlock Virtual Card](/features/unlock-virtual-card) | | Check balance, status and lock state (batch) | Query | `REVEAL_VIRTUALCARD` + `PCI_COMPLIANCE` | [Get Virtual Card Balance](/recipes/get-virtual-card-balance) | | View transaction history | Query | `CREATE_VIRTUALCARD` | [Get Virtual Card Transactions](/features/get-virtual-card-transactions) | | Issue cards in bulk (async) | Mutation | `CREATE_VIRTUALCARD` | [Bulk Operations](/features/create-bulk-order) | | Distribute cards via hosted link | Mutation | `CREATE_SHARE_LINK` | [Send Hosted Links With Virtual Cards](/features/send-cards) | | Set a PIN | Mutation | `EDIT_VIRTUALCARD` | [Set Virtual Card PIN](/set-virtual-card-pin) | | Push to Apple Pay / Google Pay | Mutation | — | [Digital Wallet Push Provisioning](/digital-wallet-push-provisioning) | | Apply merchant-specific offers | — | — | [Card Linked Offers](/features/card-linked-offers) | | Error reference | — | — | [Virtual Card Error Codes](/features/virtual-card-error-codes) | ## Next steps > **[Create your first virtual card](/features/create-card)** > > The full `createVirtualCard` contract — inputs, funding options, spend controls, and examples. # Wallets & transfers Source: https://docs.fluz.app/features/wallets-overview How money is held and moved on Fluz — balance types, spend accounts, funding in from external sources or virtual account numbers, and reading the ledger. Every dollar on Fluz sits in a **balance**. Money enters a balance from an external funding source or a virtual account number, moves between balances through transfers, and leaves through purchases or withdrawals. This section covers all of it. The most useful thing to understand first: **an account does not have one balance. It has several, and they behave differently.** Some are withdrawable, some are not, and one of them — the spend account — can exist many times over. *** ## The Money Map ```mermaid theme={null} flowchart LR subgraph IN["Money In"] F1["Bank account (ACH)"] F2["Bank card"] F3["PayPal / Apple Pay"] F4["Virtual account number\nRTP · FedNow · Wire · ACH"] F5["Fluz gift card redemption"] F6["Cashback earned"] end subgraph BAL["Balances"] SA1["Spend Account\n'Operations'"] SA2["Spend Account\n'Team Travel'"] RW["Rewards Balance"] GC["Gift Card Balance\nnon-withdrawable"] RS["Reserve Balance\nnon-withdrawable"] end subgraph OUT["Money Out"] O1["Gift card purchases"] O2["Virtual card funding"] O3["Transfers to other\nFluz accounts"] O4["Withdrawals to\nexternal accounts"] end F1 & F2 & F3 --> SA1 F4 --> SA1 F4 --> SA2 F5 --> GC F6 --> RW SA1 & SA2 --> O1 & O2 & O3 & O4 RW --> O1 & O4 GC --> O1 & O2 RS -.->|covers failed settlement| O1 ``` *** ## Balance Types An account can hold up to four kinds of balance. Spend accounts are the only kind a user can have more than one of. | Balance | Withdrawable | What it holds | | - | - | - | | **Spend account** (cash balance) | Yes | The main working balance. Funds gift cards, virtual cards, transfers, and withdrawals. | | **Rewards balance** | Yes | Cashback and bonus rewards earned on Fluz activity. | | **Gift card balance** | No | Prepaid value usable toward gift card and virtual card purchases only. | | **Reserve balance** | No | Held by Fluz to cover transactions that fail to settle, keeping the account in good standing. | The sum of these is the account's **available Fluz balance** — the total that can be applied toward funding a payment. > [!NOTE] > > **The same balance appears under more than one name.** > > The gift card balance is returned as `giftCardCashBalance` under `getWallet` balances and as `giftCardPrepaymentBalance*` on the `Transaction` type. The rewards balance is `rewardsBalance` under `getWallet` balances and `seatBalance*` on `Transaction`. These are aliases, not separate pots of money. *** ## Spend Accounts Hold the Balance A [spend account](/features/spend-accounts) — `UserCashBalance` in the API — is a named container for cash. A user can open several and give each one a nickname, so funds can be separated by purpose without opening separate Fluz accounts. **Each spend account carries its own independent balance.** Money in one is not spendable from another until it is moved with an [internal transfer](/features/transfer-between-spend-accounts). ```mermaid theme={null} flowchart TD ACC["Fluz Account"] ACC --> RW["Rewards Balance\naccount-level, one only"] ACC --> GC["Gift Card Balance\naccount-level, one only"] ACC --> RS["Reserve Balance\naccount-level, one only"] ACC --> SAS["Spend Accounts\none or many"] SAS --> S1["'Operations'\ntotal · available · lifetime\n+ virtual account numbers"] SAS --> S2["'Team Travel'\ntotal · available · lifetime\n+ virtual account numbers"] SAS --> S3["'Marketing'\ntotal · available · lifetime\n+ virtual account numbers"] S1 <-->|internal transfer| S2 S2 <-->|internal transfer| S3 ``` Every spend account tracks three figures, all returned as strings: | Field | Meaning | | - | - | | `totalCashBalance` | The full balance currently held in the account. | | `availableCashBalance` | The portion that can be spent right now. | | `lifetimeCashBalance` | Cumulative total ever deposited into this account. | *** ## Getting Funds In There are two fundamentally different directions money can move into a balance, and the distinction matters for how you build. > **Pull — you initiate** > > Your application calls `depositCashBalance` and Fluz pulls funds from a **funding source** the user has already linked: a bank account, bank card, or digital wallet. You control the timing and the amount. > **Push — someone else initiates** > > An outside party sends money to a **virtual account number** attached to a spend account. Fluz posts it as a deposit when it arrives. You do not control the timing or the amount. | Path | Rails | Initiated by | Lands in | | - | - | - | - | | [Deposit funds](/features/deposit-from-external-accounts) | ACH pull, card, PayPal | Your app | A chosen spend account | | [Virtual account number](/features/virtual-account-numbers) | RTP, FedNow, Wire, ACH credit | An external sender | The spend account behind that VAN | | [Redeem a Fluz gift card](/features/redeem-fluz-gift-card) | — | The user | Gift card balance | | Cashback on qualifying activity | — | Fluz | Rewards balance | > [!NOTE] > > **A virtual account number is an address, not a balance.** > > Each spend account can have one or more virtual account numbers — real routing and account number pairs. Anything sent to them credits that spend account. Multiple VANs on one account all feed the same balance; they exist so you can tell a payroll credit apart from a customer payment. See [Virtual Account Numbers](/features/virtual-account-numbers). *** ## Moving and Removing Funds | Action | Operation | Scope | | - | - | - | | [Transfer between spend accounts](/features/transfer-between-spend-accounts) | Internal transfer | `MANAGE_PAYMENT` | | [Transfer to another Fluz account](/features/transfer-to-another-fluz-wallet) | Wallet transfer | `MANAGE_PAYMENT` | | [Look up a transfer recipient](/features/recipient-lookup) | Resolve an `accountId` | — | | [Withdraw to an external account](/features/withdraw-to-external-account) | Withdrawal | `MANAGE_PAYMENT` | > [!WARNING] > > **The gift card and reserve balances cannot be withdrawn.** Gift card balance can only be spent on gift card and virtual card purchases. Reserve balance is held by Fluz and is not user-directed. *** ## Reading Balances `getWallet` returns the account's balances in one call, alongside the user's linked funding sources. ```graphql theme={null} query getWallet { getWallet { balances { rewardsBalance { availableBalance totalBalance lifetimeBalance } cashBalance { availableBalance totalBalance pendingBalance lifetimeBalance } giftCardCashBalance { availableBalance totalBalance pendingBalance lifetimeBalance } userCashBalances(paginate: { limit: 10, offset: 0 }) { userCashBalanceId nickname totalCashBalance availableCashBalance lifetimeCashBalance status createdAt } } blockedPaymentTypes } } ``` `userCashBalances` is paginated and returns accounts ordered by creation date, most recent first. To fetch a single spend account, use [`getUserCashBalanceById`](/features/get-spend-accounts). > [!WARNING] > > **To show a user's total spendable cash, sum `availableCashBalance` across `userCashBalances`.** Do not add `cashBalance` on top of the individual spend account figures — doing so will overstate the total. The reserve balance is held by Fluz rather than directed by the user. Its current state is visible on each transaction through the `reserveBalanceAvailableBalance` and `reserveBalanceTotalBalance` snapshot fields described below. *** ## Reading the Ledger Balances tell you where things stand. `getTransactions` tells you how they got there. To see the ledger for one specific spend account, filter by its ID. **Scopes required:** `LIST_PAYMENT` **and** `LIST_PURCHASES` ```graphql theme={null} query spendAccountLedger($userCashBalanceId: [UUID], $limit: Int, $offset: Int) { getTransactions( filter: { userCashBalanceId: $userCashBalanceId } paginate: { limit: $limit, offset: $offset } ) { transactions { recordId transactionType amount source destination status usedUserCashBalanceId cashBalanceAvailableBalance createdAt } totalCount hasNextPage } } ``` ```json Variables theme={null} { "userCashBalanceId": ["9c1f6b2e-4d7a-4c3b-9f11-2a5e8b0d6c74"], "limit": 20, "offset": 0 } ``` Every transaction also carries a **balance snapshot** — the state of each balance *after* that transaction was applied — plus flags indicating which balances the transaction touched: | Balance | Snapshot fields | Affected flag | | - | - | - | | Spend / cash | `cashBalanceAvailableBalance` · `cashBalanceTotalBalance` | `isCashBalanceAffected` | | Rewards | `seatBalanceAvailableBalance` · `seatBalanceTotalBalance` | `isSeatBalanceAffected` | | Gift card | `giftCardPrepaymentBalanceAvailableBalance` · `giftCardPrepaymentBalanceTotalBalance` | `isGiftCardBalanceAffected` | | Reserve | `reserveBalanceAvailableBalance` · `reserveBalanceTotalBalance` | `isReserveBalanceAffected` | | Other cash | `otherCashBalanceAvailableBalance` · `otherCashBalanceTotalBalance` | — | > [!NOTE] > > **Only spend accounts can be filtered by ID.** `TransactionFilterInput` exposes `userCashBalanceId`, but there is no equivalent filter for the rewards, gift card, or reserve balances. To isolate activity on those, retrieve transactions over a date range and filter on the corresponding `is_..._affected` flag. `getTransactions` is capped at **20 records per page**. Check `hasNextPage` and advance `offset` to page through. See [Get All Transactions](/features/get-all-transactions) for the complete filter reference. *** ## Scopes at a Glance | You want to… | Scope | | - | - | | Read balances, spend accounts, and VANs | `LIST_PAYMENT` | | Read the transaction ledger | `LIST_PAYMENT` + `LIST_PURCHASES` | | Create, edit, or close a spend account | `MANAGE_PAYMENT` | | Deposit, transfer, or withdraw | `MANAGE_PAYMENT` | *** ## Where to Go Next > **[Spend Accounts](/features/spend-accounts)** > > Create, rename, and close the accounts that hold the balance. > **[Virtual Account Numbers](/features/virtual-account-numbers)** > > Receive RTP, FedNow, wire, and ACH credits directly into a spend account. > **[Deposit Funds](/features/deposit-from-external-accounts)** > > Pull money in from a linked bank account or card. > **[Withdraw Funds](/features/withdraw-to-external-account)** > > Move money out to an external account. > **[Transfer Between Accounts](/features/transfer-between-spend-accounts)** > > Move balance between a user's own spend accounts. > **[Get All Transactions](/features/get-all-transactions)** > > The full ledger, with filtering and pagination. *** **Want to learn more?** Speak with our experts for more info or to request a demo. # Withdraw to external account Source: https://docs.fluz.app/features/withdraw-to-external-account ## Overview A withdrawal allows users to transfer funds from their Fluz balance to an external account. Users can withdraw from two types of balances: * **Cash Balance** - Funds deposited by the user into their Fluz account * **Rewards Balance** - Cashback earnings accumulated from purchases Fluz supports the following withdrawal methods: | Method | Description | | - | - | | `BANK_ACH` | ACH transfer to a linked bank account | | `BANK_CARD` | Push-to-card transfer to a linked debit card, delivered as an original credit transaction (OCT) | | `PAYPAL` | Transfer to a linked PayPal account | | `VENMO` | Transfer to a linked Venmo account | ## Withdraw Cash Balance ### Sample Request You can initiate a withdrawal with the `withdrawCashBalance` mutation. This mutation transfers funds from a user's Fluz balance to their specified external account. ```json theme={null} { "query": "mutation withdrawCashBalance($input: WithdrawCashBalanceInput!) { withdrawCashBalance(input: $input) { withdraws { withdrawId amount processingFee chargedFee status displayStatus withdrawMethod withdrawSource submissionDate createdAt updatedAt transactionLogId bankAccountId userCashBalanceId seatId } balances { cashBalance { availableBalance totalBalance } rewardsBalance { availableBalance totalBalance } } } }", "variables": { "input": { "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000", "amount": 100.00, "method": "BANK_ACH", "source": "CASH_BALANCE", "bankAccountId": "a578fe07-7165-47b8-b147-2251c99b7fc1", "cashBalanceId": "8d197a56-53df-439b-85cd-bb88dfca9a5f" } } } ``` This mutation requires the `WithdrawCashBalanceInput` input type. Any field marked with an exclamation mark (`!`) in the schema is mandatory and must be included in the request. ### Input Fields | Field Name | Type | Description | | - | - | - | | `idempotencyKey` | `UUID!` | A unique client-generated UUID to ensure the request is processed only once. This prevents duplicate withdrawals if the same request is sent multiple times. | | `amount` | `Float!` | The amount to withdraw. Must be greater than 0. | | `method` | `WithdrawMethods!` | The withdrawal method to use. One of: `PAYPAL`, `BANK_ACH`, `BANK_CARD`, `VENMO`. | | `source` | `WithdrawSource` | The source balance from which to withdraw funds. One of: `CASH_BALANCE`, `REWARDS_BALANCE`. Defaults to `CASH_BALANCE` if not specified. | | `bankAccountId` | `UUID` | Required when `method` is `BANK_ACH`. The identifier of the linked bank account for ACH withdrawals. | | `bankCardId` | `UUID` | Required when `method` is `BANK_CARD`. The identifier of the linked debit card for push-to-card withdrawals. | | `paypalVaultId` | `UUID` | Required when `method` is `PAYPAL`. The identifier of the linked PayPal account. | | `venmoAccountId` | `UUID` | Required when `method` is `VENMO`. The identifier of the linked Venmo account. | | `cashBalanceId` | `UUID` | The identifier of the specific cash balance account to withdraw from. Required when `source` is `CASH_BALANCE`. | | `isExpedited` | `Boolean` | If `true`, the withdrawal is processed immediately. If `false` or omitted, it is processed on a standard settlement schedule. Currently applies to `BANK_CARD` withdrawals, and the two speeds are priced differently — preview with `getWithdrawFeeEstimate`. | ### WithdrawCashBalanceInput ```json theme={null} { "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000", "amount": 100.00, "method": "BANK_ACH", "source": "CASH_BALANCE", "bankAccountId": "a578fe07-7165-47b8-b147-2251c99b7fc1", "bankCardId": null, "paypalVaultId": null, "venmoAccountId": null, "cashBalanceId": "8d197a56-53df-439b-85cd-bb88dfca9a5f", "isExpedited": false } ``` *** ### Sample Response The response from the `withdrawCashBalance` mutation includes the withdrawal record(s) and the user's updated balances. ```json theme={null} { "data": { "withdrawCashBalance": { "withdraws": [ { "withdrawId": "c4d5e6f7-8901-2345-6789-0abcdef12345", "amount": "100.00", "processingFee": "0.00", "chargedFee": "0.00", "status": "PENDING", "displayStatus": "PROCESSING", "withdrawMethod": "BANK_ACH", "withdrawSource": "CASH_BALANCE", "submissionDate": "2024-01-15T10:30:00Z", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z", "transactionLogId": "f1234567-89ab-cdef-0123-456789abcdef", "bankAccountId": "a578fe07-7165-47b8-b147-2251c99b7fc1", "userCashBalanceId": "8d197a56-53df-439b-85cd-bb88dfca9a5f", "seatId": "e7979eb7-1727-48ed-8c5a-c56532908c1e" } ] } } } ``` ### Response Fields #### Withdraw Object | Field Name | Type | Description | | - | - | - | | `withdrawId` | `UUID!` | Unique identifier for the withdrawal. | | `amount` | `String!` | The amount withdrawn. | | `processingFee` | `String!` | Fee charged for processing the withdrawal. | | `chargedFee` | `String!` | Fee charged to the user for the withdrawal. | | `status` | `String!` | Internal status of the withdrawal (e.g., `PENDING`, `COMPLETED`, `FAILED`). | | `displayStatus` | `String!` | User-friendly display status (e.g., `PROCESSING`, `COMPLETE`). | | `withdrawMethod` | `WithdrawMethods!` | The withdrawal method used. | | `withdrawSource` | `WithdrawSource!` | The source balance from which funds were withdrawn. | | `submissionDate` | `DateTime!` | Date and time when the withdrawal was submitted. | | `createdAt` | `DateTime!` | Date and time when the withdrawal was created. | | `updatedAt` | `DateTime!` | Date and time when the withdrawal was last updated. | | `transactionLogId` | `UUID` | Identifier of the associated transaction log. | | `externalTransactionId` | `String` | External transaction identifier from payment gateway (for ACH). | | `payoutId` | `String` | Payout identifier from payment gateway (for PayPal/Venmo). | | `emailAddress` | `String` | Email address associated with the withdrawal. | | `bankAccountId` | `UUID` | Identifier of the bank account used (if applicable). | | `userCashBalanceId` | `UUID` | Identifier of the user cash balance account withdrawn from. | | `seatId` | `UUID` | The seat ID associated with the account. | *** ## Required Scope This mutation requires the **`MAKE_WITHDRAWAL`** scope to be granted to the access token. The `getWithdrawFeeEstimate` query requires the same scope. ```graphql theme={null} generateUserAccessToken( userId: "...", accountId: "...", scopes: [MAKE_WITHDRAWAL] ) ``` *** ## Withdrawal Methods by Account Type | Method | Required Field | Notes | | - | - | - | | `BANK_ACH` | `bankAccountId` | Standard ACH transfer. Typically settles in 1-3 business days. No fees. | | `BANK_CARD` | `bankCardId` | Push-to-card transfer, delivered as an original credit transaction (OCT). Available for eligible debit cards only. Set `isExpedited: true` for immediate delivery, or omit it to settle on the standard schedule. May have fees. | | `PAYPAL` | `paypalVaultId` | Transfer to linked PayPal account. May have fees. | | `VENMO` | `venmoAccountId` | Transfer to linked Venmo account. May have fees. | *** ## Preview Fees Before Withdrawing Use the [`getWithdrawFeeEstimate`](/api-reference/queries/get-withdraw-fee-estimate) query to quote a withdrawal before you submit it. Pass the same `amount`, `method`, `source`, and `isExpedited` you intend to send, and the response tells you exactly what the user will receive. ```json theme={null} { "query": "query getWithdrawFeeEstimate($input: GetWithdrawFeeEstimateInput!) { getWithdrawFeeEstimate(input: $input) { fee netAmount feePercent feeMaxCap settlementDelayDays } }", "variables": { "input": { "amount": 100.00, "method": "BANK_CARD", "source": "CASH_BALANCE", "isExpedited": true } } } ``` | Field Name | Type | Description | | - | - | - | | `fee` | `Float!` | The fee that will be charged for this withdrawal. | | `netAmount` | `Float!` | The amount the user will actually receive, after fees. | | `feePercent` | `Float!` | The fee percentage applied. | | `feeMaxCap` | `Float` | The maximum fee cap in dollars, if one applies. | | `settlementDelayDays` | `Int` | Business days until settlement. `null` for expedited withdrawals that settle immediately. | The fee is deducted from the withdrawal rather than added on top: the balance is debited the full `amount`, and the destination receives `netAmount`. Passing `FLUZPAY` as `method` returns `ARG-0001`. *** ## Push-to-Card Withdrawals (OCT) A `BANK_CARD` withdrawal is a push-to-card payout, delivered as an **original credit transaction (OCT)** — the card-network transaction type for crediting funds to a card. It requires `bankCardId`, and the linked debit card must support OCT. An ineligible card cannot be delivered to by any other means, so the user has to pick a different withdrawal method. **Delivery speed is controlled by `isExpedited`:** | `isExpedited` | Behavior | | - | - | | `true` | Funds are pushed to the card during the request. | | `false` or omitted | The withdrawal is recorded as `PENDING` and released after `settlementDelayDays` business days. | > [!WARNING] > > **A `PENDING` push-to-card withdrawal is not yet final.** > > A standard withdrawal is delivered to the card after the settlement delay, and it can still fail at that point — for example if the card is no longer able to receive the payout. When it does, the funds are credited back to the source balance. Reconcile withdrawal status instead of treating the initial `PENDING` response as a completed payout. ### Card Eligibility Most Visa and Mastercard debit cards can receive a push-to-card payout. Eligibility is evaluated when the withdrawal is submitted, and is a property of the card itself rather than something you configure. > [!NOTE] > > **Eligibility is not available in advance, and does not carry over from deposits.** > > There is no query that reports whether a card supports OCT — it surfaces on the first withdrawal to that card. Withdrawal and deposit eligibility are also independent, so a card a user successfully deposited from is not necessarily a card you can send a withdrawal to. See [Deposit From External Accounts](/features/deposit-from-external-accounts). `BANK_CARD` withdrawals are not restricted by `cardType`: a `PREPAID` card is not ruled out up front, and is accepted or declined on eligibility like any other card. Because `BANK_CARD` has no alternative delivery path, always keep `BANK_ACH`, `PAYPAL`, or `VENMO` reachable in your UI so an ineligible card does not dead-end the flow. Push-to-card failures surface as `HN-0124` or `BC-0004` — see [Error Handling](#error-handling) below. *** ## Error Handling Common error scenarios: | Error Code | Description | | - | - | | `WDR-0001` | Insufficient balance — the user does not have enough funds to complete the withdrawal. | | `WDR-0002` | Invalid amount — the amount is zero or negative. | | `WDR-0003` | General withdrawal error. Check the error message for details. | | `WDR-0004` | Invalid withdrawal method — use `PAYPAL`, `BANK_ACH`, `BANK_CARD`, or `VENMO`. | | `WDR-0005` | Invalid Venmo phone number. Applies to `VENMO` withdrawals. | | `WDR-0006` | The Venmo account is already linked. Applies to `VENMO` withdrawals. | | `WDR-0007` | The payment method's withdrawal limit has been exceeded. | | `ARG-0001` | A required argument is invalid. Passing `FLUZPAY` to `getWithdrawFeeEstimate` also returns this code. | | `ARG-0002` | A required argument is missing — `idempotencyKey`, `amount`, `method`, or the destination ID for the chosen method. | | `HN-0124` | The card is not eligible for push-to-card (OCT) withdrawals. Offer another method. | | `BC-0004` | The card was declined. Surface the message and offer another method. | | `G-0004` | Bank card withdrawals are temporarily unavailable. Retry later or fall back to another method. | `ARG-*` errors are raised before any funds move. ### Example Error Response ```json theme={null} { "errors": [ { "message": "Missing required argument - require idempotencyKey.", "extensions": { "code": "MISSING_ARGUMENT" } } ] } ``` *** ## Multiple Withdrawals In some cases, a single withdrawal request may result in multiple withdrawal records. This can happen when the withdrawal amount is split across multiple seats (network positions). The response will contain all withdrawal records created. ```json theme={null} { "data": { "withdrawCashBalance": { "withdraws": [ { "withdrawId": "withdraw-1", "amount": "75.00", "seatId": "seat-1" }, { "withdrawId": "withdraw-2", "amount": "25.00", "seatId": "seat-2" } ] } } } ``` *** ## Best Practices 1. **Always use unique idempotency keys** - Generate a new UUID for each withdrawal request to prevent duplicate transactions. 2. **Check balances before withdrawing** - Use the `getWallet` query to verify the user has sufficient funds before initiating a withdrawal. 3. **Handle pending states** - Withdrawals may take time to process. The `status` field will indicate the current state of the withdrawal. 4. **Store transaction references** - Save the `withdrawId` and `transactionLogId` for reconciliation and support purposes. *** ## Changelog ### v1.3.0 **Expedited withdrawals and fee previews** * Reintroduced `isExpedited` on `WithdrawCashBalanceInput`. It controls delivery speed for `BANK_CARD` withdrawals: `true` pushes to the card during the request, `false` or omitted settles on the standard schedule. This supersedes the v1.2.0 note below, which said the field had been removed. * Added the `getWithdrawFeeEstimate` query and the `GetWithdrawFeeEstimateInput` / `WithdrawFeeEstimate` types, so fees, net amount, and settlement timing can be previewed before submitting. * Documented push-to-card withdrawals as original credit transactions (OCT), including card eligibility and the fact that a standard withdrawal can still fail after it is submitted and be refunded to the source balance. * Corrected the required scope for `withdrawCashBalance` to `MAKE_WITHDRAWAL`. The `MANAGE_PAYMENT` scope previously listed on this page was wrong; `seat_id` on the `Withdraw` type is also optional (`UUID`), not required as v1.2.0 stated. * Documented that `FLUZPAY`, though present on the `WithdrawMethods` enum, is not a usable withdrawal method: `withdrawCashBalance` rejects it with `WDR-0004` (invalid withdrawal method), and `getWithdrawFeeEstimate` rejects it with `ARG-0001`. ### v1.2.0 - 2024-11-20 **Schema refinements and field cleanup** * Removed `isExpedited` field from `WithdrawCashBalanceInput` - expedited ACH is no longer configurable via the API * Changed `seat_id` field on `Withdraw` type from optional to required (`UUID` → `UUID!`) * Updated description for `BANK_CARD` method to remove "expedited" reference ### v1.1.0 - 2024-10-15 **Added Venmo support and rewards balance withdrawals** * Added `VENMO` to `WithdrawMethods` enum * Added `venmoAccountId` field to `WithdrawCashBalanceInput` * Added `REWARDS_BALANCE` to `WithdrawSource` enum to support withdrawing cashback earnings * Added `seat_id` field to `Withdraw` response type for multi-seat account tracking ### v1.0.0 - 2024-09-01 **Initial release** * Introduced `withdrawCashBalance` mutation with `MAKE_WITHDRAWAL` scope requirement * Added `WithdrawMethods` enum with `PAYPAL`, `BANK_ACH`, and `BANK_CARD` methods * Added `WithdrawSource` enum with `CASH_BALANCE` source * Added `WithdrawCashBalanceInput` input type with idempotency support * Added `Withdraw` response type with full withdrawal record details * Added `WithdrawCashBalanceResponse` type returning withdrawal records and updated balances * Integrated with payout-service for withdrawal processing * Added application action logging for audit trail # Application scopes Source: https://docs.fluz.app/fluz-dashboard/application-scopes ## Scopes Overview You are able to define what an application can do on an account. This is referred to as application scopes. Scopes represent permissions granted to an application to access specific resources or perform certain actions on behalf of a user. They define the level of access that applications have once they are authenticated. ### App (Global) Scope Grants **Description:** Global scopes are permissions set at an application-wide level by the system administrators through CMS. These define the broadest permissions an application can use. **Purpose:** Ensures that an application cannot request access to more resources than it is allowed at a maximum, regardless of individual user permissions. ### User (Individual) Scope Grants **Description:** User scopes are permissions granted by individual users, specifying what aspects of their data or functionalities an application can access. **Purpose:** Provides users control over their data and limits applications to access only what is necessary and explicitly permitted by the user. ## Valid Scope Requirements This is checked during generateUserAccessToken. For an application's scope to be considered active and valid: Both Grants Must Be Active: The permission must be granted both at the global level (by administrators/system) and at the individual user level. Non-expired Grants: Both the global and user grants must be current and not expired. An expired grant will automatically revoke the application's access to the specified resources. # Application Scopes The following is a list of scopes that are available for any application. | KEY | Usage | | :- | :- | | LIST\_PAYMENT | Allows access to user's payment methods and account balance. | | LIST\_PURCHASES | Allows access to user's purchase history. | | LIST\_OFFERS | Allows access to offers catalog and inventory data. | | MAKE\_DEPOSIT | Allows making a deposit to the user's balance. | | PURCHASE\_GIFTCARD | Allows purchasing a gift card. | | REVEAL\_GIFTCARD | Allows revealing a gift card code for redemption. | | MANAGE\_PAYMENT | Allows making changes to user's payment methods. | | REVEAL\_VIRTUALCARD | Allows revealing a virtual card details. | | CREATE\_VIRTUALCARD | Allows access to create virtual card. | | EDIT\_VIRTUALCARD | Allows editing, locking, and unlocking virtual cards. | | VIEW\_OBJECT\_CHANGE\_HISTORY | Allows reading the activity history of virtual cards, gift cards, spend accounts, and funding sources. See [Activity history](/features/activity-history). | | PCI\_COMPLIANCE | Required alongside other scopes for operations that return raw card data to your servers. Application-level and Fluz-administered — see the note below. | > [!NOTE] > > **`PCI_COMPLIANCE` is not self-selected.** It's granted at the application level by Fluz and can't be requested at `generateUserAccessToken`. > > * **Your own account, your own cards** — granted on request. No certification needed. > * **Showing a user their own card details** — you don't need this scope. [Secure Elements Card Reveal](/build-a-platform/card-reveal) renders the PAN, expiry, and CVV in a Fluz-hosted frame, so your systems never come into PCI scope. > * **Raw card data on your servers, on accounts that aren't yours** — submit your PCI DSS certification for review. See [Due diligence](/build-a-platform#due-diligence). # Get Application Scopes You can use the `getApplicatonScopes` query to get your account's current application scopes. The query will respond with an array of strings stating what scope is currently granted. ```json theme={null} # Query query getApplicationScopes { getApplicationScopes } # Response { "data": { "getApplicationScopes": ["LIST_PAYMENT"] } } ``` Ensure you have the correct permissions to perform certain mutations and queries with the Fluz API. # Managing your application Source: https://docs.fluz.app/fluz-dashboard/managing-your-application Application fields, types and statuses, and where to find each environment's API keys. ## Application types What an application can do is decided when you create it: | Created from | Can act on | | :- | :- | | **Create new app** | Only the developer's own account | | **Templates → OAuth Integration** | Other users' accounts, once they authorize you | An application can't be switched from one type to the other. If you need OAuth, create a new application from the **OAuth Integration** template. See [How accounts work](/get-started/accounts-and-applications#two-kinds-of-application). ## Application status | Status | Who the app can act for | | :- | :- | | `PERSONAL` | Only the developer who created it | | `REVIEW` (**In Review**) | Only the developer who created it | | `ACTIVE` | Other Fluz users, through OAuth and platform calls | Fluz moves applications to `ACTIVE`; it isn't self-serve. Send the application name or ID to your Fluz contact. Until then, calls that act for other users fail with `AUTH-0030 … current status: PERSONAL`. ## Fields on an application Each application has the following fields: * **Display Name** — must be unique; shown when you request access from a user. * **Developer Contact** — the contact information for this application. Recommended for any publicly available application. * **App Logo** — an image file, shown in the app marketplace. * **App Sub Title** — a short one-line explanation of the application. * **App Description** — the overview description of the application. * **App Visibility** — controls whether the app is available only on your Fluz account or to the wider Fluz network. ## API keys Each application has an API key, User ID, and Account ID. The **Account ID** is the account this application's own tokens act as. Staging and live are separate systems, so an application and its key exist in only one environment. ### Staging API keys Log in to [uni.staging.fluzapp.com/for-developers](https://uni.staging.fluzapp.com/for-developers) and open the application. From the live dashboard, you can reach staging under **Resources → Open Staging**. Staging is a separate login, so if you haven't signed up there yet, sign up first. ### Production API keys Log in to [fluz.app/for-developers](https://fluz.app/for-developers) and open the application. A production application must be created, and activated by Fluz, separately from its staging counterpart. > [!WARNING] > > A key only works against its own environment's endpoint. See [Staging vs. Live](/concepts/environments). ## Next steps > **[API credentials](/get-started/api-credentials)** > > Use the key to mint an access token. > **[Application scopes](/fluz-dashboard/application-scopes)** > > Choose what your application may request. # Webhooks Source: https://docs.fluz.app/fluz-dashboard/webhooks Webhooks let your application receive real-time notifications when events happen on the Fluz platform. Instead of polling for changes, you register an HTTPS endpoint and Fluz pushes event data to you the moment something occurs — a virtual card transaction, a declined purchase, a completed deposit, a user linking or unlinking their account via OAuth, and more. > [!NOTE] > > This page covers all webhook events, which span multiple platform areas (transaction activity, deposits, widget flows, and OAuth account linking) rather than belonging to any single feature. Webhooks work across all application types on the Fluz platform: private apps operating on your own account, public OAuth apps acting on behalf of other users via API, and embedded widget apps. ## How it works 1. You register a webhook URL on your application in the Developer Portal. 2. You select which event types to listen for (or subscribe to all of them). 3. When a matching event occurs, Fluz sends an HTTP `POST` to your URL with a signed JSON payload. 4. Your server verifies the signature, acknowledges with a `2xx`, and processes the event. ```mermaid theme={null} sequenceDiagram participant Fluz as Fluz participant You as Your Endpoint Fluz->>You: HTTP POST with signed JSON payload You-->>Fluz: 200 OK ``` If your endpoint is unavailable or returns an error, Fluz retries up to **5 times** with exponential backoff before giving up on that delivery. ## Webhooks by app type How events are routed to your app depends on your application model. ### Private apps — your own account Webhooks fire for activity on **your own account**. Any transaction, decline, or deposit on accounts you own triggers webhooks registered on your app. **Common use cases:** notifications when a virtual card transaction is authorized or settled; real-time alerting on declines; monitoring deposits and balance changes on your spend accounts. ### Public apps (OAuth) — acting on behalf of users Webhooks fire for activity on **accounts that have authorized your app**. When a user grants your app access via OAuth, their events are routed to your registered webhooks — provided the user's OAuth grant includes the required scopes. This works whether the user interacts through your direct API integration or an embedded widget; the key requirement is an OAuth relationship between the user and your app. **Common use cases:** knowing when a user has linked (or re-linked) their account to your app, and when they have unlinked it; monitoring virtual card spend across connected accounts; real-time decline notifications; tracking deposit completions; receiving KYC and KYB status updates. ### Widget apps Widget apps are a specialized public app. Events route the same way (based on the OAuth relationship), and widget apps additionally support widget-specific events like transfer completions and gift card purchases. ## Event types Fluz only delivers an event if your application — **and**, for public/OAuth apps, the individual user's OAuth grant — holds the required scopes. Events with no required scopes (such as `OAUTH_USER_LINKED` and `OAUTH_USER_UNLINKED`) are delivered to any subscribed public/OAuth app. ### Transaction events > [!NOTE] > > **Staging only:** `externalReferenceId` on transaction events > > `TRANSACTION_CREATE`, `TRANSACTION_UPDATE` and `TRANSACTION_DECLINE` carry `externalReferenceId` in the staging environment only. Production deliveries do not include it yet; this page will be updated when it is rolled out. Cover the full transaction lifecycle. Apply to **all app types**. | Event | Description | Required scopes | | - | - | - | | `TRANSACTION_CREATE` | A new transaction was created (e.g., virtual card purchase, deposit, transfer). | `LIST_PAYMENT`, `LIST_PURCHASES` | | `TRANSACTION_UPDATE` | A transaction's status or amount changed (e.g., settled, adjusted). | `LIST_PAYMENT`, `LIST_PURCHASES` | | `TRANSACTION_DECLINE` | A transaction was declined (e.g., insufficient funds, card controls). | `LIST_PAYMENT`, `LIST_PURCHASES` | ### Deposit events | Event | Description | Required scopes | | - | - | - | | `DEPOSIT_COMPLETE` | A deposit from a funding source to a spend account completed. | `MAKE_DEPOSIT` | ### KYC & KYB events Apply to **public/OAuth and widget apps**. Like `OAUTH_USER_LINKED`, these events have no private-app equivalent — they only fire for accounts connected via an OAuth relationship, and only when your app's (and the user's) OAuth grant includes the required scope. | Event | Description | Required scopes | | - | - | - | | `KYC_STATUS_UPDATE` | A connected user's identity verification (KYC) status changed. | `VERIFY_KYC` | | `KYB_STATUS_UPDATE` | A connected business's verification (KYB) status changed. | `REGISTER_BUSINESS` | ### Funding source events > [!NOTE] > > **Staging only** > > `FUNDING_SOURCE_STATUS_UPDATE` is currently delivered only in the staging environment. It is not yet emitted in production; production apps will not receive it until it is rolled out, and this page will be updated when it is. Apply to **all app types**. Delivered for users tied to your application — an OAuth user who granted `LIST_PAYMENT`, or any user on an account that owns a private app. | Event | Description | Required scopes | | - | - | - | | `FUNDING_SOURCE_STATUS_UPDATE` | A user added, renamed or removed a funding source. | `LIST_PAYMENT` | One event type covers all three cases; branch on `changeType` (`ADDED`, `UPDATED`, `DELETED`) rather than on the event name. The user's Fluz cash balance is not a funding source and never produces this event. The event is emitted from the change to the funding source itself, so it fires wherever the change was made — the Fluz mobile app, the web app, or an embedded widget. ### OAuth events > [!NOTE] > > **Staging only** > > `OAUTH_USER_UNLINKED` is currently delivered only in the staging environment. It is not yet emitted in production; production apps will not receive it until it is rolled out, and this page will be updated when it is. Apply to **public/OAuth and widget apps**. | Event | Description | Required scopes | | - | - | - | | `OAUTH_USER_LINKED` | A user completed OAuth linking to your application. Fires on both the initial link and subsequent updates (e.g., when the granted scopes change). | *None* | | `OAUTH_USER_UNLINKED` | A user's OAuth link to your application was revoked, by the user or by Fluz. | *None* | Both events are delivered only to public/OAuth app types — private-app subscribers do not receive them. Because they have no scope requirement, you receive them for every user who links, re-links or unlinks from your app, regardless of which scopes were granted. Use `OAUTH_USER_LINKED` to provision or update your local record of the connected user, capture the granted scope set, and map your own user identifier via `externalReferenceId`. Use `OAUTH_USER_UNLINKED` to mark that record as disconnected: once it fires, the user's access and refresh tokens are rejected and Fluz stops sending you every other event for that user. If the user links again, `OAUTH_USER_LINKED` fires again. ### Widget-specific events Apply to widget and OAuth integrations where users interact through Fluz-embedded flows. | Event | Description | Required scopes | | - | - | - | | `WIDGET_KYC_INITIATION` | A user initiated identity verification (KYC). | `VERIFY_KYC` | | `WIDGET_DEPOSIT_COMPLETE` | A transfer from a customer to your app completed. | `MAKE_PAYOUT_TRANSFER_SEND` | | `WIDGET_WITHDRAW_COMPLETE` | A transfer from your app to a customer completed. | `MAKE_PAYOUT_TRANSFER_RECEIVE` | | `WIDGET_PURCHASE_GIFT_CARD` | A gift card purchase completed. | `PURCHASE_GIFTCARD` | > 📘 **Naming note:** `WIDGET_DEPOSIT_COMPLETE` and `WIDGET_WITHDRAW_COMPLETE` describe customer↔app *transfers*; the `DEPOSIT`/`WITHDRAW` wording is historical. Treat "deposit" as "customer → app" and "withdraw" as "app → customer." ## Setting up webhooks ### 1. Open the Developer Portal Go to the [Developer Portal](https://app.fluz.app/for-developers) and select your application. ### 2. Open the Webhooks section * **OAuth apps:** **OAuth** tab → **Webhook URLs**. * **Widget apps:** **Widget** tab → **Webhook URLs**. * **API / private apps:** the **Webhook URLs** section in your app settings. ### 3. Add a webhook URL Click **Add new URL** and enter your HTTPS endpoint (e.g., `https://api.yourapp.com/webhooks/fluz`). ### 4. Select events Choose the event types you want to receive. ### 5. Save Click **Create Webhook**. Your endpoint begins receiving events immediately. ## Managing webhooks * **Multiple endpoints** — you can register more than one webhook URL per application. * **Change subscribed events** — delete the webhook and recreate it with the new event selection. * **Remove a webhook** — click **Remove** next to it. The webhook is archived immediately and stops receiving events. ## Receiving webhooks ### Request format Every webhook is delivered as an HTTP `POST` with these headers: | Header | Description | | - | - | | `Content-Type` | Always `application/json`. | | `X-HMAC-Signature` | HMAC-SHA256 signature of the raw JSON body, signed with your app's API key. | | `X-Event-ID` | Unique UUID for this event — use it for deduplication. | The body is a JSON object, and every payload includes an `eventType` field identifying the event. ### Endpoint requirements * **HTTPS only** — plain HTTP endpoints are rejected at registration time. * **Publicly accessible** and able to accept `POST` requests. * **Respond with a `2xx` within 30 seconds.** Non-`2xx` responses or timeouts trigger retries. * **Verify the HMAC signature** on every request. ## Verifying signatures Every delivery includes an `X-HMAC-Signature` header — an HMAC-SHA256 hash of the **raw** JSON body, signed with your application's **API key**. Always verify it before trusting a payload. > ⚠️ **Verify against the raw request body.** Compute the HMAC over the exact bytes Fluz sent — do **not** re-serialize the parsed JSON. Re-stringifying can reorder keys or change whitespace and cause valid signatures to fail. The examples below capture the raw body for this reason. ### Node.js (Express) ```javascript theme={null} const express = require('express'); const crypto = require('crypto'); const app = express(); // Capture the raw body so the HMAC is computed over the exact bytes Fluz sent. app.use('/webhooks/fluz', express.raw({ type: 'application/json' })); function verifyFluzWebhook(rawBody, signature, apiKey) { const expected = crypto.createHmac('sha256', apiKey).update(rawBody).digest('hex'); const received = Buffer.from(signature || '', 'hex'); const computed = Buffer.from(expected, 'hex'); return received.length === computed.length && crypto.timingSafeEqual(received, computed); } app.post('/webhooks/fluz', (req, res) => { const signature = req.headers['x-hmac-signature']; const eventId = req.headers['x-event-id']; // req.body is a Buffer because of express.raw above if (!verifyFluzWebhook(req.body, signature, process.env.FLUZ_API_KEY)) { return res.status(401).send('Invalid signature'); } // Acknowledge immediately, then process asynchronously res.status(200).send('OK'); const event = JSON.parse(req.body.toString('utf8')); handleEvent(eventId, event).catch(err => console.error('Webhook processing error:', err) ); }); ``` ### Python (Flask) ```python theme={null} import hmac, hashlib from flask import Flask, request app = Flask(__name__) def verify_fluz_webhook(raw_body: bytes, signature: str, api_key: str) -> bool: expected = hmac.new(api_key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature or "") @app.post("/webhooks/fluz") def fluz_webhook(): signature = request.headers.get("X-HMAC-Signature") event_id = request.headers.get("X-Event-ID") # request.get_data() returns the raw bytes — do NOT use request.json for verification if not verify_fluz_webhook(request.get_data(), signature, FLUZ_API_KEY): return "Invalid signature", 401 enqueue(event_id, request.get_json()) # process asynchronously return "OK", 200 ``` ## Responding to webhooks Your endpoint **must**: * Respond with a `2xx` status within 30 seconds. * Return quickly — acknowledge first, then process asynchronously. * Be reachable over HTTPS. Your endpoint **must not**: * Respond with redirects (`3xx`). * Respond with `4xx`/`5xx` for valid webhooks (this triggers retries). ## Retry policy | Behavior | Detail | | - | - | | Max attempts | 5 | | Schedule | Exponential backoff | | Trigger | Any non-`2xx` response or timeout | | Timeout | 30 seconds per attempt | | After all retries fail | Delivery is abandoned; no further attempts. | New events resume delivery automatically once your endpoint recovers. To re-send events whose retries were already exhausted, contact support with the relevant `X-Event-ID`. ## Idempotency & ordering Webhooks may be delivered **more than once**, and **delivery order is not guaranteed**. * **Deduplicate** using the `X-Event-ID` header. Persist processed IDs (Redis or a database in production) and skip events you've already handled. * **Order by data, not arrival.** If sequence matters, order by payload timestamps (`createdAt`, `updatedAt`, `transactionDateTime`) and event IDs. ```javascript theme={null} const seen = new Set(); // use Redis or a database in production async function handleEvent(eventId, event) { if (seen.has(eventId)) return; // already processed — skip seen.add(eventId); switch (event.eventType) { case 'TRANSACTION_CREATE': await onTransactionCreated(event); break; case 'TRANSACTION_UPDATE': await onTransactionUpdated(event); break; case 'TRANSACTION_DECLINE': await onTransactionDeclined(event); break; case 'DEPOSIT_COMPLETE': await onDepositComplete(event); break; case 'OAUTH_USER_LINKED': await onUserLinked(event); break; case 'OAUTH_USER_UNLINKED': await onUserUnlinked(event); break; case 'KYC_STATUS_UPDATE': await onKycStatusUpdate(event); break; case 'KYB_STATUS_UPDATE': await onKybStatusUpdate(event); break; case 'FUNDING_SOURCE_STATUS_UPDATE': await onFundingSourceChange(event); break; case 'WIDGET_KYC_INITIATION': await onKycInitiated(event); break; // ...handle remaining widget events } } ``` ## Identifying the app and user * **User:** `userId` is the Fluz user ID. For OAuth/widget events, `externalReferenceId` maps to *your* user identifier from the OAuth flow. * **App:** transaction payloads include `connectedAppId` and `connectedAppName`. If you route multiple apps to one endpoint, branch on `connectedAppId`. For `OAUTH_USER_LINKED` and `OAUTH_USER_UNLINKED`, the app is identified by `appId`. ## Payload reference Every payload includes an `eventType`. Field availability can vary by event; handlers should ignore unrecognized fields for forward compatibility. > 📘 **A note on `status` values.** For created/updated transactions the `status` field takes one of `PENDING`, `SETTLED`, or `FAILED`. Declined transactions carry a `status` of `DECLINED` (or `FAILED`). Earlier drafts of this page showed `COMPLETED` as a status — that value is not emitted; use `SETTLED` to detect a finalized transaction. ### Transaction created (`TRANSACTION_CREATE`) Fired for any new transaction — virtual card purchases, deposits, transfers, and more. ```json theme={null} { "eventType": "TRANSACTION_CREATE", "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "externalReferenceId": "usr_8f3d2a91", "transactionType": "PURCHASE", "amount": 42.99, "destination": "Coffee Shop", "source": "Virtual Card", "status": "PENDING", "channel": "VIRTUAL_CARD", "connectedAppId": "your-app-id", "connectedAppName": "Your App", "createdAt": "2025-01-15T10:30:00.000Z", "merchantName": "Coffee Shop", "merchantCity": "New York", "merchantState": "NY", "merchantCountry": "US", "cardLastFour": "1234", "virtualCardId": "vc-uuid-here", "digitalWalletProvider": "APPLE_PAY", "digitalWalletToken": "dw_2204zdpwi8352fa1jvhs2nl0ikgvdgnism", "isReversal": false, "cashbackRate": 0.05 } ``` | Field | Type | Description | | - | - | - | | `eventType` | String | Always `TRANSACTION_CREATE`. | | `recordId` | String (UUID) | Unique webhook event record ID. | | `transactionId` | String (UUID) | Identifies this transaction record. A pending authorization and its settlement share it; additional settlement legs and returns get their own. See [Linking the events of one purchase](#linking-the-events-of-one-purchase). | | `referenceId` | String | The Fluz purchase id. Every event that belongs to the same virtual card purchase carries the same value, so group on it to follow money end to end. May be absent on a `Virtual Card Return` that the merchant submitted as a new transaction. | | `accountId` | String (UUID) | The Fluz account ID. | | `userId` | String (UUID) | The Fluz user ID. May be absent on some system-generated transactions. | | `externalReferenceId` | String | *Optional.* **Staging only** for now — not yet delivered in production. Your user identifier from the OAuth flow, resolved from the grant between that user and your app. Absent for private-app activity and for grants that carry no external reference. | | `transactionType` | String | Transaction type, e.g. `PURCHASE`, `GIFT_CARD_PURCHASE`, `DEPOSIT`, `WITHDRAWAL`, `TRANSFER`. Treat as an open string. | | `amount` | Number | Transaction amount in USD. | | `originalCurrencyAmount` | Number | The amount the card issuer reports as requested, in minor units of the merchant's currency (`6300` HKD is 63.00 HKD). On a `Virtual Card Return` it is the amount still on hold before that return, not the amount returned. Reconcile on `amount`, see [Reconciling amounts](#reconciling-amounts). | | `destination` | String | Where funds went (e.g., merchant name). | | `source` | String | Funding source (e.g., `Virtual Card`). | | `status` | String (enum) | One of `PENDING`, `SETTLED`, `FAILED`. | | `channel` | String | Origin/channel of the transaction (e.g., `VIRTUAL_CARD`). Treat as an open string. | | `connectedAppId` | String | The app the event is routed to. May be absent for private-app activity. | | `connectedAppName` | String | Display name of the connected app. | | `createdAt` | String (ISO 8601) | When the transaction was created. | | `merchantName` / `merchantCity` / `merchantState` / `merchantCountry` | String | Merchant location details. | | `cardLastFour` | String | Last four digits of the card used. | | `virtualCardId` | String (UUID) | The virtual card ID, if applicable. | | `digitalWalletProvider` | String (enum) | Present when a card transaction was made through a mobile wallet. One of `APPLE_PAY`, `GOOGLE_PAY` (more may be added over time). Absent or `null` for non-wallet transactions. This is the field to use to determine that a transaction was a mobile-wallet payment. | | `digitalWalletToken` | String | Network token identifier for the tokenized card (e.g. `dw_...`). Not a wallet indicator on its own: it is also set for non-wallet network tokens (such as a merchant's stored card-on-file) and may be absent on some wallet transactions. Use `digitalWalletProvider`, not the presence of this token, to decide whether a payment came from a mobile wallet. | | `isReversal` | Boolean | `true` when the transaction is an authorization reversal: a released hold, whether the card issuer or Fluz released it, or a card verification void. `false` on captures and merchant refunds. Absent when we could not classify the transaction. See [Reversals vs refunds](#reversals-vs-refunds) below. | | `cashbackRate` | Number | Decimal cashback rate (e.g., `0.05` = 5%). | ### Transaction updated (`TRANSACTION_UPDATE`) Fired when a transaction's status or details change — for example, when a pending authorization settles. ```json theme={null} { "eventType": "TRANSACTION_UPDATE", "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "externalReferenceId": "usr_8f3d2a91", "transactionType": "PURCHASE", "referenceId": "11323677", "amount": 42.99, "status": "SETTLED", "connectedAppId": "your-app-id", "connectedAppName": "Your App", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-16T08:00:00.000Z", "isReversal": false } ``` Fields match `TRANSACTION_CREATE` where present, plus `updatedAt` (ISO 8601) marking when the change occurred. `userId`, `externalReferenceId` (staging only for now), `connectedAppId`, and `connectedAppName` are carried through the same way as on `TRANSACTION_CREATE`, so you can identify the user and app consistently across the lifecycle. A `status` transition to `SETTLED` is the signal that a previously pending transaction has finalized. `isReversal` carries the same meaning here as on `TRANSACTION_CREATE`. See [Reversals vs refunds](#reversals-vs-refunds). ### Transaction declined (`TRANSACTION_DECLINE`) Fired when a transaction is declined. Includes structured decline reasons your app can act on. ```json theme={null} { "eventType": "TRANSACTION_DECLINE", "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "transactionType": "PURCHASE", "amount": 500.00, "fluzAmount": 500.00, "externalFundingAmount": 0, "currency": "USD", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "externalReferenceId": "usr_8f3d2a91", "status": "DECLINED", "merchantName": "Electronics Store", "cardLastFour": "1234", "virtualCardId": "vc-uuid-here", "isCashBalanceUsed": true, "isPrepaymentBalanceUsed": false, "isRewardsBalanceUsed": false, "isReserveBalanceUsed": false, "isReversal": false, "transactionDateTime": "2025-01-15T10:30:00.000Z", "declineTitle": "Transaction Declined", "declineReason": "Insufficient funds", "declineDescription": "Your account balance was not sufficient for this transaction.", "declineCategory": "Balance" } ``` | Field | Type | Description | | - | - | - | | `amount` | Number | Total attempted amount. | | `fluzAmount` | Number | Portion drawn from Fluz balance. | | `externalFundingAmount` | Number | Portion drawn from external funding. | | `currency` | String | ISO currency code (e.g., `USD`). | | `status` | String (enum) | One of `DECLINED`, `FAILED`. | | `userId` | String (UUID) | The Fluz user ID. May be absent on some system-generated transactions. | | `externalReferenceId` | String | *Optional.* **Staging only** for now — not yet delivered in production. Your user identifier from the OAuth flow. Same semantics as on `TRANSACTION_CREATE`. | | `isCashBalanceUsed` / `isPrepaymentBalanceUsed` / `isRewardsBalanceUsed` / `isReserveBalanceUsed` | Boolean | Which balance type(s) the attempt drew from. | | `isReversal` | Boolean | `true` for an authorization reversal: a released hold, whether the card issuer or Fluz released it, or a card verification void. `false` on captures and merchant refunds. Absent when we could not classify the transaction. See [Reversals vs refunds](#reversals-vs-refunds). | | `transactionDateTime` | String (ISO 8601) | When the decline occurred. | | `declineTitle` | String | Short, user-facing title. | | `declineReason` | String | Short reason (e.g., `Insufficient funds`). | | `declineDescription` | String | Longer, user-facing explanation. | | `declineCtaText` | String | Suggested resolution action, when available (e.g., `Add money`). | | `declineCategory` | String | High-level decline grouping. Common values include `Balance`, `Limit`, `Validation`, `Virtual Card`, and `Acquiring`. Treat as an open string and see [Decline Codes](/features/decline-codes) for the full set and per-code detail. | Additional optional fields may be present depending on the transaction (e.g. `merchantId`, `merchantCity`, `merchantState`, `merchantCountry`, `cardDisplayName`, `virtualCardProgram`, `channel`, `bankAccountNickname`, `bankAccountLastFour`). Ignore any you don't use. ### Linking the events of one purchase A virtual card purchase usually produces more than one transaction event. Two fields tie them together: * `referenceId` is the Fluz purchase id. The authorization, its settlement, any additional settlement leg and any return of that purchase all carry the same value. Group on it. * `transactionId` identifies one ledger record. A pending authorization and its settlement share it, because the settlement is delivered as a `TRANSACTION_UPDATE` of the same record. Anything Fluz records separately gets a new `transactionId`. The sequence for a typical purchase: | Step | Event | `transactionId` | `status` | `amount` | | - | - | - | - | - | | Authorization | `TRANSACTION_CREATE`, `Virtual Card Purchase` | A | `PENDING` | negative | | Settlement of that authorization | `TRANSACTION_UPDATE`, `Virtual Card Purchase` | A | `SETTLED` | negative | | Additional settlement leg (the merchant cleared more than it authorized, a currency difference, or a second capture) | `TRANSACTION_CREATE`, `Virtual Card Purchase` | B | `SETTLED` | negative | | Return (released hold or merchant refund) | `TRANSACTION_CREATE`, `Virtual Card Return` | C | `SETTLED` | positive | Example: an 8.54 authorization arrives as `TRANSACTION_CREATE` on `transactionId` A with `status: PENDING`. The merchant clears 8.56. You receive a `TRANSACTION_UPDATE` on A with `status: SETTLED` and `amount: -8.54`, then a `TRANSACTION_CREATE` on a new `transactionId` B with `status: SETTLED` and `amount: -0.02`. All three carry `referenceId: "11323677"`. The net charge for the purchase is the sum of the latest amount per `transactionId` under that `referenceId`: -8.56. Rules that follow from this: * Key your records by `transactionId`, but reconcile by `referenceId`. A `TRANSACTION_CREATE` with `status: SETTLED` and no earlier pending event is normal: it is an additional leg or a return of a purchase you already hold under the same `referenceId`. * When an authorization is never captured and the hold is released, you receive two events: a `TRANSACTION_UPDATE` that moves the pending purchase to `SETTLED`, and a `Virtual Card Return` with the same `referenceId` and a positive `amount`. They net to zero. Treat the pair as the release of the pending amount, not as a purchase that completed. * A `Virtual Card Return` for a merchant refund that the merchant submitted as a new transaction, rather than against the original authorization, may carry no `referenceId`. Match those by `virtualCardId`, `amount` and merchant. ### Reversals vs refunds A reversal releases an authorization hold before any money is captured. The cardholder's available balance goes back up, but no funds ever left and none come back. A refund happens after a capture: money moved, and the merchant sent it back. `isReversal` tells the two apart: | Value | Meaning | | - | - | | `true` | An authorization reversal. The hold was released and the amount was never captured. Released holds, whether the card issuer or Fluz released them, and card verification voids are flagged this way. | | `false` | Anything we classified that is not a reversal, such as an authorization, a capture, a genuine refund, or a decline. | | absent | We could not classify the transaction. Treat it as unknown, never as `false`. | Use this field instead of `transactionType` wherever it is present. Card issuers label these events inconsistently, so one issuer's type value for a merchant refund is another's for a pre-capture reversal, and the type alone cannot tell you whether money actually moved. `Virtual Card Return` events carry the field too: `true` for a released hold, `false` for a merchant refund. A small number of returns still arrive without it; for those use `referenceId` and the original authorization to tell them apart. Which event carries a reversal depends on what was reversed: * A card verification void is recorded as its own transaction, so it arrives as `TRANSACTION_CREATE` with a negative `amount` and then updates. These are the small checks a merchant runs to confirm a card is valid, which we void straight away. They carry `isReversal: true`. * A released pre-capture hold arrives as a separate transaction with `transactionType` `Virtual Card Return` and a positive `amount`, carrying the purchase's `referenceId`. The original purchase is updated to `SETTLED` at the same time, so the two net to zero. The return carries `isReversal: true` whether Fluz or the card issuer released the hold. * A merchant refund also arrives with `transactionType` `Virtual Card Return` and carries `isReversal: false`. Match it against the original purchase by `referenceId` when present, otherwise by `virtualCardId`, `amount` and merchant. A reversal is not a failure, but it does need handling: release the hold you recorded for the matching authorization, or it stays open on your side forever. Do not post it as a new outflow, since no money moved. ### Reconciling amounts Reconcile on `amount`. It is the USD amount that moved on that event, so the latest `amount` per `transactionId`, summed under one `referenceId`, is the net for the purchase however many settlement legs or returns it has. `originalCurrencyAmount` does not add up that way. It is the amount the card issuer reports as requested for the event. On a return that is the amount still on hold before the return, so it repeats what earlier events already carried and a sum of it double counts. Example: a 39.69 authorization whose hold is released in two steps, as when part of an order goes out of stock and the rest is released later. | Event | `amount` | `originalCurrencyAmount` | | - | - | - | | Authorization, `Virtual Card Purchase` | -39.69 | 3969 | | First release, `Virtual Card Return` | 17.95 | 3969 | | Second release, `Virtual Card Return` | 21.74 | 2174 | The `amount` values net to 0.00 because nothing was captured. The `originalCurrencyAmount` values describe what was on hold at each step, not money that moved. ### Deposit complete (`DEPOSIT_COMPLETE`) Fired when a deposit from a funding source to a spend account completes. ```json theme={null} { "eventType": "DEPOSIT_COMPLETE", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "externalReferenceId": "your-reference-123", "amount": 50.00 } ``` ### OAuth user linked (`OAUTH_USER_LINKED`) Fired when a user completes OAuth linking to your application — on both the initial link and on subsequent updates (for example, when the user re-authorizes with a different set of scopes). Public/OAuth and widget apps only. No scope is required to receive this event. ```json theme={null} { "eventType": "OAUTH_USER_LINKED", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "appId": "55555555-5555-4555-8555-555555555555", "externalReferenceId": "your-reference-123", "scopes": [ "LIST_PAYMENT", "MAKE_DEPOSIT" ], "emailAddress": "user@example.com", "phoneNumber": "+14155551234", "spendAccountAccess": { "restricted": true, "userCashBalanceIds": [ "7c9e6679-7425-40de-944b-e07fc1f90ae7" ], "includesFutureSpendAccounts": false } } ``` | Field | Type | Description | | - | - | - | | `eventType` | String | Always `OAUTH_USER_LINKED`. | | `accountId` | String (UUID) | The Fluz account ID that linked to your app. | | `userId` | String (UUID) | The Fluz user ID that linked to your app. | | `appId` | String (UUID) | Your application's ID. | | `externalReferenceId` | String | *Optional.* Your user identifier passed through the OAuth flow. Present for widget app types and any flow that supplied one; may be absent otherwise. | | `scopes` | String\[] | The distinct scope keys granted by this user (e.g. `LIST_PAYMENT`, `MAKE_DEPOSIT`). | | `emailAddress` | String \| null | **Staging only** for now — not yet delivered in production. The email address on the linked user's Fluz profile, or `null` if the user has none. | | `phoneNumber` | String \| null | **Staging only** for now — not yet delivered in production. The phone number on the linked user's Fluz profile, or `null` if the user has none. The format is not normalized, so normalize it (for example, to E.164) before matching against your own records. | | `spendAccountAccess` | Object | **Staging only** for now — not yet delivered in production. Which of the user's [spend accounts](/features/spend-accounts) this link can act on. See [Spend account access](#spend-account-access) below. | | `spendAccountAccess.restricted` | Boolean | `true` when the user granted your app access to specific spend accounts only; `false` when the grant covers all of the user's spend accounts. | | `spendAccountAccess.userCashBalanceIds` | String (UUID)\[] \| null | **Staging only** for now — not yet delivered in production. The `userCashBalanceId` of each granted spend account when `restricted` is `true`. Always `null` when `restricted` is `false`. | | `spendAccountAccess.includesFutureSpendAccounts` | Boolean | Whether spend accounts the user opens after linking are added to a restricted grant automatically. Only meaningful when `restricted` is `true`; see [Spend account access](#spend-account-access). | Because `OAUTH_USER_LINKED` fires again on re-link/updates, treat it as an upsert: create the connected user on first receipt, and refresh the stored scope set and contact details on later deliveries. `emailAddress` and `phoneNumber` reflect the user's profile at the moment of the link; a later profile change does not trigger a new event on its own. > [!NOTE] > > **Staging only** > > `emailAddress` and `phoneNumber` on `OAUTH_USER_LINKED` are currently delivered only in the staging environment. Production deliveries do not include them yet; this page will be updated when they are rolled out. In rare cases Fluz can't load the contact details when the event is sent, and `emailAddress` and `phoneNumber` are then absent from the payload. Treat an absent field as unknown and keep any value you already store. Only an explicit `null` means the user has no value. #### Spend account access When a user links to your app, the permissions step lets them grant access to all of their spend accounts or only to the ones they choose. `spendAccountAccess` tells you which of the two the link ended up with, so you can start acting on a spend account — for example, calling [`getSpendAccountPaymentInstructions`](/api-reference/queries/get-spend-account-payment-instructions) to produce deposit instructions — without a follow-up lookup right after the link. * **Restricted grant.** `restricted` is `true` and `userCashBalanceIds` lists the spend accounts the user selected when linking. The IDs are currently delivered in staging only; production deliveries do not include them yet. `includesFutureSpendAccounts` tells you whether that list is fixed: * `false` — the grant covers exactly these spend accounts. The list changes only if the user re-links, which sends a new `OAUTH_USER_LINKED` event. * `true` — the operator let the user include spend accounts they open later. Each new spend account is added to the grant automatically, but opening one does not fire this event, so `userCashBalanceIds` is accurate only as of the link. Call [`getWallet`](/api-reference/queries/get-wallet) with the user's access token for the current list. * **Unrestricted grant.** `restricted` is `false` and `userCashBalanceIds` is `null`. The grant covers every spend account the user has now and any they open later. Fluz does not enumerate the current spend accounts in the payload, because opening a new spend account does not fire this event and a snapshot would go stale. Call [`getWallet`](/api-reference/queries/get-wallet) with the user's access token for the current list. ```json theme={null} { "spendAccountAccess": { "restricted": false, "userCashBalanceIds": null } } ``` Only IDs are included. Nicknames and balances are not, because they can change without firing this event — read them from `getWallet`, whose `balances.userCashBalances` is already filtered to the spend accounts the link permits. The event fires again with the updated value whenever the grant changes: the user re-consents, the granted scopes change, or the granted spend accounts change even when the scopes stay the same (for example, the user swaps one spend account for another). Treat the latest delivery as authoritative and replace, rather than merge, the stored ID list. Treat this field as a convenience. Webhook deliveries can arrive late or out of order, so if you need to be certain which spend accounts a link permits, call `getWallet` — it is the source of truth. > [!NOTE] > > **Staging only** > > `spendAccountAccess` on `OAUTH_USER_LINKED` is currently delivered only in the staging environment. Production deliveries do not include it yet; this page will be updated when it is rolled out. ### OAuth user unlinked (`OAUTH_USER_UNLINKED`) Fired when a user's OAuth link to your application is revoked — the user removes your app under **Apps & Integrations** in Fluz, or Fluz revokes the link on their behalf. Public/OAuth and widget apps only. No scope is required to receive this event. > [!NOTE] > > **Staging only** > > `OAUTH_USER_UNLINKED` is currently delivered only in the staging environment. Production deliveries do not include it yet; this page will be updated when it is rolled out. ```json theme={null} { "eventType": "OAUTH_USER_UNLINKED", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "appId": "55555555-5555-4555-8555-555555555555", "externalReferenceId": "your-reference-123", "reason": "USER_REVOKED", "occurredAt": "2026-10-16T14:02:11.000Z" } ``` | Field | Type | Description | | - | - | - | | `eventType` | String | Always `OAUTH_USER_UNLINKED`. | | `accountId` | String (UUID) | The Fluz account ID whose link was revoked. | | `userId` | String (UUID) | The Fluz user ID whose link was revoked. | | `appId` | String (UUID) | Your application's ID. | | `externalReferenceId` | String \| null | *Optional.* Your user identifier from the OAuth flow, the same value the matching `OAUTH_USER_LINKED` carried. Absent or `null` when the link was created without one. | | `reason` | String (enum) | `USER_REVOKED` when the user removed your app themselves; `ADMIN_REVOKED` when Fluz revoked the link on their behalf. Treat the field as an open string, because new values can be added. | | `occurredAt` | String (ISO 8601) | When the link was revoked. | The payload identifies the user by ID only. It never carries `emailAddress`, `phoneNumber`, or the scopes the link held — read those from the `OAUTH_USER_LINKED` event you already stored. When this event fires, the link is already revoked: * The user's access tokens are rejected, and refreshing them returns `invalid_grant`. Calling [`generateUserAccessToken`](/api-reference/mutations/generate-user-access-token) with the revoked `externalReferenceId` is refused instead of silently creating a new link. * Fluz stops delivering every other event for that user to your app — transactions, KYC and KYB updates, funding source changes, and so on. * `OAUTH_USER_UNLINKED` is sent once per revoked link. Removing an app that is already unlinked produces no second event. * The user can link again at any time through your normal OAuth flow. Re-linking with the same `externalReferenceId` restores the same link, and `OAUTH_USER_LINKED` fires again. Treat `OAUTH_USER_LINKED` as reconnecting the user rather than as a brand-new user. On a business account, each member links your app separately. One member removing it revokes only that member's link, and the event carries that member's `userId`. Other members' links, and their tokens, keep working. ### KYC initiation (`WIDGET_KYC_INITIATION`) Fired when a user begins identity verification. Public/widget apps only. ```json theme={null} { "eventType": "WIDGET_KYC_INITIATION", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "externalReferenceId": "your-reference-123" } ``` ### Transfer completed — customer to app (`WIDGET_DEPOSIT_COMPLETE`) Fired when a user transfers funds to your application. ```json theme={null} { "eventType": "WIDGET_DEPOSIT_COMPLETE", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "externalReferenceId": "your-reference-123", "amount": 25.50 } ``` ### Transfer completed — app to customer (`WIDGET_WITHDRAW_COMPLETE`) Fired when your application transfers funds to a user. ```json theme={null} { "eventType": "WIDGET_WITHDRAW_COMPLETE", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "externalReferenceId": "your-reference-123", "amount": 15.00 } ``` ### Gift card purchase (`WIDGET_PURCHASE_GIFT_CARD`) Fired when a gift card purchase completes through the widget. ```json theme={null} { "eventType": "WIDGET_PURCHASE_GIFT_CARD", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "externalReferenceId": "your-reference-123", "amount": 25.00 } ``` **Common widget payload fields:** `userId` (Fluz user ID), `accountId` (Fluz account ID), `externalReferenceId` (your user identifier from the OAuth flow), and `amount` where applicable. ### KYC status update (`KYC_STATUS_UPDATE`) Fired when a connected user's KYC status changes. Public/OAuth and widget apps only; requires the `VERIFY_KYC` scope. Every payload shares a common envelope; the `verificationData` object's shape is discriminated by `verificationType`, shown per-type below. | Field | Type | Description | | - | - | - | | `eventType` | String | Always `KYC_STATUS_UPDATE`. | | `userId` | String (UUID) | The Fluz user ID whose KYC status changed. | | `accountId` | String (UUID) | The Fluz account ID. | | `previousStatus` / `newStatus` | String (enum) | One of `NOT_REQUIRED`, `PENDING_DOCUMENTS`, `MISSING_1099_INFO`, `PASS`, `PASS_INTERNATIONAL`, `SOFT_DECLINE`, `HARD_DECLINE`, `FAIL`. | | `externalReferenceId` | String | *Optional.* Your user identifier from the OAuth flow. | | `verificationType` | String (enum) | One of `SSN`, `DOCUMENTS`, `SYSTEM`. Discriminates the shape of `verificationData`, shown per-type in the tabs below. | **SSN** Emitted when the status change resulted from SSN-based verification. ```json theme={null} { "eventType": "KYC_STATUS_UPDATE", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "previousStatus": "PENDING_DOCUMENTS", "newStatus": "PASS", "externalReferenceId": "your-reference-123", "verificationType": "SSN", "verificationData": { "userFirstName": "Jane", "userLastName": "Doe", "userDateOfBirth": "1990-01-15T00:00:00.000Z", "lastFourSSNDigits": "6789", "legalAddress": { "streetAddressLine1": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US" } } } ``` | Field | Type | Description | | - | - | - | | `verificationData.userFirstName` | String | User's legal first name. | | `verificationData.userLastName` | String | User's legal last name. | | `verificationData.userDateOfBirth` | String (ISO 8601) | *Optional.* User's date of birth. | | `verificationData.lastFourSSNDigits` | String | *Optional.* Last four digits of the SSN used for verification. | | `verificationData.legalAddress` | Object | *Optional.* See shape below. | **Documents** Emitted when the status change resulted from document-based identity verification. ```json theme={null} { "eventType": "KYC_STATUS_UPDATE", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "previousStatus": "PENDING_DOCUMENTS", "newStatus": "PASS", "externalReferenceId": "your-reference-123", "verificationType": "DOCUMENTS", "verificationData": { "userFirstName": "Jane", "userLastName": "Doe", "userDateOfBirth": "1990-01-15T00:00:00.000Z", "documentType": "PASSPORT", "documentId": "b3f8b6d0-1234-4a5b-9c3d-abcdefabcdef", "documentIssuer": "US", "documentIssueDate": "2020-05-01T00:00:00.000Z", "documentExpireDate": "2030-05-01T00:00:00.000Z", "legalAddress": { "streetAddressLine1": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US" } } } ``` | Field | Type | Description | | - | - | - | | `verificationData.userFirstName` | String | User's legal first name. | | `verificationData.userLastName` | String | User's legal last name. | | `verificationData.userDateOfBirth` | String (ISO 8601) | *Optional.* User's date of birth. | | `verificationData.documentType` | String | *Optional.* Type of document submitted (e.g., `PASSPORT`). | | `verificationData.documentId` | String (UUID) | *Optional.* Fluz's ID for the submitted document. | | `verificationData.documentIssuer` | String | *Optional.* Issuing country/authority. | | `verificationData.documentIssueDate` | String (ISO 8601) | *Optional.* When the document was issued. | | `verificationData.documentExpireDate` | String (ISO 8601) | *Optional.* When the document expires. | | `verificationData.legalAddress` | Object | *Optional.* See shape below. | **System** Emitted for system-driven status changes not tied to a specific SSN or document submission (e.g., automated re-review). `verificationData` itself is optional here — it may be entirely absent. ```json theme={null} { "eventType": "KYC_STATUS_UPDATE", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "previousStatus": "SOFT_DECLINE", "newStatus": "HARD_DECLINE", "externalReferenceId": "your-reference-123", "verificationType": "SYSTEM", "verificationData": { "reason": "Failed secondary AML screening" } } ``` | Field | Type | Description | | - | - | - | | `verificationData` | Object | *Optional.* May be entirely absent. | | `verificationData.reason` | String | *Optional.* Free-text reason for the system-driven change. | `legalAddress`, when present (`SSN` and `DOCUMENTS` types only), has the shape `{ streetAddressLine1, streetAddressLine2 (optional), city, state, postalCode, country }`. ### KYB status update (`KYB_STATUS_UPDATE`) Fired when a connected business's KYB status changes. Public/OAuth apps only; requires the `REGISTER_BUSINESS` scope. ```json theme={null} { "eventType": "KYB_STATUS_UPDATE", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "previousStatus": "IN_PROGRESS", "newStatus": "PASS", "externalReferenceId": "your-reference-123" } ``` | Field | Type | Description | | - | - | - | | `eventType` | String | Always `KYB_STATUS_UPDATE`. | | `accountId` | String (UUID) | The business's Fluz account ID. | | `userId` | String (UUID) | *Optional.* The Fluz user ID associated with the business, when available. | | `previousStatus` / `newStatus` | String (enum) | One of `IN_PROGRESS`, `SUBMITTED_FOR_APPROVAL`, `PASS`, `REVIEW`, `FAILED`, `SOFT_DECLINE`, `HARD_DECLINE`, `PENDING_FORMATION`, `PENDING_DOCUMENTS`, `PASS_INTERNATIONAL`, `WAITING_FOR_SCREENING_RESULT`. | | `externalReferenceId` | String | *Optional.* Your user identifier from the OAuth flow. | ### Funding source status update (`FUNDING_SOURCE_STATUS_UPDATE`) Fired when a user adds, renames or removes a funding source — a bank account, a bank card, or a PayPal account. Requires the `LIST_PAYMENT` scope. **Staging only** for now — not yet delivered in production. ```json theme={null} { "eventType": "FUNDING_SOURCE_STATUS_UPDATE", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "userId": "550e8400-e29b-41d4-a716-446655440000", "externalReferenceId": "usr_8f3d2a91", "changeType": "ADDED", "fundingSourceId": "9f8b7a6c-5d4e-3f21-0a9b-8c7d6e5f4a3b", "fundingSourceType": "BANK_CARD", "brand": "VISA", "lastFour": "4242", "nickname": "Travel card", "status": "ACTIVE", "createdAt": "2026-09-14T17:04:10.000Z", "updatedAt": "2026-09-14T17:04:11.000Z", "changedAt": "2026-09-14T17:04:12.481Z" } ``` | Field | Type | Description | | - | - | - | | `eventType` | String | Always `FUNDING_SOURCE_STATUS_UPDATE`. | | `accountId` | String (UUID) | The Fluz account the funding source belongs to. | | `userId` | String (UUID) | *Nullable.* The user the funding source belongs to. Always present for cards and PayPal accounts; `null` for some bank accounts, which are held at the account level. Route on `accountId`, not `userId`. | | `externalReferenceId` | String | *Optional.* Your user identifier from the OAuth flow. Absent for private-app deliveries and wherever `userId` is `null`. | | `changeType` | String (enum) | One of `ADDED`, `UPDATED`, `DELETED`. | | `fundingSourceId` | String (UUID) | The funding source's Fluz ID — a bank account ID, bank card ID, or PayPal vault ID. Stable across all three change types. | | `fundingSourceType` | String (enum) | One of `BANK_ACCOUNT`, `BANK_CARD`, `PAYPAL`. | | `brand` | String | *Nullable.* The card network for `BANK_CARD`, the institution name for `BANK_ACCOUNT`, `PAYPAL` for a PayPal account. | | `lastFour` | String | *Nullable.* Last four digits of the card or bank account. Always `null` for `PAYPAL`. | | `nickname` | String | *Nullable.* The name the user sees for this source. Always `null` for `PAYPAL`. | | `status` | String | *Nullable.* The source's own status at the time of the change, such as `ACTIVE`. Values differ by type: cards use `ACTIVE` / `INACTIVE` / `PENDING_REMOVAL`, bank accounts `ENABLED` / `DISABLED`, PayPal `ACTIVE` / `REMOVED`. | | `createdAt` / `updatedAt` | String (ISO 8601) | *Nullable.* When the funding source itself was created and last modified. | | `changedAt` | String (ISO 8601) | When this change happened. Use it to order events for the same `fundingSourceId`. | `UPDATED` fires only when a field in this payload actually changed — renaming a source or a status change produces one; routine internal record-keeping does not. Removing a funding source does not delete the row, it marks it inactive, so a `DELETED` event carries the terminal `status` (`INACTIVE`, `DISABLED` or `REMOVED`) rather than the absence of a record. Rely on `changeType` to decide that a source is gone. Full card and bank account numbers are never included in this payload. ## Best practices * **Respond fast, process later.** Return `200` immediately and handle the event asynchronously to avoid timeouts and unnecessary retries. * **Verify every request.** Validate `X-HMAC-Signature` against the raw body using your API key before processing. * **Deduplicate with event IDs.** Track `X-Event-ID` to handle retried/duplicate deliveries. * **Treat enum-like fields as open strings.** New `transactionType`, `channel`, and `declineCategory` values can appear over time; branch on the values you care about and tolerate unknowns. * **Upsert on `OAUTH_USER_LINKED`.** It can fire more than once per user; refresh the stored scope set and `spendAccountAccess` each time rather than assuming first-link-only semantics. * **Disconnect on `OAUTH_USER_UNLINKED`.** Mark the user as disconnected and discard their tokens rather than deleting the record, so a later `OAUTH_USER_LINKED` can reconnect it. Because deliveries can arrive out of order, compare `occurredAt` against the time of the last link you stored before acting on it. * **Key funding sources by `fundingSourceId`.** One event type carries all three change types; branch on `changeType` and order same-source events by `changedAt`. * **Accept unknown fields.** Payloads may gain fields over time; ignore unrecognized ones rather than failing. * **Monitor your endpoint.** Alert on repeated non-`2xx` responses — after 5 failed attempts an event's delivery is abandoned. ## Troubleshooting | Symptom | Likely cause | Fix | | - | - | - | | Not receiving any webhooks | Endpoint unreachable or returning errors | Confirm the URL is correct, publicly accessible over HTTPS, and returns `2xx`. | | Missing certain event types | App lacks the required scopes | Ensure your app holds the scopes listed for that event. | | Public app not receiving events for a user | User hasn't authorized the required scopes | Confirm the user's OAuth grant includes the needed scopes. | | Not receiving `OAUTH_USER_LINKED` or `OAUTH_USER_UNLINKED` | Subscribed on a private app, or not subscribed at all | These events are public/OAuth only. Subscribe to them (or use catch-all) on an OAuth or widget app. `OAUTH_USER_UNLINKED` is delivered in staging only for now. | | Events for one user stopped, and their tokens are rejected | The user unlinked your app | Check for an `OAUTH_USER_UNLINKED` event for that `userId`. Events and tokens resume once the user links again. | | Signature verification fails | Wrong API key, or verifying a re-serialized body | Use your app's API key and verify against the raw request body. | | Duplicate deliveries | Retry after a timeout | Implement idempotency using `X-Event-ID`. | | `spendAccountAccess.userCashBalanceIds` is `null` | The grant is unrestricted (`restricted: false`) | The link covers all of the user's spend accounts, current and future. Call `getWallet` with the user's access token to list them. | | A restricted grant's `userCashBalanceIds` is missing a spend account the user opened after linking | `includesFutureSpendAccounts` is `true`, and opening a spend account does not fire the event | The new spend account is already in the grant. Call `getWallet` with the user's access token for the current list. | | `externalReferenceId` missing on transaction events | Production delivery, private-app delivery, or an OAuth grant created without an external reference | Transaction events carry it in staging only for now. Private apps never carry it. For OAuth apps, supply `external_id` at authorization time — see [Managing External Reference IDs](/managing-external-reference-ids). | | Events stopped coming | 5 consecutive failures exhausted retries | Fix your endpoint; new events resume automatically. Contact support to replay events whose retries were exhausted. | ## Need help? * **Technical issues:** check your endpoint logs and contact support with the `X-Event-ID`. * **Scope questions:** see [Application Scopes](/fluz-dashboard/application-scopes) and [Decline Codes](/features/decline-codes). # Get best offer Source: https://docs.fluz.app/get-best-offer getOfferQuote If your goal is to secure the best cashback rate, the `getOfferQuote` [query](/api-reference/queries/get-offer-quote) will help you find the top available offer for a specific merchant, tailored to the criteria you provide. ## **Response:** The response for the getOfferQuote query returns an Offer object, which contains detailed information about the best available offer for the merchant. ## **Arguments:** **`input (GetOfferQuoteInput!)`**:\ The input object that specifies the criteria for retrieving the best offer. This input is required and should include the merchant's slug and other relevant details. **`GetOfferQuoteInput`** **Fields:** **`merchantSlug (String!)`**:\ The unique slug identifier for the merchant. This field is required. This slug is used to specify the merchant for which the offer is being requested. You can use the `getMerchants` query described earlier to get your merchant's slug. **`denomination (Float!)`**:\ The purchase amount for which the offer is being requested. This field is also required and the denomination value will determine what offers are available. **`paymentMethod (PaymentMethodType):`**\ The payment method that will be used for the purchase. The default is `FLUZPAY`, your available Fluz balance, but other payment methods can be specified depending on the offer's availability. These include: * `BANK_CARD` * `BANK_ACCOUNT` * `PAYPAL` * `FLUZPAY` * `APPLE_PAY` * `GOOGLE_PAY` ## Example Usage: Here’s how you might structure a getOfferQuote query: ```graphql theme={null} query getOfferQuote($input: GetOfferQuoteInput!) { getOfferQuote(input: $input) { offeringMerchantId offerId type hasStockInfo offerRates { ...OfferRateFragment } denominationsType termsAndConditions stockInfo { ... on StockInfoVariableType { __typename description maxDenomination minDenomination } ... on StockInfoFixedType { __typename denomination availableStock } } } } ``` ### Response Details The `Offer` object represents the offer details associated with the merchant. **`offeringMerchantId (UUID)`** : The unique id for the offering merchant. **`offerId (UUID!)`**: The unique id for the offer. **`type (OfferType)`**: Indicates the type of the offer. This could be a standard gift card offer or an exclusive rate offer. \[Example: GIFT\_CARD\_OFFER, EXCLUSIVE\_RATE\_OFFER]. **`hasStockInfo (Boolean)`**: Specifies whether the offer includes stock information. If true, the stockInfo field will be populated with available denominations. **`denominationsType (OfferDenominationType)`**: Defines the type of denominations available for the offer. This field is used to specify how the offer's value is structured, such as fixed or variable denominations. **`termsAndConditions (String)`**: The terms and conditions text for this offer (legal language, restrictions, expiration policies). Use this to surface T\&C to end users before they purchase a gift card. **`stockInfo ([StockInfoType]!)`** StockInfo is a list of available denominations for a specific offer. This field object is only available if the offer's hasStockInfo is true. This requires Fluz to confirm the inventory. Keep in mind, response time varies by vendors. The stockInfo field will conditionally return info depending on the `VARIABLE` or `FIXED` type. To query for stockInfo, you will need to utilize an inline fragment using the `... on`keyword. This will return either `StockInfoVariableType` if the denomination type is `VARIABLE` or `StockInfoFixedType` if the denomination type is `FIXED`. Sample Response: ```json theme={null} { "data": { "getOfferQuote": { "offeringMerchantId": "7b4280a6-dadc-4cf3-a99a-70d291e43c1c", "offerId": "7b4280a6-dadc-4cf3-a99a-70d291e43c1c", "type": "GIFT_CARD_OFFER", "hasStockInfo": true, "offerRates": [], "denominationsType": "VARIABLE", "termsAndConditions": "Card valid only for purchases on the merchant's U.S. website. Cannot be used to purchase other gift cards. Not redeemable for cash except where required by law.", "stockInfo": [ { "__typename": "StockInfoVariableType", "description": "Available denominations range from $5 to $200", "maxDenomination": "200", "minDenomination": "5" } ] } } } ``` For more detail, refer to the [How the GraphQL API works](/concepts/graphql) article. The `OfferRate` object represents the specific reward rates and conditions associated with an offer. This object contains detailed information about the rewards that users can receive when they take advantage of an offer, as well as the conditions under which these rewards apply: **`maxUserRewardValue (Float)`**:\ The maximum value cashback that a user can receive. This field defines the upper limit of the reward amount that a user can earn for a specific offer. **`cashbackVoucherRewardValue (Float)`**:\ The cashback value after a boost is applied to the offer. Typically this is a 25% cashback boost on the first \$10 of your spend. **`boostRewardValue (Float)`**:\ When the cashback value on an offer has been increased, it will display here. These offers are an additional incentive that may be offered to users for a period of time. **`displayBoostReward (Boolean)`**:\ Indicates whether the boost reward should be displayed to the user. This field is used to control the visibility of the boost reward in the user interface. **`denominations ([String])`**:\ An array of denominations that are eligible for the offer. These denominations represent specific amounts (e.g., "10", "25", "50") that are applicable for the offer, defining the monetary values that qualify for the rewards. **`allowedPaymentMethods ([String])`**:\ An array of payment methods that are allowed for this offer. This field specifies which payment methods (e.g., "CREDIT\_CARD", "PAYPAL") can be used by the user to take advantage of the offer. > 🚧 Cashback rates are subject to change. > > We do our best to always give our customers the best offers available. This means that our rates change regularly. Always confirm the rate before making a purchase. # Get catalog Source: https://docs.fluz.app/get-catalog getMerchants Use the `getMerchants` GraphQL query to retrieve the current list of available merchants and their offers from the Fluz catalog. This is the primary way to discover which merchants are available and what kinds of deals they offer. You can refine the results using optional input arguments: * `name`: Filter merchants by a specific name. * `paginate`: Control the number of results per page (`limit`) and the starting point (`offset`). * `offerTypes`: Specify whether you want `giftCardOffer`s, `cardLinkedOffer`s, or both using boolean flags (e.g., `{ giftCardOffer: true, cardLinkedOffer: false }`). * `categoryId`: Return only merchants in one shopping category, for example Dining or Travel. Category ids come from [`getMerchantCategories`](/api-reference/queries/get-merchant-categories). An unknown or inactive category returns an empty list. * `filterBy`: An object used to apply specific filters to the offers within each merchant. If a merchant has no offers remaining after filtering, it is excluded from the final results.\ Available filters: 1. `deliveryFormat`: Filters gift card offers by their delivery method.\ Allowed values: `URL`, `CODES`, `PIN_AS_CODE`, `PIN_WITH_URL`. > 📘 Retrieving the entire merchant catalog without filters can be time-consuming. We recommend fetching the full, unfiltered catalog only once per day. For more frequent updates or specific lookups, use the `name` or `offerTypes` filters. > 📘 **Notes on pagination:** > > The default and max limit for pagination is 20. The result will not necessarily return the amount that you define as the limit. For example, you may request a limit of 10, but the response may only have 7 results. > > When an offset is included, remember to include the limit amount in your offset calculation for the next batch. If no limit is specified, you will need to offset by the default limit of 20 (e.g. offset+20). > 📘 **Fetching the full merchant catalog:** > > **If you want all available merchants, you must paginate until the API returns an empty array.** > > Steps to fetch the full catalog: > > 1. Call getMerchants (offset = 0, limit = 20). > 2. Append the returned merchants to your local list. > 3. Increase the offset by your limit (offset += 20). > 4. Repeat the request. > 5. Stop only when the API returns an empty array (\[]). ## Basic query structure Here’s a flexible query structure using variables. You can adjust the `offerTypes` variable to fetch the specific offers you need. This example requests only the most common, basic fields. **Sample request:** ```graphql theme={null} # Define the query with variables query GetMerchantCatalogBasic( $paginate: OffsetInput, $offerTypes: OfferTypesInput, # Controls which offer types are returned $filterBy: FilterByInput ) { getMerchants( paginate: $paginate, offerTypes: $offerTypes, filterBy: $filterBy ) { # --- Common Merchant Fields --- merchantId name slug logoUrl # Merchant logo image URL faceplateUrl # Rectangular card art/faceplate image URL # --- Shopping categories (a merchant can be in more than one) --- categories { categoryId name } # --- Basic Offer Info (Common to all types) --- offers { offeringMerchantId offerId type # Crucial field to identify offer type barcodeType # Barcode format (NONE, C128, PDF417, QRCODE) # Request offer-specific fields like offerRates, stockInfo, # or cloDetails here based on expected types. # See subpages for details. } } } # Example Variables (Fetch Gift Cards Only): # { # "paginate": { "limit": 20, "offset": 0 }, # "offerTypes": { "giftCardOffer": true, "cardLinkedOffer": false } # } # Example Variables (Fetch CLOs Only): # { # "paginate": { "limit": 20, "offset": 0 }, # "offerTypes": { "giftCardOffer": false, "cardLinkedOffer": true } # } # Example Variables (Filter by deliveryFormat: CODES): # { # "paginate": { "limit": 20, "offset": 0 }, # "filterBy": { "deliveryFormat": URL } # } ``` **Sample response structure (gift card example):** ```json json theme={null} { "data": { "getMerchants": [ { "merchantId": "123e4567-e89b-12d3-a456-426614174000", "name": "Example Merchant", "slug": "example-merchant", "logoUrl": "https://storage.googleapis.com/.../example-merchant-logo.jpg", "faceplateUrl": "https://storage.googleapis.com/.../example-merchant-faceplate.png", "categories": [ { "categoryId": "223e4567-e89b-12d3-a456-426614174001", "name": "Dining" } ], "offers": [ { "offeringMerchantId": "3c7b4d5e-6f7g-8h9i-10jk-11l12m13n14o", "offerId": "1a2b3c4d-5e6f-7g8h-9i10-jk11l12m13n14", "type": "GIFT_CARD_OFFER", "barcodeType": "C128" // Other fields like offerRates, stockInfo would be here // if requested and applicable. } ] } ] } } ``` ## Response details The `Merchant` object returned by the `getMerchants` query includes the following fields: | Field name | Type | Description | | :- | :- | :- | | merchantId | UUID! | The unique identifier for the merchant. This ID is essential for referencing the merchant in other API queries or transactions. | | name | String | The name of the merchant. This is the display name that users will recognize when selecting a merchant for gift card usage or virtual card charges. | | slug | String | A URL-friendly version of the merchant's name. The slug is often used in web addresses or as a reference in the user interface for ease of use. | | logoUrl | String | The URL of the merchant's logo image. Typically a square (1:1) image suitable for display in merchant lists and headers. Returns null if not available. | | faceplateUrl | String | The URL of the merchant's rectangular faceplate/card art image. This is ideal for displaying when no barcode is present (barcodeType === "NONE") or as a card background. Returns null if not available. | | categories | \[MerchantCategory!]! | The shopping categories this merchant belongs to, ordered by display priority. A merchant can belong to more than one, and the list is empty when the merchant is uncategorized. Each entry has a `categoryId` and a `name`. | | offers | \[Offer] | An array of `Offer` objects that represent the promotions, discounts, or deals available at the merchant. Each Offer object provides additional details such as the discount amount, expiration date, and any applicable conditions. | Common Offer Fields (within offers array): These fields are present in every `Offer` object, regardless of its type. | Field Name | Type | Description | | :- | :- | :- | | offeringMerchantId | UUID! | A unique identifier for the merchant offering the specific deal or discount. This is a required field and must be a valid UUID. | | offerId | UUID! | A unique identifier for the offer itself. | | type | String! | Indicates the type of the offer. This could be a standard gift card offer or an exclusive rate offer. | | deliveryFormat | DeliveryFormatType | Specifies the method by which the offer will be delivered to the user. This determines how the user will receive and redeem the offer. For card-linked offers, this field will be null as the offer is automatically applied. | | barcodeType | BarcodeTypeEnum | Specifies the barcode format used for this offer. Possible values: NONE (no barcode), C128 (Code 128), PDF417, QRCODE. Use this to determine whether to render a barcode or display alternative visuals like faceplateUrl. | ## Filter by category Merchants are grouped into the same shopping categories used in the Fluz catalog, such as Dining, Travel, and Grocery. Call [`getMerchantCategories`](/api-reference/queries/get-merchant-categories) once to get the list, then pass a `categoryId` to `getMerchants` to show one category at a time. ```graphql theme={null} query GetMerchantCategories { getMerchantCategories { categoryId name } } ``` ```graphql theme={null} query GetMerchantsByCategory($categoryId: UUID, $paginate: OffsetInput) { getMerchants(categoryId: $categoryId, paginate: $paginate) { merchantId name slug categories { categoryId name } } } # Example Variables: # { # "categoryId": "223e4567-e89b-12d3-a456-426614174001", # "paginate": { "limit": 20, "offset": 0 } # } ``` Notes: * `categoryId` combines with the other arguments, so you can filter by category and `name` in the same call. * A `categoryId` that isn't a UUID returns `APPLICATIONS-0012` (`InvalidArgument`) with the message `categoryId must be a UUID. Category ids come from getMerchantCategories.` A blank string is the exception: it removes only the category filter — `name`, `offerTypes`, `filterBy` and pagination still apply. * Store the `categoryId` rather than the name if you cache the mapping — ids are stable, and a category can be renamed. * Category assignments in the sandbox are test data and won't match production. * The same pagination rules apply to a filtered call as to an unfiltered one. ## Offer type specific details The real details of an offer depend on its `type`. You need to request and interpret different fields based on the offer type: * **For gift card offers (`GIFT_CARD_OFFER`, `EXCLUSIVE_RATE_OFFER`):** * Key fields include `offerRates`, `stockInfo`, `hasStockInfo`, `denominationsType`, `allowedPaymentMethods`. * See the [**Gift Card Offers**](/get-gift-card-offers) subpage for a detailed explanation and query examples. * **For card linked offers (`CARD_LINKED_OFFER`):** * The primary field is `cloDetails`, which contains rates, periods, and conditions. * See the [**Card Linked Offers**](/features/card-linked-offers) subpage for a detailed explanation and query examples. # Get gift card offers Source: https://docs.fluz.app/get-gift-card-offers **Scope Required:** `LIST_OFFERS` ([API Reference Link](/api-reference/queries/get-merchants)) This page details how to retrieve and understand Gift Card Offers using the `getMerchants` query. Gift Card offers typically include types like `GIFT_CARD_OFFER` and `EXCLUSIVE_RATE_OFFER`. To fetch *only* Gift Card offers, use the `offerTypes` input argument set to `{ giftCardOffer: true, cardLinkedOffer: false }`. ## Detailed Query for Gift Card Offers This query requests fields specifically relevant to Gift Cards, including reward rates (`offerRates`) and stock availability (`stockInfo`). **Sample Request:** ```graphql theme={null} # Define the query with variables query GetGiftCardMerchants( $name: String, $paginate: OffsetInput, $offerTypes: OfferTypesInput # Define the input type for offerTypes (optional) ) { getMerchants( name: $name, paginate: $paginate, offerTypes: $offerTypes # Pass the variable here (optional) ) { merchantId name slug logoUrl # Merchant logo image URL faceplateUrl # Rectangular card art/faceplate image URL shortDescription # Short marketing description of the merchant offers { offerId exclusiveRateId # Optional, only returns for type = EXCLUSIVE_RATE_OFFER type deliveryFormat # How the gift card will be delivered (URL, CODES, etc.) barcodeType # Barcode format (NONE, C128, PDF417, QRCODE) hasStockInfo denominationsType # FIXED or VARIABLE termsAndConditions # Offer T&C text (legal language, restrictions, expiration) # --- Offer Rates (Relevant for Gift Cards) --- offerRates { maxUserRewardValue cashbackVoucherRewardValue boostRewardValue displayBoostReward denominations allowedPaymentMethods } # --- Stock Info (Relevant for Gift Cards) --- stockInfo { ... on StockInfoVariableType { __typename description maxDenomination minDenomination } ... on StockInfoFixedType { __typename denomination availableStock } } # --- CLO Details (Will be null for Gift Cards) --- # cloDetails { currentRateType } # Can query, but expect null } } } # Example Variables to pass with the query: # { # "paginate": { "limit": 20, "offset": 0 }, # "offerTypes": { "giftCardOffer": true, "cardLinkedOffer": false } # } ``` Please note that due to rate-limiting, you might need to paginate and call the merchants list a few times. ## Sample Response (When Filtering for Gift Card Offers): The response will contain merchants, but the offers array for each merchant will only include offers matching the offerTypes filter (in this case, Gift Card Offers). ```json JSON theme={null} { "data": { "getMerchants": [ { "merchantId": "123e4567-e89b-12d3-a456-426614174000", "name": "Best Buy", "slug": "best-buy", "logoUrl": "https://storage.googleapis.com/.../best-buy-logo.jpg", "faceplateUrl": "https://storage.googleapis.com/.../best-buy-faceplate.png", "shortDescription": "America's leading destination for tech, gadgets, and home electronics.", "offers": [ { "offeringMerchantId": "255f8245-02c7-4817-901e-15fe265f6968", "offerId": "1a2b3c4d-5e6f-7g8h-9i10-jk11l12m13n14", "exclusiveRateId": null, "type": "GIFT_CARD_OFFER", "deliveryFormat": "CODES", "barcodeType": "C128", "hasStockInfo": true, "offerRates": [ { "maxUserRewardValue": 50.0, "cashbackVoucherRewardValue": 5.0, "boostRewardValue": 10.0, "displayBoostReward": true, "denominations": ["10", "500"], "allowedPaymentMethods": ["CREDIT_CARD", "DEBIT_CARD"] } ], "denominationsType": "FIXED", "termsAndConditions": "Card valid only at Best Buy U.S. retail stores and BestBuy.com. Cannot be used to purchase other gift cards. Not redeemable for cash except where required by law.", "stockInfo": [ { "__typename": "StockInfoFixedType", "denomination": 500, "availableStock": 200 } ] } ] }, { "merchantId": "223e4567-e89b-12d3-a456-426614174001", "name": "H&M", "slug": "h&m", "logoUrl": "https://storage.googleapis.com/.../hm-logo.jpg", "faceplateUrl": null, "shortDescription": "Affordable fashion for women, men, and kids — from staples to seasonal collections.", "offers": [ { "offeringMerchantId": "3c7b4d5e-6f7g-8h9i-10jk-11l12m13n14o", "offerId": "2a2b3c4d-5e6f-7g8h-9i10-jk11l12m13n15", "exclusiveRateId": "0c8e6beb-5d62-4a0c-ae94-b889395f1e2d", "type": "EXCLUSIVE_RATE_OFFER", "deliveryFormat": "URL", "barcodeType": "NONE", "hasStockInfo": false, "offerRates": [ { "maxUserRewardValue": 30.0, "cashbackVoucherRewardValue": 3.0, "boostRewardValue": 8.0, "displayBoostReward": false, "denominations": ["20", "50", "100"], "allowedPaymentMethods": ["CREDIT_CARD", "PAYPAL"] } ], "denominationsType": "FIXED", "termsAndConditions": "Redeemable online at hm.com or any H&M retail store in the U.S. Cannot be redeemed for cash except where required by law.", "stockInfo": [] } ] } ] } } ``` ### Understanding the 'Offers' array: The offers field within the `Merchant` object contains an array of `Offer` objects. The offer object contains information about specific offers available from merchants in the Fluz catalog. Below are the fields included: Field Name Type Description offeringMerchantId UUID! A unique identifier for the merchant offering the specific deal or discount. This is a required field and must be a valid UUID. offerId UUID! A unique identifier for the offer itself. exclusiveRateId UUID The exclusive\_rate\_id for the offer. Only returned for type = EXCLUSIVE\_RATE\_OFFER. This value can be passed in to purchaseGiftCard to specify the exclusive rate you want to purchase with. type String! Indicates the type of the offer. This could be a standard gift card offer or an exclusive rate offer. deliveryFormat DeliveryFormatType Specifies how the gift card will be delivered to the user. Possible values: URL, CODES, PIN\_AS\_CODE, PIN\_WITH\_URL, CODE\_WITH\_PREFIX. This determines how the user will receive and redeem the gift card. barcodeType BarcodeTypeEnum Specifies the barcode format for this offer. Possible values: NONE (no barcode - consider displaying faceplateUrl instead), C128 (Code 128), PDF417, QRCODE. Use this to determine whether to render a barcode in your UI. hasStockInfo Boolean Specifies whether the offer includes stock information. If true, the stockInfo field will be populated with available denominations. offerRates \[OfferRate] Represents the specific reward rates and conditions associated with an offer. denominationsType OfferDenominationType Defines the type of denominations available for the offer. This field is used to specify how the offer's value is structured, such as fixed or variable denominations. termsAndConditions String The terms and conditions text for this offer (legal language, restrictions, expiration policies). Use this to surface T\&C to end users before they purchase a gift card. stockInfo \[StockInfoType]! StockInfo is a list of available denominations for a specific offer. It's only available if the offer hasStockInfo. This requires Fluz to confirm the inventory, response time varies by vendors. The stockInfo field will conditionally return info depending on the `VARIABLE` or `FIXED` type. To query for stockInfo, you will need to utilize an inline fragment using the `... on`keyword. This will return `StockInfoVariableType` if the denomination type is `VARIABLE` or `StockInfoFixedType` if the denomination type is `FIXED`. For more detail, refer to the [How the GraphQL API works](/concepts/graphql) article. ### Understanding the 'Offer rate': The `OfferRate` object represents the specific reward rates and conditions associated with an offer. This object contains detailed information about the rewards that users can receive when they take advantage of an offer, as well as the conditions under which these rewards apply: | Field Name | Type | Description | | :- | :- | :- | | maxUserRewardValue | Float | The maximum value cashback that a user can receive. This field defines the upper limit of the reward amount that a user can earn for a specific offer. | | cashbackVoucherRewardValue | Float | The cashback value after a boost is applied to the offer. Typically this is a 25% cashback boost on the first \$10 of your spend. | | boostRewardValue | Float | When the cashback value is increased on an offer, it will display here. These offers are an additional incentive that may be offered to users for a period of time. | | displayBoostReward | Boolean | Indicates whether the boost reward should be displayed to the user. This field is used to control the visibility of the boost reward in the user interface. | | denominations | \[String] | An array of denominations that are eligible for the offer. These denominations represent specific amounts (e.g., "10", "25", "50") that are applicable for the offer, defining the monetary values that qualify for the rewards. | | allowedPaymentMethods | \[String] | An array of payment methods that are allowed for this offer. This field specifies which payment methods (e.g., "CREDIT\_CARD", "PAYPAL") can be used by the user to take advantage of the offer. | # Inventory on stocked offers Source: https://docs.fluz.app/get-inventory Some of the denominations on select gift card brands are stocked. Other merchants only offer variable value gift cards that are generated in real time. If a gift card offer is stocked, that is generally a better rate or higher purchasing limits on that specific offer. It will be specific denominations that are generally stocked on a gift card brand. In order to see our inventory you will need to call the get stock information API call. That will allow you to see the exact quantity available for those specific denominations. Some merchants have both variable and stocked gift card offers. Please determine which type of offer you are looking to purchase based on pricing and total volumes. Using the `Offer` object from either the `getOfferQuote` or `getMerchants` query, you can determine stock availability. * If **`hasStockInfo`** is true, the `stockInfo` field will display a list of all available denominations that are currently in stock. * If **`hasStockInfo`** is false, any value denominations in the `offerRates` `denominations` field will be in stock. * If the **`denominationsType`** is `"VARIABLE"`, you can purchase any value between the minimum and maximum denomination. * **`stockInfo`** is a list of available denominations for a specific offer. This field object is only available if the offer's hasStockInfo is true. This requires Fluz to confirm the inventory. Keep in mind, the response time varies by vendors. The stockInfo field will conditionally return information depending on the `VARIABLE` or `FIXED` type. To query for stockInfo, you will need to utilize an inline fragment using the `... on`keyword. This will return `StockInfoVariableType` if the denomination type is `VARIABLE` or `StockInfoFixedType` if the denomination type is `FIXED`. In your getOfferQuote and getMerchants queries, you will have to include both StockInfoVariableType and StockInfoFixedType fragments. For more detail, refer to the [How the GraphQL API works](/concepts/graphql) article. ```json JSON theme={null} "offerRates":[{ "denominations": ["10","1000"] }] ``` * If the **`denominationsType`** is `"FIXED"`, you will either: * If `hasStockInfo` is true, purchase a specific value in the `stockInfo` field. It will return either `StockInfoVariableType` or `StockInfoFixedType` conditionally depending on the `denominationsType`. ```json StockInfoVariableType theme={null} "stockInfo": [ { "__typename": "StockInfoVariableType", "description": "Available denominations range from $5 to $200", "maxDenomination": "200", "minDenomination": "5" } ] , ``` ```json StockInfoFixedType theme={null} "stockInfo": [ { "__typename": "StockInfoFixedType", "availableStock": "200", "denomination": "5" } ] ``` * If `hasStockInfo` is false, purchase a specific value in the `offerRates` `denominations` field. # Go from zero to your first API call Source: https://docs.fluz.app/get-started One GraphQL endpoint. Create your account, generate credentials, exchange them for a scoped access token, and make your first purchase in staging. ## Setup steps **1. Prepare your accounts** Create your Fluz account and register your first application. [Prepare accounts →](/get-started/prepare-accounts) **2. Authenticate** Get credentials and generate a scoped user access token. [API credentials →](/get-started/api-credentials) **3. Run a quickstart** Pick an end-to-end happy path below and run it in the sandbox — every quickstart goes from a scoped token to a finished, verifiable result. [Choose a quickstart ↓](#quickstarts) ## Quickstarts Each quickstart is the complete flow for one job — copy‑paste runnable, staged so every step verifies the last, and done in minutes. > **[Your first gift card purchase](/quickstart/first-gift-card)** > > Deposit funds, browse the merchant catalog, buy a gift card, and reveal its redemption details. > **[Your first virtual card purchase](/quickstart/create-and-spend-with-a-virtual-card)** > > Pick a card program, issue a card with the right spend controls, reveal it, and track its activity. > **[Move money through a wallet](/quickstart/move-money-through-a-wallet)** > > Deposit, transfer between accounts, and withdraw — one ledger, verified at every step. > **[Onboard customers](/quickstart/onboard-customers)** > > Register a user, run KYC, and connect their account so you can transact on their behalf. > **[Onboard & verify a business](/quickstart/onboard-businesses)** > > Configure a business-only app, register the legal entity and its owners, and track KYB to an approved account. > **[Testing safely](/test-merchants)** > > Staging is stocked with test merchants, bank cards, bank accounts, KYC identities, and addresses — no real money, no real PII. # Accounts, applications & environments Source: https://docs.fluz.app/get-started/accounts-and-applications The model to understand before your first call: logins, personal and business accounts, application types and statuses, and which token each operation needs. Most first-week integration problems come from one of four mix-ups: which **account** a token belongs to, which **type** of application you created, what **status** that application is in, and which **environment** you are calling. This page explains all four. ## Logins and accounts A Fluz **user** is a login: one person with an email, phone number, PIN, and identity verification. A user can hold several **accounts**: | Account | How it comes to exist | Verified by | Token account type | | :- | :- | :- | :- | | **Personal account** | Created automatically when the user signs up or is registered with `registerUser` | KYC on the person | `CONSUMER` | | **Business account** | Created by `registerBusiness` and approved through KYB. A user can own several | KYB on the entity and its owners | `BUSINESS` | > [!WARNING] > > A personal account is never *converted* into a business account. A business account is a separate account, owned by the user, that comes into existence through KYB. The personal account keeps existing alongside it. "Personal" (in the dashboard) and `CONSUMER` (in the API) mean the same thing. Wallets, spend accounts, cards and transactions all belong to an **account**, not to the user. The same user acting through their personal account and through their business account sees two separate ledgers. ## Every token is bound to one account `generateUserAccessToken` takes a `userId` **and** an `accountId`. The `accountId` decides which account the token acts as: * the user's personal `accountId` gives a `CONSUMER` token; * a business `accountId` gives a `BUSINESS` token. To see every account a user holds, and each account's ID, use [Get Accounts](/recipes/get-accounts). Most operations accept either token type and act on whichever account the token belongs to. A few require one type specifically: | Operation | Required token account type | Why | | :- | :- | :- | | `getBusinessCategories` | `CONSUMER` | Called before the business exists | | `registerBusiness` | `CONSUMER` | The person applies; the business doesn't exist yet | | `getBusiness` | `BUSINESS` | Reads the business that the token belongs to | | `requestOwnerDocumentVerificationLink` | `BUSINESS` | Acts on that business's owner roster | Sending the wrong type returns `AUTH-0002` with a message naming the type required, for example `getBusinessCategories requires account type CONSUMER`. Mint a token with the other `accountId` and retry. ## Two kinds of application Where you click in the **Developers** area decides what your application can do. | Created from | Can act on | Use it for | | :- | :- | :- | | **Create new app** | Only the developer's own account | Scripts and back-office automation on your own Fluz account | | **Templates → OAuth Integration** | Other users' accounts, once they authorize you | Platforms: registering customers, running KYC/KYB, issuing cards for them | > [!NOTE] > > If you will create users, verify them, or issue cards on anyone's behalf other than your own, you need an **OAuth Integration** application. An app made with **Create new app** cannot be upgraded into one. Create a new app from the template instead. ## Application status New applications do not start active. | Status | Who the app can act for | | :- | :- | | `PERSONAL` | Only the developer who created it | | `REVIEW` (shown as **In Review**) | Only the developer who created it | | `ACTIVE` | Other Fluz users, through OAuth and platform calls such as `registerUser` | Fluz moves applications to `ACTIVE`; it isn't self-serve. After you create an application, send its name or ID to your Fluz contact. Until then, calls that act for other users return: ```text theme={null} AUTH-0030 Application is not active (current status: PERSONAL) ``` > [!WARNING] > > `current status: PERSONAL` in this error is the **application's** status, not your account type. Converting or switching accounts will not fix it. Ask Fluz to activate the application. Some capabilities are also enabled per application on top of `ACTIVE`, for example `registerUser` and the `VERIFY_KYC` scope. Each environment's application is enabled separately. ## Environments Staging and live are separate systems: | | Staging (sandbox) | Live | | :- | :- | :- | | Web app | `uni.staging.fluzapp.com` | `fluz.app` | | Developers area | `uni.staging.fluzapp.com/for-developers` | `fluz.app/for-developers` | | GraphQL endpoint | `transactional-graph.staging.fluzapp.com/api/v1/graphql` | `transactional-graph.fluzapp.com/api/v1/graphql` | Each environment has its own users, accounts, applications, and API keys. A key from one never works in the other. An application you create while logged in to `fluz.app` is a **live** application, even if you only intend to test with it. ## Putting it together A platform that onboards businesses, such as a card program for its own clients, usually looks like this in staging: 1. You have an **OAuth Integration** application, owned by your company's account, moved to `ACTIVE` by Fluz. 2. For each customer, you `registerUser` the person, verify them, and send them through OAuth. Your token for them is `CONSUMER`. 3. With that `CONSUMER` token you call `getBusinessCategories` and `registerBusiness`. 4. Once KYB approves the business, you mint a `BUSINESS` token (same `userId`, business `accountId`) and issue cards and move money on the business. See [Onboard & Verify a Business](/quickstart/onboard-businesses) for the runnable version. # API credentials and access tokens Source: https://docs.fluz.app/get-started/api-credentials Exchange your API key for a short-lived, scoped user access token. ## Obtain your API credentials Once your application is created in the Developer Console, select it to find its **API Key**, **User ID**, and **Account ID**. > [!NOTE] > > **Where to find them:** the [Developers page](https://uni.staging.fluzapp.com/developers), inside the application you created. These credentials power API key authorization — primarily administrative calls and generating user access tokens. ![Obtain API credentials](https://files.readme.io/d1b78cd2ecfef1f945a9685f81efe0a8b30b7324d32d5ee62e610bced35585b4-obtain_api_creds.gif) Every Fluz API request uses a **user access token** in the `Authorization` header. You mint the token by calling the `generateUserAccessToken` mutation with your **API Key** in the `Authorization: Basic ` header, passing the **User ID** and **Account ID** from the console as arguments. ## Generate an access token ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "query": "mutation ($userId: UUID, $accountId: UUID, $scopes: [ScopeType!]!) { generateUserAccessToken(userId: $userId, accountId: $accountId, scopes: $scopes) { token refreshToken scopes } }", "variables": { "userId": "", "accountId": "", "scopes": ["LIST_OFFERS", "PURCHASE_GIFTCARD", "REVEAL_GIFTCARD"] } }' ``` The response contains the access token, a refresh token, and the scopes the token carries: ```json theme={null} { "data": { "generateUserAccessToken": { "token": "eyJhbGciOi...", "refreshToken": "eyJhbGciOi...", "scopes": ["LIST_OFFERS", "PURCHASE_GIFTCARD", "REVEAL_GIFTCARD"] } } } ``` > [!NOTE] > > Select `refreshToken` explicitly. If you request only `token` and `scopes`, you never receive one, and your only option when the access token expires is to mint a new one from scratch. See [Refresh an expired access token](/get-started/refresh-expired-access-token). ### Identifying the user `scopes` is the only always-required argument. Identify the user in one of two ways: **userId + accountId** `UUID` For applications operating on **your own account**. Both values are shown on your application in the Developer Console. **externalReferenceId** `String` For **OAuth applications** operating on a customer's account — your own identifier for that user, the same value passed as `external_id` during the OAuth authorization flow. When you provide it, `userId` and `accountId` are optional. See [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow). **seatId** `UUID` Optional. Selects which seat transacts. Defaults to the most recently created seat. ## Use the token Attach the token to every GraphQL request against the transactional graph: ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"query":"query { getMerchants(name: \"Burger King\") { name slug } }"}' ``` > [!WARNING] > > Never ship your API key to a browser or mobile client. Mint access tokens server-side and forward only the token to the client if you must. ## If the token request returns 401 A rejected API key returns the same message whether it is unknown, malformed, or from the wrong environment: ```json theme={null} { "error": "Error verifying API basic token" } ``` The one exception is a real key on an application that has been disabled — that returns a `403` naming the status instead: `Application is not in a valid status: .` Work through these in order. > **You are using credentials from the wrong environment** > > This is the most common cause. Staging and live have **separate applications and separate credentials** — a key created in one is unknown to the other, and the error looks identical either way. > > Check where you created the application: > > | Console | Environment | Endpoint the key works against | > | - | - | - | > | `uni.staging.fluzapp.com/developers` | Staging | `transactional-graph.staging.fluzapp.com` | > | `fluz.app/for-developers` | Live | `transactional-graph.fluzapp.com` | > > A live key sent to the staging endpoint fails here, and the reverse is also true. If you only have a live application, register a second one in staging. > **The API key does not match the application** > > Your API key is a base64-encoded `app_id:app_secret` pair. Copy it whole from the console rather than reassembling it, and do not base64-encode it again — it is already encoded. Send it verbatim: > > ``` > Authorization: Basic > ``` > > If the key was regenerated in the console, older copies stop working immediately. > **The application is not in an accepted state** > > Applications only authenticate while active. A deleted application is indistinguishable from a bad key — the same `401` comes back. A **disabled** application is the one case that looks different: the response is a `403` with `Application is not in a valid status: .` Confirm the application still exists in the console for the environment you are calling and has not been disabled. > **The userId or accountId belongs to a different application** > > `userId` and `accountId` must be the values shown on the same application as the API key. Mixing IDs from one application with the key from another fails. > [!TIP] > > Confirm the endpoint host matches the console you created the credentials in. Environment mismatch accounts for most first-run `401`s. ## Mint a new token before expiry Access tokens are short-lived. Mint a new one before the current one expires — don't wait for a `401`. See [Refresh an expired access token](/get-started/refresh-expired-access-token) for the exact call, and [Authentication](/concepts/authentication) for the full flow, including OAuth grants for customer-scoped tokens. ## Next steps > **[Quickstart: your first gift card purchase](/quickstart/first-gift-card)** > > Run the full happy path end to end — deposit funds, buy a gift card, and reveal it in the sandbox. > **[Replace an expired token](/get-started/refresh-expired-access-token)** > > Mint a fresh access token when the current one expires. # Make your first API call Source: https://docs.fluz.app/get-started/first-api-call Run an authenticated GraphQL request against staging and confirm your token works. With an access token in hand, you're ready to hit the API. Every call goes to a single GraphQL endpoint — the token decides whose account you're operating on. ## The endpoint | Environment | Endpoint | | - | - | | Staging | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | | Live | `https://transactional-graph.fluzapp.com/api/v1/graphql` | There are no versioned REST paths and no per-capability base URLs. You send every query and mutation to the same address. ## Confirm your token works `getMerchants` is the cheapest way to prove your setup end to end. It reads nothing sensitive, moves no money, and needs only the `LIST_OFFERS` scope. ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"query":"query { getMerchants(name: \"Burger King\") { name slug } }"}' ``` A working token returns the merchant: ```json theme={null} { "data": { "getMerchants": [ { "name": "Burger King", "slug": "burger-king" } ] } } ``` That single response confirms four things at once: your endpoint is right, your token is valid, it carries the scope the query needs, and it is scoped to an account that can read the catalog. > [!NOTE] > > **An empty array is not an error.** `getMerchants` filters by name, so a merchant that isn't in the staging catalog returns `[]` with no `errors` block. If you get an empty result, try the query without a `name` argument to see what staging currently carries. ## When it doesn't work > **401 Unauthorized** > > The token was rejected before the query ran. Most often the credentials came from the wrong environment — staging and live have separate applications. See [If the token request returns 401](/get-started/api-credentials#if-the-token-request-returns-401). > **AUTH-0031 Insufficient permissions** > > The token is valid but lacks the scope the operation needs. The message names it, for example `getMerchants requires LIST_OFFERS`. Mint a new token with that scope added — scopes are fixed at mint time and cannot be widened afterwards. > **GRAPHQL\_VALIDATION\_FAILED** > > The query doesn't match the schema — usually a field that doesn't exist or a wrong argument shape. The response names the offending field. Check the operation in the [API reference](/api-reference/overview). > > The same code comes back with HTTP `400` when a query exceeds the [query cost limits](/concepts/rate-limits). The message then reads `Query Cost limit of 7000 exceeded`; select fewer fields or split the query. ## Where to next > **[Your first gift card purchase](/quickstart/first-gift-card)** > > Deposit funds, buy a gift card, and reveal its redemption details — the full happy path. > **[Create and spend with a virtual card](/quickstart/create-and-spend-with-a-virtual-card)** > > Pick a card program, issue a card with spend controls, and track its activity. > **[Idempotency](/concepts/idempotency)** > > Money-moving mutations take an `idempotencyKey`. Read this before your first write. > **[API reference](/api-reference/overview)** > > Every query, mutation, and type. # Introduction to the Fluz API Source: https://docs.fluz.app/get-started/introduction-to-the-fluz-api The Fluz API lets you embed money movement directly into your platform, program, or workflow — pay-ins, wallets, spend, rewards, and disbursements — through a single integration with one unified ledger. Fluz has processed over \$5 billion across cards, wallets, pay-ins, payouts, and bill payments. This overview covers what you can build, how the API works at a high level, and the fastest path to your first successful call. Skim it before diving into the guides. > [!TIP] > > 📘 **Using AI tools?** Point your assistant at our machine-readable docs index — [llms.txt](/llms.txt) — a complete, always-current map of every guide, recipe, and changelog entry. You can also connect via MCP at [/mcp](/mcp). ## Five ways money moves The API maps to Fluz's core money-movement solutions, which you can use together or on their own: * **Pay-Ins** — Bring funds into your platform through configurable deposit and funding flows, with support for multiple payment methods (bank, card, PayPal, wallet balance), automated settlement and reconciliation, and real-time balance updates. * **Wallets** — Open dedicated spend accounts that govern how funds are held and used, backed by user onboarding and identity verification, virtual account numbers, and a universal ledger. * **Spend** — Issue network-accepted, branded virtual cards embedded in your product, with user-controlled balances, merchant and category spend controls, and engagement and loyalty features. * **Rewards** — Distribute incentives through digital gift cards and prepaid cards with instant delivery, flexible funding sources, and real-time tracking. * **Disbursement** — Deliver fast, secure payouts across preferred payment methods, with open-loop payout options, embedded payout flows, and built-in compliance. ## What you can build A few flagship patterns the API is designed for: **Embedded wallets** — Move money in both directions between your platform and your customers. A one-time OAuth connection creates a persistent wallet-to-wallet link, so you can pay out with a single API call, let customers spend via virtual card, bank transfer, bill pay, or gift cards, track every movement in a real-time ledger with merchant-level data, and accept pay-ins back in — all through one integration. See the [embedded wallets solution](https://fluz.app/us/embedded-wallets/). **Open-loop prepaid cards** — Issue configurable Visa and Mastercard cards for incentives, recognition, and disbursements. Cards can be virtual or physical, single-load or reloadable, Apple/Google Wallet–ready, restricted by category or merchant, and ordered in bulk (up to 10,000 at a time) with white-label or co-branded card art. See [open loop cards](https://fluz.app/us/open-loop/). These power real-world programs like employee recognition, sales SPIFFs, customer loyalty, referral rewards, gig-worker and creator payouts, insurance claim payouts, market-research incentives, and user spend/allowance programs. Beyond these, the API also covers gift card purchasing (a catalog of thousands of brands with offer comparison, best-offer lookup, and stock checks), authorized users, transaction reporting and annotation, embedded JavaScript widgets with webhooks, and customer registration with KYC/KYB verification. ## How the API works A few things to know before your first request: * **It's a GraphQL API.** You send queries and mutations to a single endpoint rather than calling REST routes. The staging endpoint is `https://transactional-graph.staging.fluzapp.com/api/v1/graphql`. * **Authentication is two-step.** You authenticate with your API key (Basic auth) to generate a short-lived **User Access Token**, which carries the specific permission **scopes** an operation requires (for example, `CREATE_VIRTUALCARD`). You pass that token as a Bearer token on subsequent requests. See the [Authentication & Authorization Guide](/concepts/authentication). * **Test before you go live.** A full [staging environment](/concepts/environments) lets you build against sandbox keys, test bank accounts, and test card offers before switching to production. * **Requests are idempotent.** Use [idempotency keys](/concepts/idempotency) to safely retry requests without duplicating purchases or transfers. * **Multiple paths to launch.** Beyond the raw API, you can use low/no-code widgets and plug-and-play snippets, drop-in API adapters for existing integrations, and an OAuth-secured KYB/KYC "Comply" widget to automate onboarding. ## Built on regulated, secure infrastructure Fluz applies bank-grade controls across every money movement: **SOC 2 Type II** and **PCI DSS** compliant, continuous 24/7 fraud monitoring, and built-in KYC/KYB. Spend account funds are held by FDIC-insured banking partners, with pass-through deposit insurance coverage available up to \$10,000,000 under certain conditions.\* ## Get started 1. [**Prepare your accounts**](/get-started/prepare-accounts) — create your Fluz account and register your first application. 2. [**Obtain your API credentials**](/get-started/api-credentials) and [**generate a User Access Token**](/recipes/generate-user-access-token). 3. [**Perform your first API actions**](/get-started/first-api-call) — deposit funds, browse merchants, and purchase your first gift card. From there, explore the guides for [virtual cards](/features/virtual-cards), [wallets & transfers](/features/create-spend-accounts), and [embedded widgets](/developers/widgets), or jump to the [API Reference](/api-reference/overview). *** **Want to learn more?** Contact us at [humans@fluz.app](mailto:humans@fluz.app) to speak with our experts or request a demo. *** > \**FDIC disclosure: Fluz is a financial technology company, not an FDIC-insured bank. Banking services are provided by our FDIC-insured bank partners. FDIC insurance only covers the failure of an FDIC-insured bank. FDIC insurance up to \$10,000,000.00 is available on customer funds through pass-through insurance provided by bank partners where we have a direct relationship for the placement of deposits and into which customer funds are deposited, but only if certain conditions have been met. There may be a risk that FDIC insurance is not available because conditions have not been satisfied. In such cases, funds may not be fully insured in the event the insured depository institution where the funds have been deposited were to fail.* Get started [here](/get-started/prepare-accounts). # Prepare your accounts Source: https://docs.fluz.app/get-started/prepare-accounts Create your sandbox login, the right type of application, and get it activated — for yourself and for each developer on your team. Before writing code you need three things: a **staging login**, an **application of the right type**, and that application **activated by Fluz** if it will act for anyone other than you. Read [How accounts work](/get-started/accounts-and-applications) first if you haven't. It explains the terms used here. > [!NOTE] > > **Everything starts in staging.** Sign up and create applications at [uni.staging.fluzapp.com](https://uni.staging.fluzapp.com), not at fluz.app. Anything created on fluz.app is live. **1. Sign up in staging** Go to [uni.staging.fluzapp.com](https://uni.staging.fluzapp.com) and sign up. This creates a staging user with a personal account. Already have a live Fluz login? Staging is a separate system: sign up there too. You can also open it from the live dashboard under **Resources → Open Staging**. **2. Pick the application type** Open [uni.staging.fluzapp.com/for-developers](https://uni.staging.fluzapp.com/for-developers). * **Acting only on your own account?** Click **Create new app**. * **Acting for other users** (registering customers, KYC/KYB, issuing cards for them)? Go to [Templates](https://uni.staging.fluzapp.com/for-developers/templates) and choose **OAuth Integration**. > [!WARNING] > > Platforms need **OAuth Integration**. An app made with **Create new app** can only call the API as its developer, and can't be converted later. **3. Ask Fluz to activate the application** New applications are not active. They work only on the developer's own account until Fluz moves them to `ACTIVE`. Send the application name or ID to your Fluz contact, together with any restricted capabilities you need: `registerUser`, `VERIFY_KYC`, or restricting the app to business accounts. Until the app is activated, platform calls fail with `AUTH-0030 … current status: PERSONAL`. **4. Copy your credentials** Open the application to find its **API Key**, **User ID** and **Account ID**. The Account ID is the account this application's own tokens act as. Continue to [API credentials](/get-started/api-credentials). ## Is my company's staging account a business? If Fluz set up a staging business account for your company during onboarding, that account already exists alongside your personal one. Use [Get Accounts](/recipes/get-accounts) to find its `accountId`, and mint tokens against it when you need a `BUSINESS` token. ## Setting up a team of developers To give each developer an isolated sandbox, so tests don't share data: 1. **Each developer signs up in staging** with their own email and phone. Each gets their own personal account. 2. **Each developer creates their own OAuth Integration application** and sends it to Fluz to activate. 3. **Each developer creates their own test business** if they need a `BUSINESS` account. Use `registerBusiness` with the approved test tax ID from [KYB Testing](/KYB-testing). Prefer one shared login? A single account can hold several applications, each with its own key. You can also add teammates as [authorized users](/features/authorized-user-overview). Both share one ledger, so tests will see each other's data. ## Next steps > **[API credentials](/get-started/api-credentials)** > > Mint a token for the right account. > **[Fund your staging wallet](/concepts/environments#funding-a-staging-wallet)** > > Put test money in before you issue cards. # Refresh an expired access token Source: https://docs.fluz.app/get-started/refresh-expired-access-token Exchange a refresh token for a fresh user access token without re-minting from scratch. User access tokens are short-lived JWTs. When one expires, exchange the refresh token you received alongside it for a new access token. > [!NOTE] > > **Two different flows.** This page covers **API-key applications** operating on your own account. If you are an **OAuth platform application** acting on a customer's account, refresh customer tokens through the OAuth token refresh endpoint instead — see [Refresh an OAuth access token](/refresh-o-auth-access-token). ## How the two tokens relate ```mermaid theme={null} flowchart TD KEY[API key
Basic auth] KEY -->|generateUserAccessToken| PAIR subgraph PAIR[One mint returns both] AT[access token
short-lived] RT[refresh token
long-lived] end AT -->|Bearer, every request| API[Fluz API] AT -.->|expires| EXP[401] RT -->|refreshUserAccessToken
Basic auth| NEW[new access token only] NEW -->|Bearer| API RT -.->|reused, not replaced| RT style KEY fill:#e8e8e8,stroke:#888 style AT fill:#d4edda,stroke:#5a9 style NEW fill:#d4edda,stroke:#5a9 style RT fill:#fff3cd,stroke:#c93 ``` Two things catch people out. The refresh call authorizes with your **API key**, not with the expired access token. And it returns **only** a new access token — the refresh token you already hold stays valid and is not replaced. ## Where the refresh token comes from `generateUserAccessToken` returns a refresh token alongside the access token. Request it explicitly — if you only select `token` and `scopes`, you never receive one: ```graphql theme={null} mutation ($userId: UUID, $accountId: UUID, $scopes: [ScopeType!]!) { generateUserAccessToken(userId: $userId, accountId: $accountId, scopes: $scopes) { token refreshToken scopes } } ``` Store the `refreshToken` server-side with the access token. See [API credentials](/get-started/api-credentials) for the full minting call. ## Refresh the token `refreshUserAccessToken` authorizes with your **API key**, not with the expired access token. **1. Call refreshUserAccessToken** Replace `` and `` with your values: ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "query": "mutation ($refreshToken: String!) { refreshUserAccessToken(refreshToken: $refreshToken) { token scopes } }", "variables": { "refreshToken": "" } }' ``` **2. Store the new access token** The response carries a fresh access token and the scopes it grants: ```json theme={null} { "data": { "refreshUserAccessToken": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "scopes": ["LIST_OFFERS", "PURCHASE_GIFTCARD"] } } } ``` Use the new `token` in the `Authorization: Bearer ` header, exactly as before. > [!NOTE] > > **The refresh response does not include a new refresh token.** `refreshUserAccessToken` returns only `token` and `scopes`. Keep the refresh token you received from `generateUserAccessToken`. ## When to refresh Refresh proactively — at the start of a job or session — rather than waiting for a request to fail with a `401` and refreshing reactively. That keeps requests from failing under load. If you no longer hold a valid refresh token, mint a new access token from scratch with [`generateUserAccessToken`](/get-started/api-credentials). ## Next steps > **[API credentials](/get-started/api-credentials)** > > Mint your first access token and see the full set of arguments. > **[Authentication](/concepts/authentication)** > > The complete authentication model, including OAuth grants for customer-scoped tokens. # Gift card error codes Source: https://docs.fluz.app/gift-card-error-codes | Error | Code | Message | | :- | :- | :- | | INVALID\_STATUS\_OR\_OWNER | GC-0001 | Sorry, we are unable to update restricted purchases. | | INVALID\_PURCHASE\_AMOUNT | GC-0002 | Value or Fluzpay Amount should be a positive number! | | UNABLE\_TO\_RETRIEVE | GC-0003 | Unable to retrieve gift card record, please try again or call customer support! | | UNABLE\_TO\_PURCHASE | GC-0004 | Please try another payment method. If you continue experiencing issues, please contact our support team. | | UNABLE\_TO\_UPDATE\_STATUS | GC-0005 | Sorry, we are unable to update status, please try again or contact customer support. | | UNABLE\_TO\_REVEAL | GC-0006 | Unable to reveal gift card, please try again or contact customer support. | | UNABLE\_TO\_GENERATE\_EXPORT | GC-0007 | Unable to generate gift card export, please try again or contact customer support. | # Gift cards overview Source: https://docs.fluz.app/gift-card-overview Read the merchant catalog, understand fixed versus variable offers, buy a card, and reveal it — the whole gift card arc and which page to read for each step. Buying a gift card through Fluz is four steps, and each one has a page of its own. This is the map. **1. Find the offer** Pull the catalog, or ask for the best rate on one merchant. → [Read the catalog](#reading-the-catalog) **2. Confirm the amount is purchasable** Fixed or variable, stocked or generated on demand. → [Fixed vs. variable](#fixed-vs-variable-offers) **3. Buy** One mutation, one card, one idempotency key. → [Buying](#buying-a-card) **4. Reveal** Get the code, PIN, or URL, and render it correctly. → [Revealing](#revealing-the-card) > [!WARNING] > > **Rates change constantly.** Fluz continuously re-prices to give customers the best available offer, and your rates are customized to your account. Never cache a rate and purchase against it later — re-confirm immediately before you buy. *** ## Vocabulary Five terms do most of the work in this section, and three of them sound alike. | Term | What it is | | :- | :- | | **Merchant** | The brand — Starbucks, Best Buy. Has a `merchantId` and a human-readable `slug`. | | **Offer** | A specific purchasable deal from that merchant, with its own `offerId`. One merchant can have several. | | **Offer rate** | The reward attached to an offer — cashback, boosts, eligible denominations, permitted payment methods. | | **Denomination** | The face value of the card. Either chosen from a preset list or any amount in a range. | | **Delivery format** | How the card arrives: `URL`, `CODES`, `PIN_AS_CODE`, `PIN_WITH_URL`, or `CODE_WITH_PREFIX`. Determines how you render it. | Two identifiers are easy to confuse: `merchantId` identifies the brand, `offeringMerchantId` identifies the party offering that particular deal. You buy against an `offerId` or a `slug`, never against a `merchantId`. *** ## Reading the catalog Three ways in, for three different jobs. | Approach | Query | Use when | | :- | :- | :- | | **Full catalog** | `getMerchants` | You're building a browsable storefront, syncing a local catalog, or comparing across brands | | **Best rate for one merchant** | `getOfferQuote` | You already know the brand and amount and just want today's best rate | | **CSV export** | Dashboard → **Stores** → generate export | Analysis, finance, or a human wants a spreadsheet | Filter `getMerchants` to gift cards with `offerTypes: { giftCardOffer: true, cardLinkedOffer: false }`. Card-linked offers exist in the catalog but only gift card and exclusive offers are purchasable through the API today. > [!NOTE] > > **The full catalog is a cached file, refreshed twice daily.** `getOfferQuote` is live. If the exact rate matters at purchase time — and it usually does — quote before you buy rather than trusting a catalog pull from this morning. Promotional rates are reflected in both, including in CSV exports generated during a promotion. Both queries are rate-limited, so paginate the full catalog rather than requesting it in one call. → [Get Catalog](/get-catalog) · [Get Gift Card Offers](/get-gift-card-offers) · [Get the Best Offer](/get-best-offer) ### Exclusive offers Rates are customized per account. If yours has negotiated rates, they appear as offers with `type: "EXCLUSIVE_RATE_OFFER"` carrying an `exclusiveRateId`. Pass that ID to `purchaseGiftCard` to force the purchase onto that rate; omit it and Fluz picks the best available. *** ## Fixed vs. variable offers This is the distinction that most shapes how you build, and a merchant can have both. > **FIXED** > > The card comes in **preset denominations** — $25, $50, \$100 — and you buy one of them exactly. > > Frequently backed by **real inventory** Fluz holds, which is why fixed offers usually carry **better rates and higher purchasing limits**. > > Inventory is finite. It runs out. > **VARIABLE** > > You choose **any amount within a min/max range**, and the card is generated in real time. > > No inventory to deplete — effectively unlimited supply. > > Usually a **lower reward rate** than the same brand's fixed offer. The practical trade-off: fixed offers pay better but can be exhausted mid-run; variable offers always work but pay less. High-volume ordering usually means taking fixed inventory first and falling back to variable. ### Where to read the purchasable amounts Two fields decide this, and the combination determines which field holds the answer. Get this wrong and you'll submit amounts the offer can't fulfill. | `denominationsType` | `hasStockInfo` | Purchasable amounts live in | Meaning | | :- | :- | :- | :- | | `FIXED` | `true` | `stockInfo` → `StockInfoFixedType` | Specific denominations with a countable `availableStock` | | `FIXED` | `false` | `offerRates.denominations` | Preset denominations, no inventory constraint published | | `VARIABLE` | `true` | `stockInfo` → `StockInfoVariableType` | A `minDenomination`–`maxDenomination` range | | `VARIABLE` | `false` | `offerRates.denominations` | Any amount in the published range | > [!WARNING] > > `stockInfo` is a **union type**. You must query it with inline fragments for *both* shapes, or you'll get nothing back for one of them: > > ```graphql theme={null} > stockInfo { > ... on StockInfoFixedType { __typename denomination availableStock } > ... on StockInfoVariableType { __typename description minDenomination maxDenomination } > } > ``` > > Always include both fragments, even when you think you know which one you'll get. See [How the GraphQL API works](/concepts/graphql). Note that "has stock info" doesn't mean "has countable stock." On a variable offer, `stockInfo` returns a *range*, not a quantity. Only `StockInfoFixedType` carries an `availableStock` number you can decrement against. Populating `stockInfo` requires Fluz to confirm inventory with the vendor, and vendor response times vary — so requesting it makes the query slower. Only ask for it when you're about to act on it. → [Get Inventory on Stocked Offers](/get-inventory) *** ## Buying a card One mutation: `purchaseGiftCard`. Three decisions. ### 1. How to pick the offer | You pass | Behavior | | :- | :- | | `offerId` | **Pinned.** Buys that exact offer. If it's depleted, the call fails — no fallback. | | `merchantSlug` | **Auto-select.** Buys the best available rate for that brand, falling back as inventory shifts. | | `merchantSlug` + `minRewardRate` | Auto-select **with a floor.** Fails rather than buying below your minimum rate. | | `exclusiveRateId` | Forces a specific negotiated rate. | Pinning gives you certainty about the rate; auto-select gives you certainty about fulfillment. `merchantSlug` + `minRewardRate` is the middle ground and usually the right default for automated ordering. ### 2. How to pay At least one funding source is required, and you can combine your Fluz balance with another. | Field | What it does | | :- | :- | | `balanceAmount` | **How much** to pay from your Fluz balance | | `userCashBalanceId` | **Which spend account** that balance draws from | | `bankCardId` / `bankAccountId` / `paypalVaultId` | External funding sources | | `defaultToBalance` | Fall back to balance if another method fails. Defaults to `true` | > [!WARNING] > > **If your account holds more than one spend account, always pass `userCashBalanceId` explicitly.** Omit it and Fluz draws from whichever account is flagged `isDefault` — which can change without your code changing, silently redirecting where your money comes from. This is the most common cause of surprise insufficient-funds failures. > > In automated pipelines, also set `defaultToBalance: false` so a purchase either draws from the account you named or fails cleanly. ### 3. Idempotency `idempotencyKey` is required, and it's the difference between a retry and a double purchase. One key per intended card, reused on every retry of that same card. → [Idempotency](/docs/idempotency-requests) → [Purchase Gift Card](/purchase-gift-card) ### Buying more than one **One call buys exactly one card**, on one offer, at one rate. There's no quantity field and no blending across offers. For ten cards, send ten calls with ten distinct idempotency keys. What happens when you outrun inventory depends on how you picked the offer: * **Pinned (`offerId`)** — once the stocked offer is depleted, remaining calls fail. No automatic fallback. * **Auto-select (`merchantSlug`)** — remaining calls move to the next-best offer, often a variable one at a lower rate, unless `minRewardRate` blocks it. → [Purchase in Bulk](/bulk-gift-card-purchasing) ### Ordering at volume Purchases against the same Fluz account process **sequentially**. Fire a large batch at once and calls queue behind each other, occasionally taking minutes to return. > [!NOTE] > > **A client timeout is not a cancellation.** Fluz keeps processing a request you've stopped waiting for. Treat a timeout as an *unknown* outcome, never a failure. > > Resolve it by retrying with the **same** `idempotencyKey` — the retry returns the original purchase if it already succeeded, and won't double-charge. Issuing a fresh key for a purchase you already attempted is exactly how duplicate orders happen. > > Set client timeouts to about a minute, pace requests in waves rather than all at once, and spread heavy volume across multiple accounts. *** ## Revealing the card A purchase gives you a `giftCardId`. Redemption details come from a second call. **1. Get the gift card** Skip this if you just purchased and already hold the `giftCardId`. Otherwise `getGiftCards` lists them with `purchaseId`, `purchaseDisplayId`, `purchaseValue`, `currentValue`, and `status` — enough to reconcile orders without revealing every card. **2. Reveal it** `revealGiftCardByGiftCardId` returns `code`, `pin`, `url`, and `termsAndConditions`. Three things that catch people out: * **Not every card has all three fields.** Some merchants issue a code with no PIN; some issue only a URL. Fluz passes through whatever the merchant provides — handle nulls. * **Render according to `deliveryFormat` and `barcodeType`,** and take `deliveryFormat` from `getGiftCards`, not from the merchant's current offer. Offers change; the card was issued under the format in force at purchase time. `barcodeType` is `NONE`, `C128`, `PDF417`, or `QRCODE`; when it's `NONE`, consider displaying the `faceplateUrl` instead. * **Details may not be ready instantly.** Poll with exponential backoff — 300ms, doubling, capped at three minutes — and stop as soon as details return. → [View Gift Cards](/view-gift-card) *** ## Scopes | Scope | Needed for | | :- | :- | | `LIST_OFFERS` | `getMerchants`, `getOfferQuote` | | `PURCHASE_GIFTCARD` | `purchaseGiftCard` | | `REVEAL_GIFTCARD` | `revealGiftCardByGiftCardId` | | `LIST_PURCHASES` | `getUserPurchases`, purchase history | | `LIST_PAYMENT` | `getUserCashBalances`, `getWallet` | Enable these on your app's **Permissions** tab before you build. A scope you request but haven't enabled is silently dropped. → [Configure OAuth App](/configure-o-auth-app) *** ## When things fail | Code | Meaning | | :- | :- | | `GC-0002` | Purchase amount or Fluz Pay amount isn't positive | | `GC-0003` | Couldn't retrieve the gift card record | | `GC-0004` | Purchase failed — try another payment method | | `GC-0006` | Couldn't reveal the card | Full list: [Gift Card Error Codes](/gift-card-error-codes). Before refunding an end user on a failed or timed-out purchase, **retry with the same `idempotencyKey` or look up the purchase by ID.** Timed-out requests frequently succeeded, and the code stays revealable until the purchase is refunded. *** ## Next steps > **[Get catalog](/get-catalog)** > > Pull merchants and their offers. > **[Get the best offer](/get-best-offer)** > > Live quote for one merchant and amount. > **[Get inventory](/get-inventory)** > > Stock on fixed, stocked offers. > **[Purchase a gift card](/purchase-gift-card)** > > The mutation, in full. > **[Purchase in bulk](/bulk-gift-card-purchasing)** > > Ordering many cards, and depletion behavior. > **[View gift cards](/view-gift-card)** > > Reveal codes, PINs, and URLs. # Merchant catalog overview Source: https://docs.fluz.app/merchant-catalog In order to make gift card purchases with the Fluz API, you'll need to determine what merchant and offer you would like to use. Fluz offers two ways to access our merchant catalog, tailored to different use cases: 1. **Full Merchant List with Offers:** Retrieve a comprehensive list of all available merchants and their offers. 2. **Best Rate Query:** Easily determine the best available rate for a specific merchant. Before you begin using the catalog, there are a few important details to understand. > 🚧 Cashback rates are subject to change. > > We do our best to always give our customers the best offers available. This means that our rates change regularly. Always confirm the rate before making a purchase. ## Rate Settings on Catalog Requests Rates in the catalog are customized to your account. If your account has any exclusive offers, these will be reflected in the catalog within the `type` field of the `offer` object. ## Catalog Update Frequency The full catalog file is updated twice daily. When you request the full catalog via the Fluz API, you'll receive a cached version of the most recent update. ## Offer Types Merchants can feature, gift card offers, card-linked offers and exclusive offers. However, currently, only gift card and exclusive offers are currently accessible via the API. ## Pulling Catalog From Your Dashboard Besides accessing the catalog via API, you can also download it directly from your Fluz dashboard: 1. Log in to your Fluz dashboard and navigate to the "Stores" view. 2. There, you’ll find an option to generate a catalog export. 3. The export will be downloaded as a CSV file. Keep in mind that the same update frequency mentioned earlier applies to these CSV files. > 📘 Promotional rates are reflected in the catalog export as well. If you run any of these exports while the Fluz team is running a promotion, those updated rates will be reflected on your export. # Merchant-locked virtual cards Source: https://docs.fluz.app/merchant-locked-virtual-cards Issue a virtual card that binds to a single merchant on first use, by selecting a brand-locked card program. A **merchant-locked card** — called a *brand-locked* card in Fluz product surfaces — is a virtual card that ties itself to the first merchant it transacts with. Every later authorization at any other merchant is declined. There is no `merchantId` parameter on `createVirtualCard`. Merchant locking is a property of the **card program**, not of the individual card. You get a merchant-locked card by issuing against a brand-locked `offerId`; everything else about the request is identical to a standard card. > [!WARNING] > > **The lock binds on first use, not at issuance.** > > You cannot choose *which* merchant the card locks to. You issue an unbound card from a brand-locked program, and the first successful authorization determines the merchant permanently. There is no way to pre-assign, change, or clear the binding after it is set. > > If your product needs a card that only works at a merchant *you* nominate up front, this is not that feature — see [Choosing the right control](#choosing-the-right-control). *** ## Step 1 — Find a brand-locked offer Filter [`getVirtualCardOffers`](/features/get-card-offers) with `cardBrandLocked: true`. Only programs enabled for your account are returned, so always discover offers at runtime rather than hardcoding an ID. ```graphql theme={null} query GetVirtualCardOffers($input: GetVirtualCardOffersInput!) { getVirtualCardOffers(input: $input) { offerId bin bankName programName rewardValue programLimits { dailyLimit weeklyLimit monthlyLimit } } } ``` ```json theme={null} { "input": { "cardBrandLocked": true, "cardType": "PREPAID", "cardNetwork": "MASTERCARD" } } ``` ### Filter fields | Field | Type | Description | | :- | :- | :- | | `cardBrandLocked` | `Boolean` | `true` returns only merchant-locking programs. Omit to return all programs. | | `cardType` | `VirtualCardOfferType` | `DEBIT` or `PREPAID`. | | `cardNetwork` | `VirtualCardNetwork` | `MASTERCARD` or `VISA`. | ### Sample response ```json theme={null} { "data": { "getVirtualCardOffers": [ { "offerId": "e0fb2c5f-6a75-498f-95f6-37359a64cb0f", "bin": "123456", "bankName": "Example Bank - Mastercard", "programName": "Brand Locked Virtual Card - Mastercard Debit", "rewardValue": "1.5%", "programLimits": { "dailyLimit": "100000", "weeklyLimit": "700000", "monthlyLimit": "3100000" } } ] } } ``` > [!NOTE] > > **Read `rewardValue` per offer — do not assume the standard rate.** Cashback on brand-locked programs is set independently of the standard virtual card programs and is frequently different, in both directions: some brand-locked BINs earn a materially lower flat rate, while specific merchants carry boosted rates that only apply on brand-locked cards. `programLimits` also varies by program and caps what `spendLimit` you can request. ### Sandbox Staging exposes one brand-locked program for testing: | Offer ID | Program Name | Reward Value | | :- | :- | :- | | `592c394e-26cc-44ac-a145-a5f81301fe77` | Brand Locked Virtual Card - Mastercard Prepaid | 1.5% | See [Test Virtual Card Offers](/test-virtual-card-offers) for the full sandbox offer list. *** ## Step 2 — Issue the card > **Restricted Access** > > This mutation requires a Bearer token with the `CREATE_VIRTUALCARD` scope. Pass the brand-locked `offerId` to [`createVirtualCard`](/features/create-card). No other field changes behavior with respect to the merchant lock. ```graphql theme={null} mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId cardholderName virtualCardLast4 expiryMonth expiryYear status cardType initialAmount usedAmount createdAt } } ``` ```json theme={null} { "input": { "idempotencyKey": "63b2c9e0-62d1-42ab-b1c2-1a7ee2f8c0a9", "offerId": "592c394e-26cc-44ac-a145-a5f81301fe77", "spendLimit": 250.00, "spendLimitDuration": "MONTHLY", "cardNickname": "Ad spend — locks on first use", "primaryFundingSource": "FLUZ_BALANCE", "userCashBalanceId": "b1155504-ad30-4b2f-873d-b8795277b128" } } ``` ### cURL ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId cardholderName virtualCardLast4 expiryMonth expiryYear status cardType initialAmount createdAt } }", "variables": { "input": { "idempotencyKey": "63b2c9e0-62d1-42ab-b1c2-1a7ee2f8c0a9", "offerId": "592c394e-26cc-44ac-a145-a5f81301fe77", "spendLimit": 250.00, "spendLimitDuration": "MONTHLY", "cardNickname": "Ad spend — locks on first use", "primaryFundingSource": "FLUZ_BALANCE" } } }' ``` > [!NOTE] > > **The billing address still has to be verifiable.** Whether passed inline as `billingAddress` or referenced by `userAddressId`, it must be a real, deliverable US address — no PO boxes. It is validated against USPS data via Smarty during cardholder setup, and an unverifiable address fails the whole request with `VC-0025`. See [Address Formatting Requirements](/concepts/address-formatting-requirements). ### TypeScript ```typescript theme={null} const graphql = async (query: string, variables: Record) => { const response = await fetch( 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query, variables }), }, ); return response.json(); }; // 1. Discover a brand-locked program available to this account. const offers = await graphql( `query GetVirtualCardOffers($input: GetVirtualCardOffersInput!) { getVirtualCardOffers(input: $input) { offerId programName rewardValue programLimits { dailyLimit weeklyLimit monthlyLimit } } }`, { input: { cardBrandLocked: true, cardNetwork: 'MASTERCARD' } }, ); const brandLocked = offers.data.getVirtualCardOffers[0]; if (!brandLocked) { throw new Error('No brand-locked program enabled for this account.'); } // 2. Issue against it. The card is unbound until its first authorization. const card = await graphql( `mutation CreateVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId virtualCardLast4 status initialAmount } }`, { input: { idempotencyKey: crypto.randomUUID(), offerId: brandLocked.offerId, spendLimit: 250.0, spendLimitDuration: 'MONTHLY', cardNickname: 'Ad spend — locks on first use', primaryFundingSource: 'FLUZ_BALANCE', }, }, ); ``` *** ## Choosing the right control "Lock this card to a merchant" means three different things in practice. Only one of them is the brand lock described on this page. | You want | Use | Where | | :- | :- | :- | | A card that self-binds to whichever merchant it first hits | Brand-locked `offerId` — this page | API | | A card restricted to a whole merchant *category* (e.g. fuel) | MCC category lock | Web portal only | | A card that can only ever be used once | `lockCardNextUse: true` on [`createVirtualCard`](/features/create-card) | API | > [!NOTE] > > **Brand lock and category lock are not the same control.** A category lock restricts the card to every merchant matching a chosen MCC — a fuel-locked card works at any qualifying gas station. A brand lock restricts the card to exactly one merchant, but you do not get to choose which one in advance. They solve different problems and are configured in different places. *** ## What cardholders experience * The card behaves like any other virtual card until its first successful authorization. * After that authorization, the merchant is fixed. Attempts elsewhere are declined at authorization — there is no partial approval and no prompt. * The binding survives locking and unlocking the card, and is not affected by editing the nickname, spend limit, or lock date. * Wallet provisioning is unaffected, but note that Apple Pay and Google Pay transactions typically earn no cashback regardless of program. Declines from a merchant mismatch appear in [`getDeclinedTransactions`](/features/get-decline-transactions) alongside all other declines. See [Decline Codes](/features/decline-codes) for interpreting the reason returned. *** ## Common mistakes > **Passing a merchant identifier to createVirtualCard** > > There is no such field. `CreateVirtualCardInput` accepts no merchant, brand, or MCC parameter — extra keys are rejected as invalid input with `ARG-0001`. Merchant locking comes entirely from the program you select with `offerId`. > **Hardcoding a brand-locked offerId** > > Offer availability is per-account and programs are added and retired over time. Always call `getVirtualCardOffers` with `cardBrandLocked: true` and handle the empty-result case, rather than persisting an ID from a previous integration or from the sandbox table above. > **Assuming the standard cashback rate applies** > > Brand-locked programs carry their own reward rates. Read `rewardValue` off the offer you actually issue against, and do not display a rate to your users that you inherited from a standard program. > **Testing the lock with a single transaction** > > One successful authorization only proves the card works — it does not prove the lock engaged. To verify end to end, transact once at merchant A, then attempt merchant B and confirm the decline. *** > **[Issue Cards](/features/create-card)** > > The full `createVirtualCard` reference — funding, limits, and lifecycle controls. > **[Get Virtual Card Offers](/features/get-card-offers)** > > Enumerate every program available to your account. > **[Decline Codes](/features/decline-codes)** > > Interpreting declines, including merchant mismatches. > **[Test Virtual Card Offers](/test-virtual-card-offers)** > > Sandbox offer IDs for every program, including brand-locked. # Fluz API documentation Source: https://docs.fluz.app/overview Embed money movement into your product — pay-ins, wallets, virtual cards, gift cards, and payouts — through one GraphQL API and a single unified ledger. ## Where do you want to start? > **[Spend on your account](/get-started)** > > Use the API to move money in and out of your own Fluz account. Credentials, auth, and your first call in staging — about five minutes. > **[Build for your customers](/build-a-platform)** > > Register or connect customer accounts with OAuth, then run the exact same APIs on their behalf. ## Explore the docs > **[Get started](/get-started)** > > Credentials, auth, and your first call in staging. > **[Features](/features)** > > Cards, gift cards, wallets, and transactions — on your account or any connected account. > **[Build a platform](/build-a-platform)** > > Connect customer accounts and run the same capabilities on their behalf. > **[Developers](/developers)** > > Widgets, connectors, webhooks, and reference material. ## Popular quickstarts **Your own spend** > **[Your first gift card purchase](/quickstart/first-gift-card)** > > Deposit funds, browse the catalog, buy a gift card, and reveal its redemption details. > **[Your first virtual card purchase](/quickstart/create-and-spend-with-a-virtual-card)** > > Pick a card program, issue a card with spend controls, reveal it, and track activity. **Your customers** > **[Onboard customers](/quickstart/onboard-customers)** > > Register a user, run KYC, and connect their account so you can transact on their behalf. > **[Send a card to someone](/quickstart/send-a-card)** > > Create an app, select your funding, and send a virtual card to one or many recipients. > **[See all quickstarts](/quickstart/first-gift-card)** > > Every end-to-end flow, staged so each step verifies the last. ## Browse by capability > **Virtual cards** > > * [Create a card](/features/create-card) > * [Edit a card](/features/edit-virtual-card) > * [Reveal card details](/recipes/reveal-virtual-card) > * [Track transactions](/features/get-virtual-card-transactions) > * [Issue in bulk](/features/create-bulk-order) > **Gift cards** > > * [Browse the catalog](/get-catalog) > * [Get the best offer](/get-best-offer) > * [Purchase a gift card](/purchase-gift-card) > * [Purchase in bulk](/bulk-gift-card-purchasing) > * [View gift cards](/view-gift-card) > **Wallets & money movement** > > * [Link a bank account](/features/link-via-plaid) > * [Deposit funds](/features/deposit-from-external-accounts) > * [Transfer between accounts](/features/transfer-between-spend-accounts) > * [Withdraw](/features/withdraw-to-external-account) > **Customers & platforms** > > * [Register users](/user-registration) > * [Run KYC verification](/user-kyc-verification) > * [Connect accounts with OAuth](/create-an-o-auth-app) > * [Manage authorized users](/features/create-authorized-users) > **Integration tools** > > * [Embedded widgets](/developers/widgets) > * [API connectors](/API-connectors-overview) > * [Webhooks](/fluz-dashboard/webhooks) > **API foundations** > > * [How GraphQL works](/concepts/graphql) > * [Authentication](/concepts/authentication) > * [Idempotency](/concepts/idempotency) > * [Rate limits](/concepts/rate-limits) ## Stay in sync > **[Changelog](/changelog)** > > Recent updates, additions, and breaking changes to the Fluz API. > **[Security & compliance](/developers/security)** > > SOC 2 Type II, PCI DSS, 24/7 fraud monitoring, and FDIC-insured banking partners. > [!NOTE] > > **Using AI tools?** Point your assistant at `llms.txt` for a machine-readable index, or connect via MCP at `docs.fluz.app/mcp`. # Purchase gift card Source: https://docs.fluz.app/purchase-gift-card Purchase a gift card with the purchaseGiftCard mutation, including how to select the spend account funds are drawn from. Once you've determined your preferred offer, use the `purchaseGiftCard` mutation to purchase your gift card. This mutation requires a `PurchaseGiftCardInput` input object. ## Sample Mutation Here's the quickest way to start purchasing a gift card. You are able to customize your query from the `UserPurchase` object. See the [API reference](/api-reference/overview). ```graphql theme={null} mutation purchaseGiftCard($input: PurchaseGiftCardInput!) { purchaseGiftCard(input: $input) { purchaseDisplayId purchaseAmount giftCard { giftCardId status termsAndConditions } } } ``` ## Fields ### Required `idempotencyKey` — A unique client-generated UUID to ensure a request is processed only once. `offerId `**or** `merchantSlug` — Use the `offerId` or `merchantSlug` from the [`getOfferQuote`](/get-best-offer) query to get the best offer. Using the `merchantSlug` will automatically purchase the best offer rate for that merchant. `amount` — The gift card amount you'd like to purchase. *** ### Rate selection `exclusiveRateId` — The unique identifier for a specific exclusive rate offer. When provided, this forces the purchase to use the specified exclusive rate. If not provided, the system will automatically select the best available rate. The `exclusiveRateId` can be found in the [`getMerchants`](/get-catalog) query response for offers with type `EXCLUSIVE_RATE_OFFER` when you provide `exclusiveRateId` in your query request under `offers`. `minRewardRate` — If you want to specify the minimum reward rate to purchase if the `merchantSlug` option is chosen. *** ### Payment At least one funding source is required. You may also choose to combine your Fluz balance with another funding source. `balanceAmount` — If you want to pay for your gift card with your Fluz balance, define the amount of balance here. You can use the [`getWallet`](/check-account-balance) query to check your balances. `userCashBalanceId` — The **spend account** that `balanceAmount` is drawn from. Pass this explicitly whenever your account holds more than one spend account. If omitted, Fluz draws from the spend account flagged `isDefault: true`. This is a modifier on `balanceAmount`, not an alternative funding source — see [Choosing a spend account](#choosing-a-spend-account). `bankAccountId` — If you want to pay with an external linked bank account, define the bank account ID. This is not a spend account. `bankCardId` — If you want to pay with a bank card, define the bank card ID. `paypalVaultId` — If you want to pay with a PayPal account, define the PayPal account ID. `defaultToBalance` — If you want to use your Fluz balance as the fallback payment method in case your other payment methods fail, set `defaultToBalance` to `true`. By default, this is set to `true`. If you change this setting to `false`, the system will not attempt to use your Fluz balance as a backup payment method. *** ### Expense details `memo` — If you want to attach a note to this transaction, provide a free-text memo here. Max 255 characters. `transactionCategory` — If you want to categorize this transaction, provide a category name. Categories are created automatically on first use and reused if the same name is passed again. `attachmentId` — If you want to attach a file to this transaction, provide the ID returned by the upload endpoint. See [Add Expense Details](/features/add-expense-details). > #### See [Add Expense Details](/features/add-expense-details) for full details on uploading attachments and working with memos and categories. #### PurchaseGiftCardInput ```json theme={null} { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "offerId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "amount": 987.65, "balanceAmount": 123.45, "userCashBalanceId": "85de1b3e-4e72-462c-8ed1-a6f4982e22f7", "bankAccountId": "0285c162-fb2f-4c32-b076-29166471f570", "bankCardId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "paypalVaultId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "exclusiveRateId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "merchantSlug": "xyz789", "minRewardRate": 5.6 } ``` ## Choosing a spend account A **spend account** is a cash balance account inside Fluz that holds the funds a purchase draws from. Your account can hold several of them — for example "Main account," "Operations," or "Client A" — each with its own nickname and its own balance. This section covers only what you need in order to fund a gift card purchase. For the full picture — how spend accounts work, the three balances each one tracks, and how to create, rename, and close them — see [Spend Accounts](/features/spend-accounts) in the Wallet documentation. > [!NOTE] > > **"Spend account," "cash balance," and `UserCashBalance` all refer to the same object.** > > The product surfaces it as a spend account. The API names the type `UserCashBalance`, so the field on this mutation is `userCashBalanceId` — not `accountId`. Note that `bankAccountId` is unrelated: it refers to an *external* linked bank account, not a spend account. ### Which field does what When you fund a purchase from your Fluz balance, two fields work together: | Field | Purpose | | :- | :- | | `balanceAmount` | **How much** of the purchase to pay from your Fluz balance. | | `userCashBalanceId` | **Which spend account** that balance is drawn from. | These are not mutually exclusive. `userCashBalanceId` has no effect unless the purchase draws on balance — either through `balanceAmount` or through a `defaultToBalance` fallback. ### Default behavior when `userCashBalanceId` is omitted If you omit `userCashBalanceId`, Fluz draws from the spend account flagged `isDefault: true`. > [!WARNING] > > **If your account holds more than one spend account, always pass `userCashBalanceId` explicitly.** > > Relying on the default is the most common cause of unexpected insufficient-funds failures. A deposit routed to a newly created spend account, or a change to which account is flagged as default, will silently redirect where your purchases draw from — your requests are unchanged, but they now resolve to an account with a different balance. Passing the ID explicitly makes the funding source deterministic. To move funds between spend accounts — for example, to unblock an order drawing from the wrong account — see [Transfer Funds Between Spend Accounts](/features/transfer-between-spend-accounts). Internal transfers settle immediately. ### Step 1 — Retrieve your spend account IDs Use the `getUserCashBalances` query to list your spend accounts. This requires the `LIST_PAYMENT` scope. ```graphql theme={null} query GetUserCashBalances($filter: UserCashBalanceFilterInput) { getUserCashBalances(filter: $filter) { userCashBalances { userCashBalanceId nickname availableCashBalance isDefault status } totalCount } } ``` Variables: ```json theme={null} { "filter": { "status": ["ACTIVE"] } } ``` Sample response: ```json theme={null} { "data": { "getUserCashBalances": { "userCashBalances": [ { "userCashBalanceId": "6d1b4b19-deef-42f5-80d7-ec34804ce090", "nickname": "Main account", "availableCashBalance": "425108.80", "isDefault": true, "status": "ACTIVE" }, { "userCashBalanceId": "1c1b5fcc-eb21-44c5-b678-9c41f3fa21b4", "nickname": "Gift card orders", "availableCashBalance": "12500.00", "isDefault": false, "status": "ACTIVE" } ], "totalCount": 2 } } } ``` Store the `userCashBalanceId` of the account you intend to spend from. The ID is stable, so you can hold it in configuration rather than looking it up on every purchase — though you should check `availableCashBalance` before high-volume runs. See [Get Spend Accounts](/features/get-spend-accounts) for the full field reference, filter options, and pagination. ### Step 2 — Pass the spend account on the purchase ```graphql theme={null} mutation purchaseGiftCard($input: PurchaseGiftCardInput!) { purchaseGiftCard(input: $input) { purchaseDisplayId purchaseAmount fluzpayAmount giftCard { giftCardId status } } } ``` Variables — a \$100 card paid entirely from the "Gift card orders" spend account: ```json theme={null} { "input": { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "merchantSlug": "burger-king", "amount": 100.00, "balanceAmount": 100.00, "userCashBalanceId": "1c1b5fcc-eb21-44c5-b678-9c41f3fa21b4", "defaultToBalance": false } } ``` Setting `defaultToBalance: false` prevents any implicit fallback, so the purchase either draws from the spend account you named or fails cleanly. In an automated ordering pipeline, this is usually the behavior you want. ### Splitting a purchase across balance and another funding source `userCashBalanceId` scopes only the balance portion of a purchase. To pay part from a spend account and the remainder from a linked bank card: ```json theme={null} { "input": { "idempotencyKey": "7b2e4c91-3d18-4a55-9e07-2c8f1a6b4d33", "merchantSlug": "burger-king", "amount": 100.00, "balanceAmount": 40.00, "userCashBalanceId": "1c1b5fcc-eb21-44c5-b678-9c41f3fa21b4", "bankCardId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19" } } ``` Fluz draws $40.00 from the named spend account and charges the remaining $60.00 to the bank card. ### Learn more about spend accounts Spend accounts are part of your Fluz wallet, and they are not limited to gift cards — virtual cards are funded from a spend account, and deposits land in one. * [**Spend Accounts**](/features/spend-accounts) — the full model: balances, the default account, the account lifecycle, and how to create, rename, and close accounts. * [Get Spend Accounts](/features/get-spend-accounts) — look up your accounts and their IDs. * [Transfer Funds Between Spend Accounts](/features/transfer-between-spend-accounts) — move balance between accounts instantly. * [Create Virtual Card](/features/create-virtual-card) — fund a virtual card from a spend account. * [Deposit Funds](/features/deposit-from-external-accounts) — add funds to a specific spend account. ## Sample Response Once your purchase is complete, you'll get a response that looks something like this: ```json theme={null} { "data": { "purchaseGiftCard": { "purchaseId": "255f8245-02c7-4817-901e-15fe265f6968", "purchaseDisplayId": "1019688", "purchaseBankCardId": "255f8245-02c7-4817-901e-15fe265f6968", "bankAccountId": "255f8245-02c7-4817-901e-15fe265f6968", "purchaseAmount": 123.45, "fluzpayAmount": 85.0, "seatRewardValue": 0.05, "paypalVaultId": "255f8245-02c7-4817-901e-15fe265f6968", "createdAt": "2007-12-03T10:15:30Z", "giftCard": { "giftCardId": "85de8b3e-4e72-462c-8ed1-a6f4982e22f7", "purchaserUserId": "85de8b3e-4e72-462c-8ed1-a6f4982e22f7", "endDate": "2007-12-03T10:15:30Z", "status": "ACTIVE", "termsAndConditions": "Except as required by law, Gift Cards cannot be transferred for...", "createdAt": "2007-12-03T10:15:30Z", "merchant": { "merchantId": "85de8b3e-4e72-462c-8ed1-a6f4982e22f7", "name": "Burger King", "slug": "burger-king", "logoUrl": "https://storage.googleapis.com/.../burger-king-logo.jpg", "faceplateUrl": "https://storage.googleapis.com/.../burger-king-faceplate.png", "offers": [ { "offeringMerchantId": "85de8b3e-4e72-462c-8ed1-a6f4982e22f7", "offerId": "85de8b3e-4e72-462c-8ed1-a6f4982e22f7", "type": "GIFT_CARD_OFFER", "deliveryFormat": "CODES", "barcodeType": "C128", "hasStockInfo": false, "offerRates": [ { "maxUserRewardValue": 5.0, "cashbackVoucherRewardValue": 1.0, "boostRewardValue": 0.5, "displayBoostReward": true, "denominations": [25, 50, 100], "allowedPaymentMethods": ["CREDIT_CARD", "PAYPAL"] } ], "denominationsType": "VARIABLE", "stockInfo": [] } ] } } } } } ``` ### **Cashback rates are subject to change.** We do our best to always give our customers the best offers available. This means that our rates change regularly. Always confirm the rate before making a purchase. ## Buying more than one card A single `purchaseGiftCard` call buys exactly one gift card, on one offer, at one rate. There is no quantity field, and a call is never split or blended across offers or rates. To buy several cards, send the mutation once per card, each with its own unique `idempotencyKey`. Because each card is its own call, ordering more cards than a stocked offer has in inventory resolves per call: * `offerId `**(pinned offer):** once the stocked offer is depleted, the remaining calls fail with `GC-0009`. There is no automatic fallback to another offer or rate. * `merchantSlug `**(auto-select):** the remaining calls auto-select the next-best available offer — often a variable offer at a lower reward rate — unless `minRewardRate` blocks the lower rate. For the full per-call breakdown, the `minRewardRate` rate-floor pattern, and the `GC-0009` response, see [Purchase in Bulk](/bulk-gift-card-purchasing). ### Ordering at volume: concurrency, timeouts, and retries Purchases that draw on the same Fluz account are processed sequentially. When many `purchaseGiftCard` calls are submitted at the same time against a single account, they queue behind one another, and individual calls can take longer to return — occasionally up to a few minutes under heavy load. Calls that aren't queued typically return within seconds. To keep latency predictable and avoid false failures when ordering at volume: * **Pace your concurrent requests.** Instead of firing an entire batch simultaneously against one account, submit in smaller waves, or spread volume across multiple accounts. This keeps per-call latency low. * **Use a generous client timeout.** Fluz does not abandon an in-flight purchase after a few seconds — a request can still be legitimately processing and will return a valid result. A short client-side timeout (for example, 30 seconds) may cause you to give up on a purchase that ultimately succeeds. Set your timeout high enough to absorb occasional multi-minute processing under load. We recommend 1 minute. * **A client timeout is not a cancellation.** Closing your connection does not cancel a request that Fluz has already accepted; it continues processing to completion. Treat a timeout as an *unknown* outcome, not a failure. * **Resolve timeouts by retrying with the same** `idempotencyKey`**.** Reissue the identical request with the identical `idempotencyKey`. Because the key guarantees the purchase is processed at most once, the retry returns the original purchase if it already succeeded — it will not create a duplicate or a second charge. Never issue a new `idempotencyKey` for a purchase you've already attempted; doing so is what produces duplicate orders. If a purchase timed out on your side and you're unsure of its outcome, **retry with the same** `idempotencyKey`**, or look up the purchase by its purchase ID, before refunding the end user.** A timed-out request has often already succeeded on Fluz's side, and the gift card code remains revealable until the purchase is refunded. ## Next Steps Now it's time to reveal your gift card details for use. Learn how to do so here: [View Gift Cards](/view-gift-card) # Issue 100 cards at once Source: https://docs.fluz.app/quickstart/bulk-issue-virtual-cards Create a bulk virtual card order, poll it to completion, and handle partial failures — the programmatic issuance pattern at scale. This Quickstart issues 100 virtual cards in one request. You'll place an asynchronous bulk order, poll its status as cards are created, and retrieve the full card details — the same pattern whether you're issuing 10 cards or 10,000. > [!NOTE] > > **Prerequisites** > > * A **Fluz account with a staging application** — Steps 1–2 of any quickstart, or see [Prepare your accounts](/get-started/prepare-accounts) and [API credentials](/get-started/api-credentials). > * A token with **`CREATE_VIRTUALCARD`** to place the order and **`REVEAL_VIRTUALCARD`** to poll its status. Without the second, step 2 fails with `AUTH-0031`. > * Enough available balance to cover the order — 100 cards × the spend limit you set. > * A billing address on the account, or a `billingAddress` on each order item. Without one, card creation fails and the order completes with `failedCardCreations` equal to the order size. ## The full flow A bulk order is asynchronous. The mutation returns immediately with an `orderId` and a `PENDING` status; the cards are created in the background. **The order and the cards succeed independently** — an order can reach `COMPLETED` with some cards failed, so the order status alone never tells you whether you got what you asked for. ```mermaid theme={null} stateDiagram-v2 [*] --> PENDING: createVirtualCardBulkOrder PENDING --> IN_PROGRESS: cards being created IN_PROGRESS --> COMPLETED: all cards attempted IN_PROGRESS --> FAILED: order could not be processed COMPLETED --> AllSucceeded: successfulCardCreations == totalCards COMPLETED --> PartialFailure: failedCardCreations > 0 AllSucceeded --> [*] PartialFailure --> Reissue: place a new order for the shortfall Reissue --> [*] FAILED --> [*] ``` Always reconcile `successfulCardCreations` and `failedCardCreations` against `totalCards` before treating an order as done. `orderStatus` has one more possible value not shown above — `CANCELED` — which, like `FAILED`, means the order will produce no more cards. **1. Place the bulk order** One `createVirtualCardBulkOrder` call creates all 100 cards. Each entry in `orderItems` is a card configuration with a `quantity` — here, 100 identical single-use disbursement cards. Unlike most money-moving mutations, this one takes **no `idempotencyKey`** — the input accepts only `offerId` and `orderItems`. See [Idempotency](/concepts/idempotency) for where the key does apply. ```graphql Mutation theme={null} mutation createVirtualCardBulkOrder($input: CreateVirtualCardBulkOrderInput!) { createVirtualCardBulkOrder(input: $input) { orderId orderStatus } } ``` ```json Variables theme={null} { "input": { "offerId": "592c394e-26cc-44ac-a145-a5f81301fe77", "orderItems": [ { "quantity": 100, "spendLimit": 25.00, "lockCardNextUse": true, "cardNickname": "July payout batch", "primaryFundingSource": "FLUZ_BALANCE" } ] } } ``` ```json Response theme={null} { "data": { "createVirtualCardBulkOrder": { "orderId": "ZTBhYTg2YmQt...", "orderStatus": "PENDING" } } } ``` The `offerId` applies to **every** card in the order; mix configurations by adding more `orderItems` entries (e.g. 20 qty of \$25 single-use + 80 qty of \$100 monthly). `lockCardNextUse: true` makes each card self-destruct after its first charge — the standard disbursement pattern. > [!NOTE] > > Bulk orders are **asynchronous** — the response is an `orderId` and `PENDING`, not cards. Save the `orderId`; it's your handle for everything that follows. **2. Poll the order to completion** Track progress with `getVirtualCardBulkOrderStatus`. Cards populate incrementally while the order is `PENDING`: ```graphql Query theme={null} query getVirtualCardBulkOrderStatus($input: GetVirtualCardBulkOrderStatusInput!) { getVirtualCardBulkOrderStatus(input: $input) { orderId orderStatus successfulCardCreations failedCardCreations totalCards virtualCards { virtualCardId cardNumber expiryMMYY cvv cardHolderName } } } ``` ```json Variables theme={null} { "input": { "orderId": "" } } ``` Poll with backoff until `orderStatus` is `COMPLETED` (or `FAILED`) — while pending, `successfulCardCreations` climbs toward `totalCards` and `virtualCards` fills in. > [!WARNING] > > The response contains **full PANs, CVVs, and expiry dates in plaintext**. Treat it as sensitive cardholder data: TLS only, never log it, surface it only to authorized users. **3. Handle partial failures** A bulk order can partially succeed: `successfulCardCreations: 98, failedCardCreations: 2` on a completed order means 98 real, spendable cards and 2 that need retrying. Reconcile by comparing the counts to `totalCards`, then re-issue only the shortfall as a new order: ```json theme={null} { "input": { "offerId": "592c394e-26cc-44ac-a145-a5f81301fe77", "orderItems": [{ "quantity": 2, "spendLimit": 25.00, "lockCardNextUse": true }] } } ``` Persistent failures usually trace to program limits or funding — check [Virtual Card Error Codes](/features/virtual-card-error-codes) and your available balance. ## You're done 🎉 One hundred configured cards from one request, with a poll loop that's production-ready as-is. Next: > **[Get virtual card transactions](/features/get-virtual-card-transactions)** > > Monitor spend across the whole batch — up to 10 cards per query, or account-wide by date. > **[Send cards to recipients](/quickstart/send-a-card)** > > Distributing to people outside your org? Hosted claim links handle onboarding for you. > [!NOTE] > > **Want to learn more?** Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Your first virtual card purchase Source: https://docs.fluz.app/quickstart/create-and-spend-with-a-virtual-card Run the full happy path end to end — pick a card program, issue a card with the right settings, reveal it, and track its spend — all in the sandbox. This Quickstart takes you from a fresh Fluz account to an issued, spendable virtual card. You'll register an app, mint a scoped access token, then browse card programs, create a card configured for your use case, reveal its details, and watch its transactions — all in the sandbox, where no real money moves. > [!NOTE] > > **Prerequisites** > > * A **Fluz account** — you'll create staging API credentials and mint an access token in Steps 1–2 below. > * Requests go to the **sandbox** GraphQL endpoint. Nothing here charges a real card — see [Staging vs. Live Environment](/concepts/environments). > * Every mutation needs a unique `idempotencyKey` (a client-generated UUID) so a request is only ever processed once — see [Idempotency](/concepts/idempotency). ## Before you begin Except for minting your token (Step 2), every call is a `POST` request to a single GraphQL endpoint, authenticated with your bearer token: ```bash Endpoint & headers theme={null} POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql Authorization: Bearer Content-Type: application/json ``` ```bash Example request (cURL) theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "query GetVirtualCardOffers { getVirtualCardOffers { offerId programName rewardValue } }" }' ``` > [!TIP] > > Staging ships with **test card programs** ready to issue against, and your sandbox account includes a pre-added test bank card for funding. See [Test Merchants](/test-merchants) and [Test Bank Cards](/test-bank-cards) for the full sandbox data set. ## The full flow **1. Sign up and register an app** Create a Fluz account at [fluz.app](https://fluz.app), then open the [Developer Console](https://uni.staging.fluzapp.com/developers) and create a new **Staging** application. When the credentials appear, copy your `clientId` and `clientSecret` — the secret is shown only once. Full walkthrough: [Prepare your accounts](/get-started/prepare-accounts). **2. Exchange credentials for an access token** Mint a short-lived, scoped user access token from your app credentials. This call goes to the OAuth service; every later call uses the returned token as a Bearer credential. ```bash Mint token (cURL) theme={null} curl -X POST https://oauth-service.fluzapp.com/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "mutation ($clientId: String!, $clientSecret: String!, $scopes: [Scope!]!) { generateUserAccessToken(clientId: $clientId, clientSecret: $clientSecret, scopes: $scopes) { accessToken expiresIn scopes } }", "variables": { "clientId": "", "clientSecret": "", "scopes": ["MANAGE_PAYMENT", "CREATE_VIRTUALCARD", "EDIT_VIRTUALCARD", "REVEAL_VIRTUALCARD", "PCI_COMPLIANCE"] } }' ``` ```json Response theme={null} { "data": { "generateUserAccessToken": { "accessToken": "eyJhbGciOi...", "expiresIn": 3600, "scopes": ["MANAGE_PAYMENT", "CREATE_VIRTUALCARD", "EDIT_VIRTUALCARD", "REVEAL_VIRTUALCARD", "PCI_COMPLIANCE"] } } } ``` Store the returned `accessToken` and send it as `Authorization: Bearer ` on every request below. Tokens are short-lived (`expiresIn` is in seconds) — mint them server-side and refresh before expiry. Full details: [API credentials](/get-started/api-credentials). > [!TIP] > > Scopes gate what the token can do — include only what your flow needs: `MANAGE_PAYMENT` to fund your balance, `CREATE_VIRTUALCARD` to browse programs and issue cards, `REVEAL_VIRTUALCARD` + `PCI_COMPLIANCE` to reveal cards and pull their transactions, and `EDIT_VIRTUALCARD` to lock, unlock, or edit cards later. > [!NOTE] > > `PCI_COMPLIANCE` is granted at the application level by Fluz and can't be requested here — it has to already be enabled on your app. It's granted on request for applications operating on their own account, which covers this Quickstart. Taking raw card data onto your servers on accounts belonging to other people requires a PCI DSS certification on file; see [Due diligence](/build-a-platform#due-diligence). > [!WARNING] > > Never expose `clientSecret` in a browser or mobile client. Mint tokens server-side and forward only the token. **3. Fund your Fluz balance** Virtual cards are funded from your account when they're used — by default from your **Fluz balance** (`FLUZ_BALANCE`). Make sure there's enough available balance to cover the spend limit you plan to set. You can fund two ways: * **Manually**, via the [sandbox Fluz website](https://uni.staging.fluzapp.com/manage-money). * **Programmatically**, via the `depositCashBalance` mutation. > **Deposit via API (depositCashBalance)** > > Retrieve a funding source ID with `getWallet`, then deposit: > > ```graphql theme={null} > mutation depositCashBalance($input: DepositCashBalanceInput!) { > depositCashBalance(input: $input) { > balances { > cashBalance { availableBalance totalBalance pendingBalance } > } > } > } > ``` > > ```json theme={null} > { > "input": { > "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", > "amount": 200.00, > "depositType": "CASH_BALANCE", > "bankCardId": "" > } > } > ``` > > The [gift card Quickstart](/quickstart) walks through this step in full detail, including retrieving payment method IDs with `getWallet`. > [!NOTE] > > Prefer to fund cards from a linked bank account instead? Set `primaryFundingSource: BANK_ACCOUNT` at card creation (Step 5) and pass the `bankAccountId` — no pre-funded balance needed. **4. Browse card programs and pick an offer** Every virtual card is issued against a **card program** (an "offer"): the program determines the network, issuing bank, reward rate, and the spend limits your card must stay within. Fetch the programs available to your account with `getVirtualCardOffers`. ```graphql Query theme={null} query GetVirtualCardOffers { getVirtualCardOffers { offerId programName bin bankName rewardValue programLimits { dailyLimit weeklyLimit monthlyLimit } } } ``` ```json Response theme={null} { "data": { "getVirtualCardOffers": [ { "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "programName": "Virtual Card", "bin": "543210", "bankName": "Bank of Examples", "rewardValue": "1.5%", "programLimits": { "dailyLimit": "500.00", "weeklyLimit": "2000.00", "monthlyLimit": "5000.00" } } ] } } ``` Offers are sorted by `rewardValue`, so the highest-earning programs appear first. You can filter with the optional `input` — `cardType` (`DEBIT` / `PREPAID`), `cardNetwork` (`MASTERCARD` / `VISA`), and `cardBrandLocked`. Full reference: [Get Virtual Card Offers](/features/get-card-offers). In the sandbox, these test programs are always available: | Offer ID | Program Name | Reward Value | | - | - | - | | `ed669305-5e43-40a0-9a25-7a15ed174628` | Virtual Card | 1.5% | | `b23630f6-8d91-43df-84aa-a541e7691197` | Virtual Card - Mastercard Prepaid | 1.5% | | `592c394e-26cc-44ac-a145-a5f81301fe77` | Brand Locked Virtual Card - Mastercard Prepaid | 1.5% | > [!NOTE] > > Save the `offerId` you want and note its `programLimits` — the `spendLimit` you set in the next step must fit within the program's limit for your chosen duration. **5. Create the card, configured for your use case** Issue the card with `createVirtualCard`. The input's settings are what turn a generic card into a purpose-built one — pick the pattern that matches what you're building: **One-off purchase** A card for exactly one transaction. Set `spendLimit` to the purchase amount and `lockCardNextUse: true` so the card locks itself after its first authorization — nothing else can ever be charged to it. ```json Variables theme={null} { "input": { "idempotencyKey": "07df5653-43a8-4532-9881-3ab5857bbe12", "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "spendLimit": 150.00, "lockCardNextUse": true, "cardNickname": "Vendor payment — Acme invoice #1042" } } ``` **Subscription with a monthly cap** A card dedicated to one recurring charge. `spendLimitDuration: MONTHLY` resets the limit each month, so a price hike or duplicate charge beyond the cap is declined automatically. ```json Variables theme={null} { "input": { "idempotencyKey": "07df5653-43a8-4532-9881-3ab5857bbe13", "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "spendLimit": 29.99, "spendLimitDuration": "MONTHLY", "cardNickname": "SaaS — analytics subscription" } } ``` **Budgeted, time-boxed card** A card for a project or trip with a hard end date. `lockDate` freezes the card on that day (default is 47 months out), and disabling `usePrepaymentBalance` / `useRewardsBalance` makes it draw **only** from the specified spend account — predictable, single-source funding for clean reconciliation. ```json Variables theme={null} { "input": { "idempotencyKey": "07df5653-43a8-4532-9881-3ab5857bbe14", "offerId": "ed669305-5e43-40a0-9a25-7a15ed174628", "spendLimit": 2000.00, "lockDate": "2026-09-30", "userCashBalanceId": "", "usePrepaymentBalance": false, "useRewardsBalance": false, "cardNickname": "Q3 conference travel" } } ``` All three run the same mutation: ```graphql Mutation theme={null} mutation createVirtualCard($input: CreateVirtualCardInput!) { createVirtualCard(input: $input) { virtualCardId cardholderName virtualCardLast4 expiryMonth expiryYear status cardType initialAmount usedAmount createdAt } } ``` ```json Sample response theme={null} { "data": { "createVirtualCard": { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "cardholderName": "xyz789", "virtualCardLast4": "7890", "expiryMonth": "12", "expiryYear": "27", "status": "ACTIVE", "cardType": "MULTI_USE", "initialAmount": 150.00, "usedAmount": 0, "createdAt": "2026-07-09T10:00:00Z" } } } ``` #### The settings, at a glance **spendLimit** `Float` *required* The maximum the card can be charged — you're only ever charged for what's actually used. Must fit within the program's limit for your chosen duration. **spendLimitDuration** `VirtualCardSpendLimitDuration` (default: LIFETIME) How the limit resets. `LIFETIME` caps total spend; `DAILY` / `WEEKLY` / `MONTHLY` make it a rolling budget — the right choice for subscriptions and team allowances. **lockCardNextUse** `Boolean` (default: false) Locks the card after its first successful use — the "virtual single-use card" pattern for one-off vendor payments. **lockDate** `String` (default: 47 months from creation) `yyyy-mm-dd` date the card freezes. Time-box cards to a project, trip, or contract period. **primaryFundingSource** `VirtualCardFundingSource` (default: FLUZ\_BALANCE) Where spend is drawn from. `FLUZ_BALANCE` uses your pre-funded balance; `BANK_ACCOUNT` pulls from a linked account (requires `bankAccountId`). **usePrepaymentBalance / useRewardsBalance** `Boolean` (default: true) By default a card may also draw from prepaid (gift card) and rewards balances. Set both to `false` for cash-only cards that draw solely from the specified `userCashBalanceId` — cleanest for accounting. **memo / transactionCategory** `String` Optional expense metadata attached to the resulting transaction — categories are created on first use. See [Add Expense Details](/features/add-expense-details). > [!NOTE] > > **Billing address:** if your account doesn't have one on file, pass a `billingAddress` (or a saved `userAddressId`). It must be a real, deliverable **US** address — no PO boxes — or creation fails with `VC-0025`. See [Address Formatting Requirements](/concepts/address-formatting-requirements). > [!NOTE] > > Hold on to the `virtualCardId` from the response — you'll use it to reveal the card next. If creation fails, check [Virtual Card Error Codes](/features/virtual-card-error-codes). **6. Reveal the card details** The create response deliberately excludes the sensitive numbers. Retrieve the full PAN, CVV, and expiry with `revealVirtualCardByVirtualCardId` — this is what you (or your user) enter at a checkout or add to a mobile wallet. ```graphql Mutation theme={null} mutation RevealVirtualCard($virtualCardId: UUID!) { revealVirtualCardByVirtualCardId(virtualCardId: $virtualCardId) { cardNumber expiryMMYY cvv cardHolderName billingAddress { streetAddress postalCode city state } } } ``` ```json Variables theme={null} { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11" } ``` Requires the `REVEAL_VIRTUALCARD` and `PCI_COMPLIANCE` scopes. > [!WARNING] > > The response contains the full card number and CVV in plaintext. Treat it as sensitive cardholder data — transmit over TLS only, never log it, and display it only to the authorized user. Handling it on your own servers puts your infrastructure in PCI scope. > [!TIP] > > **Building a UI where a user reads their own card?** Use [Secure Elements Card Reveal](/build-a-platform/card-reveal) instead of this mutation. It mounts a Fluz-hosted frame inline in your page, so the card data never reaches your servers or your page's JavaScript — and you don't need a PCI DSS certification on file. > [!TIP] > > Most online checkouts don't need a PIN — but if your use case does (or you want tap-to-pay), see [Set Virtual Card PIN](/set-virtual-card-pin) and [Digital Wallet Push Provisioning](/digital-wallet-push-provisioning) to add the card to Apple Pay or Google Pay in one tap. **7. Spend, then watch the activity** Use the revealed details anywhere the card network is accepted, within the limits you set. Then pull the card's activity with `getVirtualCardTransactions` to confirm the charge — the same query powers spend dashboards, reconciliation, and decline monitoring. ```graphql Query theme={null} query GetVirtualCardTransactions { getVirtualCardTransactions( input: { virtualCardIds: ["07df5653-43a8-4532-9881-3ab5857bbe11"] filters: { transactionTypes: [PURCHASE, REFUND, DECLINE] } paginate: { limit: 20, offset: 0 } } ) { virtualCardId transactions { transactionDate transactionType transactionStatus transactionAmount merchantName mcc } } } ``` ```json Sample response theme={null} { "data": { "getVirtualCardTransactions": [ { "virtualCardId": "07df5653-43a8-4532-9881-3ab5857bbe11", "transactions": [ { "transactionDate": "2026-07-09T14:22:31Z", "transactionType": "PURCHASE", "transactionStatus": "CLEARED", "transactionAmount": 42.17, "merchantName": "ACME OFFICE SUPPLY", "mcc": 5943 } ] } ] } } ``` Requires the `PCI_COMPLIANCE` and `REVEAL_VIRTUALCARD` scopes. Omit `virtualCardIds` to pull activity across every card on the account, and filter by date range for statement-style views. Full reference: [Get Virtual Card Transactions](/features/get-virtual-card-transactions). > **Done with the card? Lock it (lockVirtualCard)** > > Cards with `lockCardNextUse` or a `lockDate` handle themselves. To lock any other card on demand: > > ```graphql theme={null} > mutation { > lockVirtualCard(input: { virtualCardId: "07df5653-43a8-4532-9881-3ab5857bbe11" }) { > virtualCardId > locked > } > } > ``` > > Requires the `EDIT_VIRTUALCARD` scope. Locking is reversible — see [Unlock Virtual Card](/features/unlock-virtual-card). ## You're done 🎉 You've issued a virtual card built for a specific job — picked a program, set spend controls, revealed the card, and tracked its activity. From here, go deeper: > **[Edit, lock & manage cards](/features/edit-virtual-card)** > > Change limits, nicknames, and lock dates on existing cards. > **[Digital wallets & PINs](/digital-wallet-push-provisioning)** > > Push cards into Apple Pay / Google Wallet and set PINs. > **[Bulk issuance](/features/create-bulk-order)** > > Create up to 10,000 cards in a single order. > **[Send cards to others](/features/send-cards)** > > Distribute cards to recipients by link, email, or SMS. > [!NOTE] > > **Want to learn more?** Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Your first gift card purchase Source: https://docs.fluz.app/quickstart/first-gift-card Run the full happy path end to end — deposit funds, browse merchants, buy a gift card, and reveal it — all in the sandbox. This Quickstart takes you from a fresh Fluz account to a completed staging purchase. You'll register an app, mint a scoped access token, then deposit funds, browse the merchant catalog, purchase a gift card, and reveal its redemption details — all in the sandbox, where no real money moves. > [!NOTE] > > **Prerequisites** > > * A **Fluz account** — you'll create staging API credentials and mint an access token in Steps 1–2 below. > * Requests go to the **sandbox** GraphQL endpoint. Nothing here charges a real card — see [Staging vs. Live Environment](/concepts/environments). > * Every mutation needs a unique `idempotencyKey` (a client-generated UUID) so a request is only ever processed once — see [Idempotency](/concepts/idempotency). ## Before you begin Every call is a `POST` request to a single GraphQL endpoint. Minting your token (Step 2) authenticates with your API Key; every other call authenticates with your bearer token: ```bash Endpoint & headers theme={null} POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql Authorization: Bearer Content-Type: application/json ``` ```bash Example request (cURL) theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "query getWallet { getWallet { bankCards { bankCardId lastFourDigits } } }" }' ``` > [!TIP] > > Your sandbox account ships with a **pre-added test bank card**, so you can run this entire flow immediately. Add more test cards or bank accounts from the [Sandbox Accounts page](https://uni.staging.fluzapp.com/accounts-and-cards) using values from the [Test Bank Cards](/test-adding-bank-cards) list. ## The full flow Five calls, each one setting up the next. The token carries the scopes; the balance funds the purchase; the merchant slug picks the offer; the purchase returns the gift card ID you reveal. ```mermaid theme={null} sequenceDiagram participant Y as Your server participant F as Fluz API Note over Y,F: Authenticate Y->>F: generateUserAccessToken (Basic API key) F->>Y: token + refreshToken + scopes Note over Y,F: Fund Y->>F: depositCashBalance (bank card or account) F->>Y: deposit PENDING, updated balance Note over Y,F: Choose Y->>F: getMerchants (name) F->>Y: merchant + slug + offers Note over Y,F: Buy Y->>F: purchaseGiftCard (slug, amount, idempotencyKey) F->>Y: purchaseDisplayId + giftCardId, status ACTIVE Note over Y,F: Redeem Y->>F: revealGiftCardByGiftCardId (giftCardId) F->>Y: code, pin, url ``` **1. Sign up and register an app** Create a Fluz account at [fluz.app](https://fluz.app), then open the [Developer Console](https://uni.staging.fluzapp.com/developers) and create a new **Staging** application. When the credentials appear, copy your **API Key**, **User ID**, and **Account ID**. Full walkthrough: [Prepare your accounts](/get-started/prepare-accounts). **2. Exchange credentials for an access token** Mint a short-lived, scoped user access token with your API Key. This call goes to the same GraphQL endpoint, authorized with `Authorization: Basic `; every later call uses the returned token as a Bearer credential. ```bash Mint token (cURL) theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "query": "mutation ($userId: UUID!, $accountId: UUID!, $scopes: [ScopeType!]!) { generateUserAccessToken(userId: $userId, accountId: $accountId, scopes: $scopes) { token scopes } }", "variables": { "userId": "", "accountId": "", "scopes": ["LIST_OFFERS", "LIST_PAYMENT", "MANAGE_PAYMENT", "MAKE_DEPOSIT", "PURCHASE_GIFTCARD", "REVEAL_GIFTCARD"] } }' ``` ```json Response theme={null} { "data": { "generateUserAccessToken": { "token": "eyJhbGciOi...", "scopes": ["LIST_OFFERS", "LIST_PAYMENT", "MANAGE_PAYMENT", "MAKE_DEPOSIT", "PURCHASE_GIFTCARD", "REVEAL_GIFTCARD"] } } } ``` Store the returned `token` and send it as `Authorization: Bearer ` on every request below. Tokens are short-lived — mint them server-side and mint a new one with the same mutation when one expires (see [Refresh an expired access token](/get-started/refresh-expired-access-token)). Full details: [API credentials](/get-started/api-credentials). > [!TIP] > > Scopes gate what the token can do — include only what your flow needs: `MANAGE_PAYMENT` to add funding sources and deposit, `LIST_OFFERS` to browse the catalog, and `PURCHASE_GIFTCARD` / `REVEAL_GIFTCARD` to buy and reveal. > [!WARNING] > > Never expose your API Key in a browser or mobile client. Mint tokens server-side and forward only the token. **3. Deposit funds to your Fluz balance** Pre-loading a Fluz balance generally makes gift card purchases faster and can bypass certain velocity checks. You can deposit two ways: * **Manually**, via the [sandbox Fluz website](https://uni.staging.fluzapp.com/manage-money). * **Programmatically**, via the `depositCashBalance` mutation (shown below). > **First, retrieve a payment method ID (getWallet)** > > To deposit via API you need the ID of a funding source (e.g. a `bankCardId`). Run `getWallet` and save the ID you want to use. > > ```bash theme={null} > curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ > -H "Authorization: Bearer " \ > -H "Content-Type: application/json" \ > -d '{ > "query": "query getWallet { getWallet { bankCards { bankCardId lastFourDigits } bankAccounts { bankAccountId } } }" > }' > ``` > > Grab the `bankCardId` or `bankAccountId` from the response. Managing funding sources via the API requires the `MANAGE_PAYMENT` scope. More detail: [View Funding Sources](/features/view-funding-sources). ### Make the deposit Use the `depositCashBalance` mutation. It takes a `DepositCashBalanceInput` object. ```graphql Mutation theme={null} mutation depositCashBalance($input: DepositCashBalanceInput!) { depositCashBalance(input: $input) { cashBalanceDeposits { cashBalanceDepositId depositDisplayId depositAmount status cashBalanceDepositType } balances { cashBalance { availableBalance totalBalance pendingBalance } giftCardCashBalance { availableBalance totalBalance pendingBalance } } } } ``` ```json Variables theme={null} { "input": { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "amount": 100.00, "depositType": "CASH_BALANCE", "bankCardId": "88d2aea1-9461-419e-9ac7-87693b6eea1e" } } ``` #### Input fields **idempotencyKey** `string` *required* A unique client-generated UUID that guarantees the deposit is processed only once. **amount** `Float` *required* The amount to deposit. **depositType** `CashBalanceDepositType` Destination balance. One of `CASH_BALANCE`, `GIFT_CARD_BALANCE`, or `RESERVE_BALANCE`. **Funding source** `UUID` Where the money comes from — provide **one** of `bankAccountId`, `bankCardId`, or `paypalVaultId`. **merchantCategoryCode** `Int` Only for `GIFT_CARD_BALANCE`. A four-digit MCC classifying the business. Use `getMccList` to retrieve valid values. **userCashBalanceId** `UUID` When `CASH_BALANCE` is selected, the specific spend account to deposit into. > **Sample response** > > ```json theme={null} > { > "data": { > "depositCashBalance": { > "cashBalanceDeposits": [ > { > "cashBalanceDepositId": "5f5c5b5f-4c4b-4e4e-9e9e-4f4f4f4f4f4f", > "depositDisplayId": "102370", > "depositAmount": "100.00", > "status": "COMPLETED", > "cashBalanceDepositType": "CASH_BALANCE" > } > ], > "balances": { > "cashBalance": { > "availableBalance": "100.00", > "totalBalance": "100.00", > "pendingBalance": "0.00" > }, > "giftCardCashBalance": { > "availableBalance": "0.00", > "totalBalance": "0.00", > "pendingBalance": "0.00" > } > } > } > } > } > ``` > [!NOTE] > > Deposits may settle instantly or within 2–5 business days depending on the funding source and settlement type. The `balances` object in the response reflects your current available balance. See [Check Account Balance](/check-account-balance) to re-query it at any time. **4. Browse merchants and pick an offer** With a balance ready, fetch the catalog of available merchants and their cashback offers using `getMerchants`. ```graphql Query theme={null} query GetMerchantCatalog( $paginate: OffsetInput $offerTypes: OfferTypesInput $filterBy: FilterByInput ) { getMerchants(paginate: $paginate, offerTypes: $offerTypes, filterBy: $filterBy) { merchantId name slug logoUrl faceplateUrl offers { offeringMerchantId offerId type deliveryFormat barcodeType offerRates { maxUserRewardValue cashbackVoucherRewardValue boostRewardValue denominations allowedPaymentMethods } } } } ``` ```json Variables theme={null} { "paginate": { "limit": 20, "offset": 0 }, "offerTypes": { "giftCardOffer": true, "cardLinkedOffer": false } } ``` > [!TIP] > > The catalog is sorted by cashback percentage by default, so the highest offers appear first. #### Useful arguments **name** `String` Filter merchants by name. **paginate** `OffsetInput` `{ limit, offset }`. Default and max `limit` is `20`. **offerTypes** `OfferTypesInput` Boolean flags for which offer types to return, e.g. `{ giftCardOffer: true, cardLinkedOffer: false }`. **filterBy** `FilterByInput` Filter offers within each merchant, e.g. by `deliveryFormat` (`URL`, `CODES`, `PIN_AS_CODE`, `PIN_WITH_URL`). > [!NOTE] > > **Pagination:** the response may return fewer results than your `limit`. To pull the **full** catalog, keep incrementing `offset` by your `limit` and stop when the API returns an empty array (`[]`). The unfiltered catalog is large — fetch it at most once per day, and use `name` or `offerTypes` for targeted lookups. > **Optional: get the single best offer for a merchant (getOfferQuote)** > > If you already know the merchant and amount, `getOfferQuote` returns the top available offer directly — including live stock info. > > ```graphql Query theme={null} > query getOfferQuote($input: GetOfferQuoteInput!) { > getOfferQuote(input: $input) { > offeringMerchantId > offerId > type > hasStockInfo > denominationsType > termsAndConditions > offerRates { > maxUserRewardValue > denominations > allowedPaymentMethods > } > stockInfo { > ... on StockInfoVariableType { > __typename > description > maxDenomination > minDenomination > } > ... on StockInfoFixedType { > __typename > denomination > availableStock > } > } > } > } > ``` > > ```json Variables theme={null} > { > "input": { > "merchantSlug": "burger-king", > "denomination": 50.00, > "paymentMethod": "FLUZPAY" > } > } > ``` > > `merchantSlug` and `denomination` are required. `paymentMethod` defaults to `FLUZPAY` (your Fluz balance) and also accepts `BANK_CARD`, `BANK_ACCOUNT`, `PAYPAL`, `APPLE_PAY`, and `GOOGLE_PAY`. > [!WARNING] > > Cashback rates change regularly. Always confirm the current rate before purchasing. **5. Purchase a gift card** Use the `purchaseGiftCard` mutation. You can identify what to buy in two ways: **Option A — Merchant slug (recommended)** Pass a `merchantSlug` and Fluz automatically applies the best available offer for that merchant. ```graphql Mutation theme={null} mutation purchaseGiftCard($input: PurchaseGiftCardInput!) { purchaseGiftCard(input: $input) { purchaseDisplayId purchaseAmount giftCard { giftCardId status termsAndConditions } } } ``` ```json Variables theme={null} { "input": { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "merchantSlug": "burger-king", "amount": 50.00, "balanceAmount": 50.00 } } ``` **Option B — Specific offer ID** Pass an `offerId` you selected from the catalog to purchase that exact offer. ```graphql Mutation theme={null} mutation purchaseGiftCard($input: PurchaseGiftCardInput!) { purchaseGiftCard(input: $input) { purchaseDisplayId purchaseAmount giftCard { giftCardId status termsAndConditions } } } ``` ```json Variables theme={null} { "input": { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "offerId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "amount": 50.00, "balanceAmount": 50.00 } } ``` #### Input fields **idempotencyKey** `string` *required* A unique client-generated UUID so the purchase is processed only once. **offerId / merchantSlug** `UUID / String` *required* Provide **one**. `merchantSlug` auto-selects the best rate; `offerId` targets a specific offer. **amount** `Float` *required* The gift card amount to purchase. **Funding source** `UUID / Float` How to pay. Use `balanceAmount` (Fluz balance), `bankAccountId`, `bankCardId`, or `paypalVaultId`. You may combine your Fluz balance with another source. **defaultToBalance** `Boolean` (default: true) Falls back to your Fluz balance if another payment method fails. Set to `false` to disable that fallback. **minRewardRate** `Float` Minimum reward rate to accept when purchasing via `merchantSlug`. **exclusiveRateId** `UUID` Forces a specific exclusive rate. Found in `getMerchants` for offers of type `EXCLUSIVE_RATE_OFFER`. **userCashBalanceId** `UUID` The spend account (cash balance) to charge. **memo / transactionCategory / attachmentId** `String / String / UUID` Optional expense metadata. `memo` max 255 chars; categories are created on first use. See [Add Expense Details](/features/add-expense-details). > **Sample response** > > ```json theme={null} > { > "data": { > "purchaseGiftCard": { > "purchaseDisplayId": "1019688", > "purchaseAmount": 50.00, > "giftCard": { > "giftCardId": "85de8b3e-4e72-462c-8ed1-a6f4982e22f7", > "status": "ACTIVE", > "termsAndConditions": "Except as required by law, Gift Cards cannot be transferred for..." > } > } > } > } > ``` > [!NOTE] > > Hold on to the `giftCardId` from the response — you'll use it to reveal the card in the next step. If a purchase fails, check [Gift Card Error Codes](/gift-card-error-codes). **6. Reveal the gift card details** Finally, retrieve the redeemable details (code, PIN, and/or URL). > **Don't have the giftCardId? List your cards first (getGiftCards)** > > Skip this if you just captured a `giftCardId` in Step 3. Otherwise, list your gift cards: > > ```graphql theme={null} > query GetGiftCards { > getGiftCards(paginate: { limit: 20, offset: 0 }) { > giftCardId > status > createdAt > deliveryFormat > termsAndConditions > merchant { merchantId name slug } > } > } > ``` > > You can filter with `status` and `userCashBalanceId`, and page with `paginate`. ### Reveal redemption details Call `revealGiftCardByGiftCardId` with the `giftCardId`. ```graphql Mutation theme={null} mutation RevealGiftCard($giftCardId: UUID!) { revealGiftCardByGiftCardId(giftCardId: $giftCardId) { code pin url termsAndConditions } } ``` ```json Variables theme={null} { "giftCardId": "7c57c381-4b19-49e4-bbb0-404a45166ee4" } ``` > **Sample response** > > ```json theme={null} > { > "data": { > "revealGiftCardByGiftCardId": { > "code": "9877890000000000", > "pin": "2014", > "url": null, > "termsAndConditions": "Except as required by law, Gift Cards cannot be transferred for..." > } > } > } > ``` > [!NOTE] > > Redemption fields vary by merchant. Some cards return an alphanumeric `code` with no `pin`; others return only a `url`. Always render based on the `deliveryFormat` returned by **`getGiftCards`** (not `getMerchants`) — a merchant's active offer can change after purchase, and `getGiftCards` reflects the format the card was actually bought under. > [!WARNING] > > **Details not returned immediately?** Poll `revealGiftCardByGiftCardId` with exponential backoff: start at **300ms**, then double (300 → 600 → 1200 → 2400ms…) up to a max delay of **180000ms (3 minutes)**. Stop as soon as details come back. This balances responsiveness with load and avoids unnecessary timeouts. ## You're done 🎉 You've run a complete transaction — funded a balance, browsed offers, purchased a gift card, and revealed it. From here, explore the rest of the API: > **[Virtual Cards](/features/virtual-cards)** > > Issue and manage network-accepted virtual cards. > **[Wallets & Transfers](/features/create-spend-accounts)** > > Open spend accounts and move funds between them. > **[Transaction Activity](/features/get-all-transactions)** > > Pull, filter, and annotate transaction history. > **[Embedded Widgets](/developers/widgets)** > > Drop Fluz flows straight into your own UI. > [!NOTE] > > **Want to learn more?** Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Move money through a wallet Source: https://docs.fluz.app/quickstart/move-money-through-a-wallet The full ledger lifecycle — create a spend account, deposit into it, transfer between accounts, withdraw back out, and verify every balance. This Quickstart runs money through the complete wallet lifecycle: open a purpose-built spend account, fund it, move funds between accounts, send them back out to an external account, and verify the ledger at each step — the plumbing under every card and purchase flow. > [!NOTE] > > **Prerequisites** > > * A **Fluz account with a staging application** — Steps 1–2 of any quickstart, or see [Prepare your accounts](/get-started/prepare-accounts) and [API credentials](/get-started/api-credentials). > * A linked **test bank account** for the deposit and withdrawal legs — your sandbox ships with test funding sources, or add more from [Test Bank Accounts](/test-adding-bank-accounts). > * Every money-moving mutation needs a unique `idempotencyKey` — see [Idempotency](/concepts/idempotency). ## The full flow Money enters from an external funding source, moves between spend accounts inside Fluz, and leaves the same way it came. Each arrow is one mutation, and each needs its own scope. ```mermaid theme={null} flowchart LR EXT[External bank account
or card] NEW[New spend account
Team Travel] DEF[Default spend account] EXT -->|depositCashBalance
MAKE_DEPOSIT| NEW NEW -->|transferInternalBalance
MAKE_INTERNAL_TRANSFER| DEF NEW -->|withdrawCashBalance
MAKE_WITHDRAWAL| EXT style EXT fill:#e8e8e8,stroke:#888 style NEW fill:#d4edda,stroke:#5a9 style DEF fill:#d4edda,stroke:#5a9 ``` Transfers between your own spend accounts settle immediately. Deposits and withdrawals involve an external institution, so they land as `PENDING` and settle on the network's timetable. **1. Mint a token with the five wallet scopes** This flow touches five scopes — one per capability: ```json theme={null} { "scopes": ["MANAGE_PAYMENT", "LIST_PAYMENT", "MAKE_DEPOSIT", "MAKE_INTERNAL_TRANSFER", "MAKE_WITHDRAWAL"] } ``` `MANAGE_PAYMENT` creates accounts and deposits, `LIST_PAYMENT` reads them, `MAKE_INTERNAL_TRANSFER` moves funds between your accounts, and `MAKE_WITHDRAWAL` sends funds out. Minting details: [API credentials](/get-started/api-credentials). **2. Create a spend account** Spend accounts are named sub-ledgers — "Team Travel", "Operations", "Marketing" — that keep budgets separate while living under one Fluz account. ```graphql Mutation theme={null} mutation createUserCashBalance($input: CreateUserCashBalanceInput!) { createUserCashBalance(input: $input) { userCashBalanceId nickname availableCashBalance status } } ``` ```json Variables theme={null} { "input": { "nickname": "Team Travel" } } ``` ```json Response theme={null} { "data": { "createUserCashBalance": { "userCashBalanceId": "f8a3c9e1-7b2d-4f5e-9c8a-1d2e3f4a5b6c", "nickname": "Team Travel", "availableCashBalance": "0", "status": "ACTIVE" } } } ``` Save the `userCashBalanceId` — every later step addresses the account by it. Your account also has a **default** spend account already; you'll transfer between the two in Step 4. **3. Deposit into it** Fund the new account from a linked source with `depositCashBalance`, targeting it via `userCashBalanceId`: ```graphql Mutation theme={null} mutation depositCashBalance($input: DepositCashBalanceInput!) { depositCashBalance(input: $input) { cashBalanceDeposits { depositAmount status } balances { cashBalance { availableBalance totalBalance pendingBalance } } } } ``` ```json Variables theme={null} { "input": { "idempotencyKey": "3c9e1f8a-2b7d-4e5f-8c9a-6b5a4f3e2d1c", "amount": 100.00, "depositType": "CASH_BALANCE", "userCashBalanceId": "", "bankAccountId": "" } } ``` Deposits may settle instantly or within 2–5 business days depending on the source; the returned `balances` reflect what's available right now. Funding source IDs come from `getWallet` — see [View Funding Sources](/features/view-funding-sources). **4. Transfer between your accounts** Move \$25 from the new account into your default one with `transferInternalBalance`. Internally it's recorded as two linked movements — a withdraw from the source and a deposit into the destination — and the response returns both: ```graphql Mutation theme={null} mutation transferInternalBalance($input: TransferInternalBalanceInput!) { transferInternalBalance(input: $input) { withdraw { withdrawId amount status userCashBalanceId } cashBalanceDeposit { cashBalanceDepositId depositAmount status } } } ``` ```json Variables theme={null} { "input": { "idempotencyKey": "1f1df3e7-5d43-4e3d-83de-31922d4aefb7", "amount": 25.00, "sourceUserCashBalanceId": "", "destinationUserCashBalanceId": "" } } ``` Both IDs must be your own spend accounts, they must differ, and the source needs sufficient available balance. Sending to a **different Fluz user** is a different operation — [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet). **5. Withdraw back out** Complete the round trip by sending funds to an external account with `withdrawCashBalance` — here via ACH: ```graphql Mutation theme={null} mutation withdrawCashBalance($input: WithdrawCashBalanceInput!) { withdrawCashBalance(input: $input) { withdraws { withdrawId amount status displayStatus withdrawMethod } } } ``` ```json Variables theme={null} { "input": { "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000", "amount": 50.00, "method": "BANK_ACH", "source": "CASH_BALANCE", "cashBalanceId": "", "bankAccountId": "" } } ``` `method` also accepts `BANK_CARD` (push-to-card, `bankCardId`), `PAYPAL` (`paypalVaultId`), and `VENMO` (`venmoAccountId`) — each requires its matching ID field. ACH typically settles in 1–3 business days, so expect `PENDING` / `PROCESSING` at first. Full reference: [Withdraw to External Account](/features/withdraw-to-external-account). **6. Verify the ledger** Close the loop by reading the accounts back with `getUserCashBalances`: ```graphql theme={null} query { getUserCashBalances { userCashBalances { userCashBalanceId nickname availableCashBalance totalCashBalance lifetimeCashBalance isDefault status } totalCount } } ``` You should see the story in the numbers: "Team Travel" with `lifetimeCashBalance` of 100.00 and a reduced available balance (25 transferred out, 50 withdrawing), and your default account up 25.00. `lifetimeCashBalance` only ever grows — it's the audit trail of everything ever deposited. ## You're done 🎉 You've run the complete ledger lifecycle — create, fund, move, pay out, verify. These accounts are the substrate everything else draws on: > **[Fund a virtual card from this account](/quickstart/create-and-spend-with-a-virtual-card)** > > Point `userCashBalanceId` at your new account for clean per-budget card spend. > **[Send funds to another Fluz user](/features/account-to-account-transfers)** > > Look up a recipient and move money wallet-to-wallet. > **[Manage spend accounts](/features/edit-spend-accounts)** > > Rename, close, and administer accounts over time. > **[Transaction activity](/features/get-all-transactions)** > > Every movement above, in one filterable feed with memos and categories. > [!NOTE] > > **Want to learn more?** Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Onboard & verify a business Source: https://docs.fluz.app/quickstart/onboard-businesses Run the business happy path end to end — configure a business-only application, send an owner through OAuth, register the legal entity, and track KYB to an approved business account. This Quickstart is [Onboard & Connect a Customer](/quickstart/onboard-customers) for businesses. You'll configure an application that can only ever land on a business account, walk an owner through the OAuth grant, register their legal entity and beneficial ownership roster, track the KYB case to a decision, and prove the connection by issuing a card on the business account. The person in front of you is always an individual first. They sign in as themselves, they get identity-verified as themselves, and *then* they register a business. Everything below follows that order. > [!NOTE] > > **Prerequisites** > > * A **Fluz account with a staging application** — see [Prepare your accounts](/get-started/prepare-accounts) and [API credentials](/get-started/api-credentials). It must be an **OAuth Integration** application that Fluz has moved to `ACTIVE`; apps made with **Create new app** fail with `AUTH-0030`. > * A `client_id`, `client_secret`, and registered `redirect_uri` — see [Configure OAuth App](/create-an-o-auth-app). > * Requests go to the **sandbox** — no real money, no real PII. See [Staging vs. Live Environment](/concepts/environments). ## Which token, when Two tokens appear in this flow, and using the wrong one is the most common cause of a failed first run. | Steps | Token | Minted with | | :- | :- | :- | | KYC, `getBusinessCategories`, `registerBusiness` | **`CONSUMER`** — the applicant's personal account | The applicant's `userId` and personal `accountId` | | `getBusiness`, owner verification links, everything after approval | **`BUSINESS`** | The same `userId` and the new business `accountId` | ## The full flow **1. Configure the application for businesses** Two permission lists live on the **Permissions** tab of your app, and they are edited independently. | List | What to put on it | | :- | :- | | **Permissions** | What your app may do on a **personal** account. Include the permission to **register a business** — it is granted by the personal account, so it belongs here and nowhere else. | | **Business permissions** | What your app may do on a **business** account once one exists. | A non-empty **Business permissions** list is what makes your app business-enabled. Leave it empty and users are never offered the option to apply for a business account, no matter what else you configure. While you're in the app editor, register your `redirect_uri` on the **OAuth** tab and add a webhook URL subscribed to **KYB status update** — that's how you'll learn the review finished without polling. > [!WARNING] > > The consent screen is built from these lists, not from your authorize URL. A permission you don't select here is never offered to the user and can never appear on a token. Full detail: [Business Accounts in OAuth](/business-accounts-in-o-auth). **2. Ask Fluz to restrict the app to business accounts** Fluz can configure your application so the flow never settles on a personal account. With that in place, a user who has no business account skips the account picker entirely and goes straight into business registration. > [!WARNING] > > **This isn't self-serve.** Contact your Fluz account manager to have the restriction applied to your application. Until it is, users with no business account are offered their personal account alongside the option to apply for a business one — and some of them will pick the personal account. Skip this if your app legitimately serves both consumers and businesses. Asking for it is a statement that a personal-account grant is always a bug for you. **3. Send the owner through the OAuth grant** Build the authorize URL: ```text Authorization URL theme={null} https://uni.staging.fluzapp.com/authorize ?response_type=code &client_id= &redirect_uri= &state= &external_id= ``` With the app restricted to business accounts, a first-time user sees: sign-in and 2FA, then a consent screen carrying **two groups** — the consumer permissions they're granting now, and the business permissions being pre-approved for the business they're about to create. The list is read-only; they accept all of it or they don't finish. If the app isn't restricted, the user picks **Apply for a business account** on the account picker. They're redirected back to your `redirect_uri` with `?code=...&state=...`. Validate `state`, then capture the single-use `code` server-side. > [!NOTE] > > **Did you create this owner with `registerUser`?** Registration doesn't authorize your app, so this step is still required. These users have no password and sign in with a one-time code sent by SMS to the registered phone — authenticator apps can't complete it. In staging, no SMS is sent: enter `123456`. > > **Known issue:** some users created with `registerUser` are missing an OAuth setting, so `registerBusiness` later fails with `AUTH-0008`. Until the fix ships, make sure this pass through `/authorize` chooses **Apply for a business account** and reuses the same `external_id` as the user's existing grant. > [!TIP] > > Give the **business** its own `external_id`, derived from your own business record — not from the owning user. An external ID binds to one Fluz account on first use, so an ID you spend on someone's personal account can't be reused for their business. Full parameter reference: [Client-Facing OAuth Grant Flow](/client-facing-o-auth-grant-flow). **4. Exchange the code for the applicant's token** Same exchange as any other grant — Basic auth with base64 of `client_id:client_secret`: ```bash Exchange (cURL) theme={null} curl -X GET "https://uni.staging.fluzapp.com/token/exchange?code=&redirect_uri=" \ -H "Authorization: Basic " ``` The `redirect_uri` must byte-match the one you used at `/authorize`. > [!NOTE] > > The business doesn't exist yet, so this token belongs to the applicant's **personal** account — that is correct, and it's the token `registerBusiness` requires. Read the account off the exchange response and persist it rather than inferring it from your own records of who started the flow. > [!WARNING] > > This consent is what lets the applicant register a business. A `CONSUMER` token from `generateUserAccessToken` works for `registerBusiness` only after it, and only when you mint it with the same `externalReferenceId` as the `external_id` above. See [Applicant authorization](/kyb-overview#applicant-authorization). **5. Verify the applicant's identity (KYC)** KYB verifies the business and the *other* owners. It does not verify the applicant, so the applicant has to be verified before you register anything — otherwise registration fails with `ARG-0001`. Call `verifyUserInformation` with the applicant's token. In staging this test identity always returns `APPROVED`: ```graphql Mutation theme={null} mutation VerifyUserInformation( $firstName: String! $lastName: String! $streetLine1: String! $city: String! $state: String! $postalCode: String! $country: String! $dateOfBirth: String! $ssnLast4: String! ) { verifyUserInformation( firstName: $firstName lastName: $lastName streetLine1: $streetLine1 city: $city state: $state postalCode: $postalCode country: $country dateOfBirth: $dateOfBirth ssnLast4: $ssnLast4 ) { status message } } ``` ```json Variables (approved test identity) theme={null} { "firstName": "John", "lastName": "Smith", "streetLine1": "222333 Peachtree Place", "city": "Atlanta", "state": "GA", "postalCode": "30318", "country": "United States", "dateOfBirth": "02/28/1975", "ssnLast4": "3333" } ``` Which check the applicant needs depends on the `isUsPerson` value you'll send for them in the next step: `true` requires a successful SSN (CIP) verification on file, `false` requires a successful document verification. See [User KYC Verification](/user-kyc-verification) and [Testing KYC Flows](/test-kyc-flows). > [!WARNING] > > A user can be submitted at most **3 times** before returning `ERROR`. Don't burn attempts on the user you're about to make an applicant. An `ERROR` reading `User is already KYC verified` doesn't count as an attempt. > [!NOTE] > > If the applicant's owner record later shows `PENDING_CIP` with `verificationType: SSN` and `provided: false`, they were `isUsPerson: true` without a successful SSN verification on file. Run this step for them with the SSN path. **6. Register the business** First resolve the category the entity trades under — never hardcode these UUIDs: ```graphql theme={null} query { getBusinessCategories { id name subCategories { id name } } } ``` Then submit the entity and the full ownership roster in one call, still with the applicant's **personal-account** token: ```graphql Mutation theme={null} mutation RegisterBusiness($input: RegisterBusinessInput!) { registerBusiness(input: $input) { accountId kybStatus success error { code message } } } ``` ```json Variables theme={null} { "input": { "businessName": "Acme Corporation", "dbaName": "Acme Co", "businessStructure": "LLC", "businessLegalAddress": { "streetAddressLine1": "123 Main Street", "city": "San Francisco", "state": "California", "postalCode": "94102", "country": "United States" }, "stateOfIncorporation": "California", "taxId": "12-3456789", "businessCategoryId": "", "businessSubCategoryId": "", "natureOfBusiness": "E-commerce retail", "businessAccountUsage": ["CORPORATE_SPENDING_ADMIN"], "externalReferenceId": "your-business-id-123", "confirm": { "allOwnersWith25PercentageOwnershipListed": true, "noOwnersMoreThan25Percentage": false }, "owners": [ { "firstName": "John", "lastName": "Smith", "emailAddress": "john@example.com", "phoneNumber": "+14155551234", "title": "Chief Executive Officer", "ownershipPercentage": 100, "isControlPerson": true, "isInvited": false, "isUsPerson": true } ] } } ``` A success response returns an `accountId` and a `kybStatus` of `SUBMITTED`. Store the `accountId` immediately — it's your only handle on the application. Four things that reject most first attempts: * **The wrong token.** `getBusinessCategories` and `registerBusiness` both need the applicant's `CONSUMER` token. A `BUSINESS` token fails with `AUTH-0002` ("requires account type CONSUMER"). * **`isUsPerson` is required on every owner**, including the applicant and invited owners. It's the single most common cause of a rejected roster. * **Exactly one owner must be the applicant** — matched by email or phone against the token's user — and exactly one must be the control person. Beneficial owners are only required for anyone holding 25% or more. * **The legal address is checked against an address-validation provider.** Invented streets fail with `BS-0002`; use [Test Addresses](/test-addresses). > [!WARNING] > > Errors come back **inside the response payload**, not as GraphQL errors — branch on `success` and the `error` object. Full parameter and error reference: [Register a Business](/business-registration). Staging tax IDs that force an outcome: [KYB Testing](/KYB-testing). **7. Get the remaining owners verified, then wait** `SUBMITTED` means the payload validated and a case opened. It does not mean approved. Mint a **business-account** token — `generateUserAccessToken` with the applicant's `userId` and the new business `accountId` — and read the roster. The applicant's grant extends to the new business automatically, so this works while KYB is still pending. ```graphql theme={null} query GetBusiness { getBusiness { accountId kybStatus owners { id name verificationType status } } } ``` Owners on the document path get a link from [requestOwnerDocumentVerificationLink](/owner-verification-link); owners on the SSN path verify as themselves with `verifyUserInformation` (the link returns `ARG-0001` for them). Owners you marked `isInvited: true` are emailed by Fluz and verify themselves. Keep going until `kybStatus` is final **and** every owner reports `READY` — an approved business can still have an owner at `PENDING_CIP`. The status moves `PENDING` → `APPROVED` or `DECLINED`, usually within one to two business days. Take the decision off the **KYB status update** webhook you configured in Step 1 and use `getBusiness` for reconciliation — hourly, not per page load. > [!NOTE] > > Show the user an honest "under review" state. Don't drop them into a business dashboard that can't transact yet, and don't auto-retry a decline — a second submission is blocked by `BS-0007`. There's no API to change the business's legal name, address, or owners after submission; those changes go through Fluz support. Full lifecycle: [Register & Verify Businesses](/kyb-overview). **8. Operate on the business account — prove it** Once `kybStatus` is `APPROVED`, run any Fluz operation with the **business-account** token and it executes against the business. There is no separate business API. ```graphql theme={null} mutation { createVirtualCard( input: { idempotencyKey: "9f2c4d61-77aa-4b0e-8f2a-1c9d3e5b7a04" offerId: "ed669305-5e43-40a0-9a25-7a15ed174628" spendLimit: 250.00 lockCardNextUse: true cardNickname: "First card on a verified business" } ) { virtualCardId virtualCardLast4 status } } ``` An `ACTIVE` card back means the loop is closed: configured → authorized → verified → registered → approved → operating. ## You're done 🎉 You've taken a business from an empty application to a verified account that can spend. From here: > **[Business accounts in OAuth](/business-accounts-in-o-auth)** > > The two permission lists, the account picker, and which account a code resolves to. > **[Register & verify businesses](/kyb-overview)** > > Prerequisites, the KYB status lifecycle, and tracking a case to a decision. > **[Submit business documents](/submit-business-documents)** > > Authorized-signer uploads and responding to documentation requests. > **[KYB testing](/KYB-testing)** > > Staging tax IDs, owner verification, and the errors you'll hit along the way. > **[Onboard & connect a customer](/quickstart/onboard-customers)** > > The same journey for individuals. > [!NOTE] > > **Want to learn more?** Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Onboard & connect a customer Source: https://docs.fluz.app/quickstart/onboard-customers Run the platform happy path end to end — register a customer, connect their account with OAuth, verify their identity, and operate on their behalf. This Quickstart is the "Build a platform" story made runnable. You'll register a customer programmatically, walk the OAuth grant to get a token scoped to *their* account, verify their identity with the sandbox KYC flow, and prove the connection by issuing a virtual card on their behalf — with the exact same API you use on your own account. > [!NOTE] > > **Prerequisites** > > * A **Fluz account with a staging application** — Steps 1–2 of any quickstart, or see [Prepare your accounts](/get-started/prepare-accounts) and [API credentials](/get-started/api-credentials). > * An **OAuth-configured app**: a `client_id`, `client_secret`, and registered `redirect_uri` — see [Create an OAuth App](/create-an-o-auth-app). > * Requests go to the **sandbox** — no real money, no real PII. See [Staging vs. Live Environment](/concepts/environments). > [!WARNING] > > **Registering users requires special permission.** The `registerUser` mutation is gated per application (`AUTH-0022` if not enabled) — contact your account manager to enable it. No registration access yet? Skip Step 1 and run the flow with any existing staging user. ## The full flow **1. Register the customer** Provision a complete user — account, balances, and referral capability — with `registerUser`, authenticated with **your application's** token. ```graphql Mutation theme={null} mutation RegisterUser( $firstName: String! $lastName: String! $phoneNumber: String! $regionCode: String! $emailAddress: String! $dateOfBirth: String! $billingAddress: VirtualCardBillingAddressInput! $acceptCardholderAgreement: Boolean! ) { registerUser( firstName: $firstName lastName: $lastName phoneNumber: $phoneNumber regionCode: $regionCode emailAddress: $emailAddress dateOfBirth: $dateOfBirth billingAddress: $billingAddress acceptCardholderAgreement: $acceptCardholderAgreement ) { success error { code message } } } ``` ```json Variables theme={null} { "firstName": "Jane", "lastName": "Doe", "phoneNumber": "5551234567", "regionCode": "US", "emailAddress": "jane.doe@example.com", "dateOfBirth": "1990-05-15", "billingAddress": { "streetAddressLine1": "1600 Amphitheatre Pkwy", "city": "Mountain View", "state": "CA", "postalCode": "94043", "country": "United States" }, "acceptCardholderAgreement": true } ``` > [!WARNING] > > This mutation returns errors **in the response data**, not as GraphQL errors — always check `success` and handle the `error` object. Common failures: `AUTH-0026` / `AUTH-0027` (phone or email already in use), `AUTH-0004` (invalid phone). Full list: [User Registration](/user-registration). **2. Send the customer through the OAuth grant** The customer authorizes your app by visiting the hosted grant page. Build the URL with your OAuth settings and the scopes your integration needs: ```text Authorization URL theme={null} https://uni.staging.fluzapp.com/authorize ?response_type=code &client_id= &redirect_uri= &scopes=CREATE_VIRTUALCARD%20REVEAL_VIRTUALCARD%20VERIFY_KYC &state= ``` * `scopes` is **space-delimited** (URL-encoded as `%20`) and must be a subset of the scopes enabled on your app — unknown scopes are silently ignored. * `state` is echoed back unmodified — use it to correlate the redirect with your session. The customer sees the Fluz permissions screen, approves, and is redirected to your `redirect_uri` with `?code=...&state=...`. That `code` is single-use — capture it server-side. Full parameter reference: [Client-Facing OAuth Grant Flow](/client-facing-o-auth-grant-flow). **3. Exchange the code for the customer's token** Trade the authorization code for the customer-scoped `accessToken` (plus a `refreshToken`). This call authenticates with **Basic auth**: base64 of `client_id:client_secret`. ```bash Exchange (cURL) theme={null} curl -X GET "https://uni.staging.fluzapp.com/token/exchange?code=&redirect_uri=" \ -H "Authorization: Basic " ``` ```json Response (truncated) theme={null} { "accessToken": "eyJhbGciOi...", "accessTokenExpiresAt": "2026-07-13T21:05:30.673Z", "refreshToken": "8ec16c25951616150b0332a4a6d66547", "refreshTokenExpiresAt": "2026-08-13T20:55:30.738Z", "scope": ["CREATE_VIRTUALCARD", "REVEAL_VIRTUALCARD", "VERIFY_KYC"], "user": { "id": "5070d5a1-d71a-4190-91b0-f116eec51771" } } ``` The `redirect_uri` must match the grant step **exactly**. Store the `refreshToken` and rotate before `accessTokenExpiresAt` — see [Refresh an OAuth Access Token](/refresh-o-auth-access-token). > [!TIP] > > Onboarding many customers? Append your own identifier as `external_id` on the authorize URL to link Fluz accounts to records in your system — see [Managing External Reference IDs](/managing-external-reference-ids). **4. Verify the customer's identity (KYC)** Money movement requires a verified identity. Call `verifyUserInformation` **with the customer's token** — the token determines who gets verified. In staging, this test identity always returns `APPROVED`: > [!WARNING] > > This mutation requires the **`VERIFY_KYC`** scope, and `VERIFY_KYC` is not self-serve — it has to be enabled on your application by Fluz first (see [User KYC Verification](/user-kyc-verification)). Until it is, the grant page silently drops it from the requested scopes and this step fails with `AUTH-0031`. ```graphql Mutation theme={null} mutation VerifyUserInformation( $firstName: String! $lastName: String! $streetLine1: String! $streetLine2: String $city: String! $state: String! $postalCode: String! $country: String! $dateOfBirth: String! $ssnLast4: String! ) { verifyUserInformation( firstName: $firstName lastName: $lastName streetLine1: $streetLine1 streetLine2: $streetLine2 city: $city state: $state postalCode: $postalCode country: $country dateOfBirth: $dateOfBirth ssnLast4: $ssnLast4 ) { status message } } ``` ```json Variables (approved test identity) theme={null} { "firstName": "John", "lastName": "Smith", "streetLine1": "222333 Peachtree Place", "streetLine2": "", "city": "Atlanta", "state": "GA", "postalCode": "30318", "country": "United States", "dateOfBirth": "02/28/1975", "ssnLast4": "3333" } ``` > [!WARNING] > > **Three-attempt limit:** a user can be submitted at most 3 times before returning `ERROR` — don't burn attempts on a user you need. Full sandbox outcomes: [Testing KYC Flows](/test-kyc-flows). **5. Operate on their account — prove it** This is the payoff: run any Fluz operation with the **customer's token** and it executes against **their** account. `createVirtualCard` is `createVirtualCard` — no separate platform API. ```graphql theme={null} mutation { createVirtualCard( input: { idempotencyKey: "1b7de2f8-c0a9-4532-9881-3ab5857bbe15" offerId: "ed669305-5e43-40a0-9a25-7a15ed174628" spendLimit: 50.00 lockCardNextUse: true cardNickname: "First card on a connected account" } ) { virtualCardId virtualCardLast4 status } } ``` An `ACTIVE` card back means the loop is closed: registered → connected → verified → operating. The customer becomes visible in your systems through the token; they never gained access to *your* account, and you hold only the scopes they granted. ## You're done 🎉 You've onboarded and connected a customer end to end. From here: > **[Build a platform](/build-a-platform)** > > The full platform architecture — token paths, capabilities, and patterns. > **[Approvals & requests](/features/approvals-and-requests)** > > Let connected users request actions that owners approve. > **[External reference IDs](/managing-external-reference-ids)** > > Address customers by your own IDs instead of Fluz account IDs. > **[Business registration & KYB](/business-registration)** > > Onboard businesses, not just individuals. > [!NOTE] > > **Want to learn more?** Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Send a card to someone Source: https://docs.fluz.app/quickstart/send-a-card Generate a hosted virtual card link, watch the recipient claim it, and manage the link lifecycle — end to end in the sandbox. This Quickstart walks through the complete Send Cards lifecycle. You'll create a staging application, mint a scoped Bearer token, choose a card program, retrieve a funded spend account, generate hosted virtual card links, experience the recipient activation flow, verify issuance, then deactivate an unused link. By the end you'll have successfully completed every public Send Cards operation: * `generateVCShareLinks` * `getVCShareLinks` * `deactivateVCShareLinks` —all in the sandbox, where no real customer funds move. > [!NOTE] > > **Prerequisites** > > * A **Fluz account** — you'll create staging API credentials and mint a scoped access token in Steps 1–2 below. > * Requests go to the **sandbox** GraphQL endpoint. Hosted recipient links use the standard Fluz activation experience, but all API operations use your staging environment. > * Your account must have **Send Cards enabled** (`send_virtual_cards_enabled`) or every Send Cards operation returns a permissions error. > * This Quickstart uses a funded **Spend Account** as the funding source. Cards are funded **when claimed**, not when links are generated. ## Before you begin Every request in this guide is sent to the Fluz GraphQL API. Minting your token (Step 2) authenticates with your API Key; every other request uses your Bearer access token. ```bash Endpoint & headers theme={null} POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql Authorization: Bearer Content-Type: application/json ``` ```bash Example request (cURL) theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query":"query { getVirtualCardOffers { offerId programName rewardValue } }" }' ``` > [!TIP] > > Every GraphQL operation below uses this endpoint and your Bearer token. The only exception is Step 2, which uses the same endpoint but authenticates with your API Key (`Authorization: Basic`) to mint the token. ## The full flow This is the only flow in Get started with two participants. You generate hosted links and never touch the card itself; the **recipient** claims a link in a browser, and that claim is what issues the card. Anything unclaimed stays a liability until you deactivate it. ```mermaid theme={null} sequenceDiagram participant Y as Your server participant F as Fluz API participant R as Recipient Y->>F: generateVCShareLinks (offerId, quantity, cardLimit) F->>Y: share requests + hosted URLs Y->>R: send a link (email, SMS, your own channel) R->>F: opens fluz.app/virtual-prepaid-card/{id} Note over R,F: sign in, 2FA, set PIN, claim F->>R: card issued and revealed Y->>F: getVCShareLinks (status) F->>Y: one CLAIMED, one still ACTIVE Y->>F: deactivateVCShareLinks (unclaimed) F->>Y: link retired ``` No funds are reserved when a link is generated — the card is funded from your spend account at the moment the recipient claims it. Deactivating an unclaimed link retires it before that can happen. **1. Create a staging application** Create a Fluz account, then open the Developer Console and create a **Staging** application. When your application is created you'll receive: * **API Key** * **User ID** * **Account ID** Copy all three values somewhere safe. If you haven't already created a staging application, see [Prepare your accounts](/get-started/prepare-accounts). **2. Exchange your credentials for a scoped Bearer token** Call `generateUserAccessToken` with your API Key to generate a short-lived user access token. This token authorizes every Send Cards operation that follows. ```bash Mint access token (cURL) theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "query":"mutation ($userId:UUID!,$accountId:UUID!,$scopes:[ScopeType!]!){generateUserAccessToken(userId:$userId,accountId:$accountId,scopes:$scopes){token scopes}}", "variables":{ "userId":"", "accountId":"", "scopes":[ "CREATE_VIRTUALCARD", "CREATE_SHARE_LINK", "LIST_PAYMENT" ] } }' ``` ```json Response theme={null} { "data": { "generateUserAccessToken": { "token": "eyJhbGciOi...", "scopes": [ "CREATE_VIRTUALCARD", "CREATE_SHARE_LINK", "LIST_PAYMENT" ] } } } ``` Save the returned `token`. Each scope maps to one part of this flow: `CREATE_VIRTUALCARD` to browse card offers, `CREATE_SHARE_LINK` to generate, list, and deactivate share links, and `LIST_PAYMENT` to read your spend accounts. You'll use it as: ```http theme={null} Authorization: Bearer ``` for every remaining request. > [!NOTE] > > `CREATE_VIRTUALCARD` authorizes every Send Cards operation. > > This Quickstart also requests `LIST_PAYMENT` so you can retrieve the Spend Account that funds your hosted cards. > [!WARNING] > > **Never expose your API Key inside a browser or mobile application.** > > Generate Bearer tokens server-side and forward only the access token to your client. **3. Retrieve a funded Spend Account** Hosted virtual cards are funded from one of your **Spend Accounts**. Unlike regular virtual cards, **no funds are reserved when the link is generated.** Instead, the selected Spend Account is charged only when the recipient successfully claims the card. Retrieve your available Spend Accounts. ```graphql Query theme={null} query GetSpendAccounts { getUserCashBalances { userCashBalances { userCashBalanceId nickname availableCashBalance isDefault } } } ``` ```json Response theme={null} { "data": { "getUserCashBalances": { "userCashBalances": [ { "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "nickname": "Primary Spend Account", "availableCashBalance": "5000.00", "isDefault": true } ] } } } ``` Save the `userCashBalanceId`. You'll supply this value when generating your hosted card links. > [!NOTE] > > The selected Spend Account must belong to your account and must contain sufficient funds **when the recipient claims the card**. If there isn't enough available balance at claim time, card issuance fails. **4. Choose the card program** Every hosted card is issued against a virtual card **offer**. The offer determines the issuing program, available rewards, and the limits that apply to every generated card. Retrieve the offers available to your account. ```graphql Query theme={null} query GetVirtualCardOffers { getVirtualCardOffers { offerId programName rewardValue programLimits { dailyLimit weeklyLimit monthlyLimit } } } ``` ```json Response theme={null} { "data": { "getVirtualCardOffers": [ { "offerId": "11111111-2222-3333-4444-555555555555", "programName": "Virtual Card", "rewardValue": "1.5%", "programLimits": { "dailyLimit": 500, "weeklyLimit": 2000, "monthlyLimit": 5000 } } ] } } ``` Save the `offerId` for the program you'd like to issue against. > [!NOTE] > > **The selected offer must be:** > > * active > * share-enabled > > If generation fails because the offer isn't eligible for Send Cards, confirm with your Fluz representative which offers are enabled for hosted sharing. **5. Generate two hosted card links** You're ready to create hosted Send Card links. Each generated link represents **one future virtual card**. At this stage: * No card has been issued. * No funds have been withdrawn. * Each link begins in the `PENDING` state. The selected Spend Account is charged **only when a recipient claims a link**. For this Quickstart you'll generate **two** links: * **Link A** — you'll claim this as the recipient. * **Link B** — you'll leave unclaimed so it can later be revoked. ```graphql Mutation theme={null} mutation GenerateVCShareLinks($input: GenerateVCShareLinksInput!) { generateVCShareLinks(input: $input) { shareLinks } } ``` ```json Variables theme={null} { "input": { "cardLimit": 25, "offerId": "", "quantity": 2, "daysUntilExpiration": 14, "shareMethod": "GENERATE_URL", "userCashBalanceId": "" } } ``` ```json Response theme={null} { "data": { "generateVCShareLinks": { "shareLinks": [ "https://fluz.app/virtual-prepaid-card/3f8ac...c1", "https://fluz.app/virtual-prepaid-card/9b2dd...77" ] } } } ``` The response returns one hosted activation URL for every generated card. Save both URLs. They're referred to below as: * **Link A** * **Link B** throughout the remainder of this Quickstart. ### Understanding the generation settings **cardLimit** `Int` *required* The amount loaded onto each hosted card when it's claimed. Each generated card receives its own independent limit. **offerId** `UUID` *required* The hosted virtual card program to issue against. The offer must be active and enabled for Send Cards. **quantity** `Int` *required* The number of hosted links to generate. One share request and one hosted URL are created for every requested quantity. **shareMethod** `ShareMethodType` *required* Controls how recipients receive their links. * `GENERATE_URL` returns hosted URLs for you to distribute. * `EMAIL` emails recipients automatically. * `PHONE_NUMBER` sends recipients an SMS. **userCashBalanceId** `UUID` *required* The Spend Account that funds every generated card. Although this field appears optional in the GraphQL schema, it is required in practice. **daysUntilExpiration** `Int` How long recipients have to claim the hosted link. If omitted, the program default is used. The resulting expiration date also becomes the issued card's lock date. > **Deliver cards by email or SMS instead** > > Instead of distributing hosted URLs yourself, Fluz can send every recipient their own activation link. > > ### Email > > ```json theme={null} > { > "input": { > "cardLimit": 25, > "offerId": "", > "quantity": 2, > "shareMethod": "EMAIL", > "recipientListEmail": [ > "alice@example.com", > "bob@example.com" > ], > "userCashBalanceId": "" > } > } > ``` > > ### SMS > > ```json theme={null} > { > "input": { > "cardLimit": 25, > "offerId": "", > "quantity": 2, > "shareMethod": "PHONE_NUMBER", > "recipientListPhone": [ > "+18883606660", > "+18885551234" > ], > "userCashBalanceId": "" > } > } > ``` > > Recipient list length **must exactly equal** `quantity`. > > If they don't match, validation fails and **no links are created**. > > Phone numbers must: > > * include the country code > * contain no spaces > * be supplied as strings > > Example: > > ```text theme={null} > +18883606660 > ``` > [!WARNING] > > **Generating hosted links does not reserve funds.** > > If the selected Spend Account doesn't contain sufficient available balance when the recipient claims the card, card issuance fails. > [!NOTE] > > **The generation response intentionally returns only the hosted URLs.** > > It does **not** return: > > * `shareRequestBatchId` > * `shareRequestDisplayId` > > You'll retrieve those in the next step. **6. Retrieve the generated share requests** Although you already have the hosted URLs, you'll usually also want the underlying share request records. These contain: * lifecycle status * batch identifiers * display IDs * issued virtual card IDs * expiration information Immediately after generation, retrieve your pending share requests. ```graphql Query theme={null} query GetVCShareLinks($input: GetVCShareLinksInput!) { getVCShareLinks(input: $input) { shareRequestBatchId shareRequestDisplayId shareObjectStatus linkExpirationDate virtualCardId linkUrl } } ``` ```json Variables theme={null} { "input": { "shareObjectStatuses": [ "PENDING" ] } } ``` ```json Response theme={null} { "data": { "getVCShareLinks": [ { "shareRequestBatchId": "BATCH123", "shareRequestDisplayId": "SR-000001", "shareObjectStatus": "PENDING", "virtualCardId": null, "linkExpirationDate": "2026-08-01T00:00:00Z", "linkUrl": "https://fluz.app/virtual-prepaid-card/3f8ac...c1" }, { "shareRequestBatchId": "BATCH123", "shareRequestDisplayId": "SR-000002", "shareObjectStatus": "PENDING", "virtualCardId": null, "linkExpirationDate": "2026-08-01T00:00:00Z", "linkUrl": "https://fluz.app/virtual-prepaid-card/9b2dd...77" } ] } } ``` Match each returned `linkUrl` with the URLs returned during generation. Save: * the shared `shareRequestBatchId` * both `shareRequestDisplayId` values You'll use them later when deactivating the unused link. At this point both links should have: ```text theme={null} Status: PENDING Virtual Card ID: null ``` which confirms that: * the links were created successfully * no recipient has claimed them yet * no virtual cards have been issued **7. Claim one link as the recipient** Open **Link A** in your browser. This is exactly the experience your recipient sees. The hosted flow guides recipients through card activation without requiring any API integration. During activation the recipient: 1. Opens the hosted activation page. 2. Signs in or creates a Fluz account. 3. Completes identity verification. 4. Completes two-factor authentication. 5. Adds a billing address if one isn't already on file. 6. Creates a card PIN. 7. Receives their hosted virtual card. Only after the final step does Fluz: * issue the virtual card * fund it from your Spend Account * assign it to the recipient The recipient becomes the authorized holder of **that card only**. They do **not** gain access to: * your Fluz account * your Spend Accounts * your balances * any other generated cards Once issued, the recipient can: * view the card * spend online * add it to Apple Pay or Google Wallet (where supported) * view future transactions > [!NOTE] > > **Leave Link B untouched.** > > You'll use it to demonstrate link revocation in the next section. **8. Verify the card was issued** After the recipient claims **Link A**, retrieve the share requests again. This time you'll see the claimed link transition from `PENDING` to `ISSUED`. ```graphql Query theme={null} query GetVCShareLinks($input: GetVCShareLinksInput!) { getVCShareLinks(input: $input) { shareRequestDisplayId shareObjectStatus virtualCardId linkUrl } } ``` ```json Variables theme={null} { "input": { "shareRequestBatchIds": [ "BATCH123" ] } } ``` ```json Response theme={null} { "data": { "getVCShareLinks": [ { "shareRequestDisplayId": "SR-000001", "shareObjectStatus": "ISSUED", "virtualCardId": "c8dbe6d8-2f2f-4c90-8d7e-15ef5b06e387", "linkUrl": "https://fluz.app/virtual-prepaid-card/3f8ac...c1" }, { "shareRequestDisplayId": "SR-000002", "shareObjectStatus": "PENDING", "virtualCardId": null, "linkUrl": "https://fluz.app/virtual-prepaid-card/9b2dd...77" } ] } } ``` Your two links should now be in different lifecycle states. | Link | Expected status | Meaning | | - | - | - | | **Link A** | `ISSUED` | The recipient successfully claimed the card and a virtual card has been created. | | **Link B** | `PENDING` | The link has not yet been claimed. | The appearance of a `virtualCardId` confirms that card issuance has completed successfully. > **Understanding Send Card statuses** > > Every hosted Send Card progresses through one of four lifecycle states. > > | Status | Meaning | > | - | - | > | `PENDING` | The hosted link has been generated but hasn't yet been claimed. | > | `ISSUED` | The recipient successfully claimed the link and received a virtual card. | > | `USED` | The issued card has completed at least one transaction. | > | `EXPIRED` | The hosted link expired naturally or was deactivated before being claimed. | > > Only links in the `PENDING` state can be deactivated. > > Once a card reaches `ISSUED`, deactivating the original link does **not** revoke the card. **9. Deactivate the unused link** Suppose the second recipient never needed their card, or a batch was generated accidentally. You can revoke any **unclaimed** hosted link using either: * `shareRequestBatchIds` * `shareRequestDisplayIds` For this Quickstart you'll deactivate only **Link B** using its display ID. ```graphql Mutation theme={null} mutation DeactivateVCShareLinks($input: DeactivateVCShareLinksInput!) { deactivateVCShareLinks(input: $input) } ``` ```json Variables theme={null} { "input": { "shareRequestDisplayIds": [ "SR-000002" ] } } ``` ```json Response theme={null} { "data": { "deactivateVCShareLinks": "1 share requests successfully deactivated!" } } ``` Deactivation immediately prevents the hosted link from ever being claimed. If a recipient later opens the link they'll receive an expired or revoked experience instead of the activation flow. > [!WARNING] > > Deactivation only affects **unclaimed** links. > > If a recipient has already claimed a card (`ISSUED` or `USED`), the hosted link can no longer be used to revoke it. > > To stop spend on an issued card, use the appropriate virtual card lifecycle controls such as [Lock Virtual Card](/features/lock-virtual-card). **10. Confirm the final lifecycle** Retrieve the share requests one final time. ```graphql Query theme={null} query GetVCShareLinks($input: GetVCShareLinksInput!) { getVCShareLinks(input: $input) { shareRequestDisplayId shareObjectStatus virtualCardId } } ``` ```json Variables theme={null} { "input": { "shareRequestBatchIds": [ "BATCH123" ] } } ``` ```json Response theme={null} { "data": { "getVCShareLinks": [ { "shareRequestDisplayId": "SR-000001", "shareObjectStatus": "ISSUED", "virtualCardId": "c8dbe6d8-2f2f-4c90-8d7e-15ef5b06e387" }, { "shareRequestDisplayId": "SR-000002", "shareObjectStatus": "EXPIRED", "virtualCardId": null } ] } } ``` You've now exercised the complete Send Cards lifecycle. | Link | Final status | Result | | - | - | - | | **Link A** | `ISSUED` | Successfully claimed and issued to a recipient. | | **Link B** | `EXPIRED` | Successfully revoked before it could be claimed. | At this point you've successfully used all three public Send Cards operations: * ✅ `generateVCShareLinks` * ✅ `getVCShareLinks` * ✅ `deactivateVCShareLinks` ## You're done 🎉 You've completed the full hosted Send Cards lifecycle—from generation through recipient activation, lifecycle tracking, and link revocation. Along the way you: * Generated hosted virtual card links. * Selected the Spend Account that funds recipient cards. * Retrieved and tracked share request records. * Claimed a hosted card as the recipient. * Verified the transition from `PENDING` to `ISSUED`. * Revoked an unused hosted link. * Confirmed the final `EXPIRED` state. From here you can explore more advanced Send Cards workflows. > **[Send Cards overview](/features/open-loop-cards/send-open-loop-cards)** > > Recipient experience, program rules, lifecycle states, and complete error reference. > **[Generate Share Links](/features/open-loop-cards/send-open-loop-cards)** > > The detailed API reference for every Send Cards operation. > **[Get Spend Accounts](/features/get-spend-accounts)** > > Retrieve and manage the Spend Accounts used to fund hosted cards. > **[Manage Virtual Cards](/features/lock-virtual-card)** > > Lock and manage cards after they've been issued to recipients. > [!NOTE] > > **Want to learn more?** > > Contact us at [support@fluz.app](mailto:support@fluz.app) to speak with our experts or request a demo. # Set virtual card PIN Source: https://docs.fluz.app/set-virtual-card-pin The `setVirtualCardPIN` mutation allows you to set a PIN on eligible virtual cards that have not had a PIN set on them yet. When creating virtual cards through the Fluz API, your cards are created without setting the PIN. Most of the time, you will not need one. However, if you'd like to set one you can use this mutation to automatically identify and update all cards that support a PIN, but haven't had their PIN set yet. > [!NOTE] > > **Prerequisites:** a user access token with the `CREATE_VIRTUALCARD` scope. The PIN you set must match your user PIN. > [!NOTE] > > This mutation adds the eligible cards to a queue for processing. The actual updating of the PINs can take several minutes — the response indicates whether the enqueuing was successful, not whether every card has been updated yet. ## Arguments * **`input`** (`SetVirtualCardPINInput!`): The input object that contains the PIN to be set on the cards. It must match your user PIN. ## Sample mutation ```graphql theme={null} mutation { setVirtualCardPIN(input: { pin: "1234" }) { success pinError } } ``` ## cURL example ```curl theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation { setVirtualCardPIN(input: { pin: \"1234\" }) { success pinError } }" }' ``` ## Sample response ```json theme={null} { "data": { "setVirtualCardPIN": { "success": true, "pinError": null } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `success` | `Boolean` | Indicates whether the request to set PINs was successfully queued for processing. | | `pinError` | `String` | Text that will alert you if there was an error verifying your PIN. | ## Code example **Set Virtual Card PIN** ## Next steps > **[Digital wallet push provisioning](/digital-wallet-push-provisioning)** > > Push a virtual card into Apple Pay or Google Wallet for tap-to-pay. # Design a solution Source: https://docs.fluz.app/solution-designer Describe what you want to build and get a tailored Fluz solution design. [Fluz Solution Designer](https://designer.fluz.app/?embed=1) # Test adding bank accounts Source: https://docs.fluz.app/test-adding-bank-accounts Use these test account and routing numbers to simulate ACH / bank-account funding sources in the **staging** environment. Each pair returns a fixed status so you can verify how your integration handles both successful debits and the full range of failures. > [!NOTE] > > **Staging only.** These values work only in the staging environment and will be rejected in production. > [!WARNING] > > **You do not pass these to the GraphQL API.** No Fluz mutation accepts a routing or account number. Bank accounts are attached through Plaid: call `createPlaidLinkToken`, hand the token to Plaid Link, and complete the connection with `completePlaidLink`, which takes a `publicToken` — never the account details themselves. > > Enter the values below in the Plaid Link flow. See [Link external bank accounts](/features/link-external-bank-accounts) for the full sequence. ## Test accounts | Account Number | Routing Number | Status Code | | - | - | - | | 86063057 | 726286823 | SUCCESS | | 1234567890 | 011401533 | SUCCESS | | 86063046 | 726286823 | INSUFFICIENT\_FUNDS | | 41345938 | 726286823 | INVALID\_ACCOUNT | | 55073835 | 726286823 | UNABLE\_TO\_PROCESS | | 46319531 | 726286823 | UNAUTHORIZED\_DEBIT | | 99081536 | 726286823 | ACCOUNT\_CLOSED | ## Status codes | Status Code | What it simulates | | - | - | | SUCCESS | The account validates and the transaction completes. | | INSUFFICIENT\_FUNDS | The account is valid but lacks the funds to cover the debit. | | INVALID\_ACCOUNT | The account or routing number is not recognized. | | UNABLE\_TO\_PROCESS | A general processing failure that is not specific to the account. | | UNAUTHORIZED\_DEBIT | The debit is rejected as unauthorized by the account holder. | | ACCOUNT\_CLOSED | The account exists but has been closed. | ## Next steps > **[Testing KYC flows](/test-kyc-flows)** > > Test identities that return each verification outcome — approved, declined, and duplicate. > **[Deposit from external accounts](/features/deposit-from-external-accounts)** > > Put these test accounts to work by simulating a deposit end to end. # Test adding bank cards Source: https://docs.fluz.app/test-adding-bank-cards Use these test card numbers to simulate card funding sources and card-based deposits in the **staging** environment. They let you verify your add-card, deposit, and decline-handling logic without a real card. > [!NOTE] > > **Staging only** > > These numbers are accepted only on the staging endpoint (`https://transactional-graph.staging.fluzapp.com/api/v1/graphql`). They will be rejected in production. See [Add Funding Sources](/features/add-bank-card) for how to attach a card. > [!NOTE] > > **How to use these values:** pass the number to `addBankCard` as `cardNumber`. CVV — any 3 digits (any 4 for American Express). Expiration date — any date in the future. Cardholder name — any non-empty value. > [!WARNING] > > **A billing address is required.** `addBankCard` accepts either a `billingAddress` object or a saved `userAddressId`. On an account with no address on file, omitting both fails. Use a value from [Test Addresses](/test-addresses). > > **The same card can only be added once.** Re-adding a number already on the account fails with a generic decline rather than a duplicate error, which is easy to misread as the test number being invalid. Check [`getWallet`](/features/view-funding-sources) before retrying, and remove the existing card with `deleteBankCard` if you want to add it again. ## Successful Transactions The cards below simulate successful card transactions. | Card Type | Card Number | CVV Code | Exp. Date | | - | - | - | - | | American Express | 378282246310005 | Any 4 digits | Any future date | | American Express | 371449635398431 | Any 4 digits | Any future date | | BCcard and DinaCard | 6555900000604105 | Any 3 digits | Any future date | | Diners Club | 3056930009020004 | Any 3 digits | Any future date | | Diners Club (14-digit card) | 36227206271667 | Any 3 digits | Any future date | | Discover | 6011111111111117 | Any 3 digits | Any future date | | Discover | 6011000990139424 | Any 3 digits | Any future date | | Discover (debit) | 6011981111111113 | Any 3 digits | Any future date | | JCB | 3566002020360505 | Any 3 digits | Any future date | | Mastercard | 5555555555554444 | Any 3 digits | Any future date | | Mastercard (2-series) | 2223003122003222 | Any 3 digits | Any future date | | Mastercard (debit) | 5200828282828210 | Any 3 digits | Any future date | | Mastercard (prepaid) | 5105105105105100 | Any 3 digits | Any future date | | UnionPay | 6200000000000005 | Any 3 digits | Any future date | | UnionPay (19-digit card) | 6205500000000000004 | Any 3 digits | Any future date | | UnionPay (debit) | 6200000000000047 | Any 3 digits | Any future date | | Visa | 4242424242424242 | Any 3 digits | Any future date | | Visa (debit) | 4000056655665556 | Any 3 digits | Any future date | ## Declined Transactions The cards below simulate declined transactions. Each returns the listed error code so you can test how your integration surfaces failures. See [Decline Codes](/features/decline-codes) for the full reference. | Description | Card Number | Error Code | | - | - | - | | Exceeding velocity limit decline | 4000000000006975 | card\_declined | | Expired card decline | 4000000000000069 | expired\_card | | Generic decline | 4000000000000002 | card\_declined | | Incorrect CVC decline | 4000000000000127 | incorrect\_cvc | | Incorrect number decline | 4242424242424241 | incorrect\_number | | Insufficient funds decline | 4000000000009995 | card\_declined | | Lost card decline | 4000000000009987 | card\_declined | | Processing error decline | 4000000000000119 | processing\_error | | Stolen card decline | 4000000000009979 | card\_declined | ## Next steps > **[Test bank accounts](/test-adding-bank-accounts)** > > Simulate ACH and bank-account funding sources — the other half of testing deposits. > **[Decline codes](/features/decline-codes)** > > The full reference for every decline reason your integration should handle. **Want to learn more?** Contact us at [partnerships@fluz.app](mailto:partnerships@fluz.app). Speak with our experts for more info or to request a demo. # Test addresses Source: https://docs.fluz.app/test-addresses Use these addresses to exercise the address steps of your integration in staging — virtual card issuance, bank card billing, KYC, and KYB — including your `VC-0025` error handling. Virtual card billing addresses are validated with [Smarty](https://www.smarty.com/) (USPS Delivery Point Validation). See [Address Formatting Requirements](/concepts/address-formatting-requirements) for the full rules, including which contexts are US-only. > [!NOTE] > > **Staging mirrors production** > > Address validation behaves the same in staging as in production, so any real, currently-deliverable address will pass. The examples below are provided for convenience and to trigger specific outcomes. ## US test addresses Each of these is a real, USPS-deliverable US address that Smarty validates cleanly. A real, deliverable **US** address works for **every** address field in the API — virtual cards, bank accounts, bank cards, KYC, and KYB. | `streetAddressLine1` | `city` | `state` | `postalCode` | | - | - | - | - | | 1600 Amphitheatre Pkwy | Mountain View | CA | 94043 | | 5732 Lincoln Dr | Minneapolis | MN | 55436 | | 8084 Hogwarts Dr | Rives Junction | MI | 49277 | To test a secondary unit, add a valid apartment or suite to `streetAddressLine2` (for example, `Apt 5` or `Ste 200`). ### Example request ```json theme={null} { "input": { "billingAddress": { "streetAddressLine1": "1600 Amphitheatre Pkwy", "country": "United States", "city": "Mountain View", "state": "CA", "postalCode": "94043" } } } ``` ## International test addresses These are real, deliverable non-US addresses for testing contexts that accept international addresses. > [!NOTE] > > **Where international addresses are accepted** > > International addresses work for **bank card billing addresses**, **KYC**, and **KYB**. They are **not** accepted for **bank account (ACH) addresses** or **virtual card billing addresses** — both are US-only. Submitting an international address to a virtual card returns `VC-0025`. | `streetAddressLine1` | `city` | `state` | `postalCode` | `country` | | - | - | - | - | - | | 221B Baker Street | London | England | NW1 6XE | United Kingdom | | 100 Queen Street West | Toronto | ON | M5H 2N1 | Canada | | 200 George Street | Sydney | NSW | 2000 | Australia | | Unter den Linden 77 | Berlin | Berlin | 10117 | Germany | Some countries do not use a state or province. Because `state` is required on most address objects, provide the closest administrative region (for example, the province, county, or city region). ## Addresses that fail validation Use these to confirm your app handles `VC-0025` (and other rejections) gracefully. None of them will save a virtual card address. | Address | Result | Why | | - | - | - | | 2581 Oakwood Avenue, New York, NY 10605 | `VC-0025` | City / ZIP mismatch — `10605` is White Plains, NY, not New York City — and no matching delivery point. | | 896 S State St, Dover, DE 19904 | `VC-0025` | Building number `896` is not a confirmed USPS delivery point on that street and ZIP. | | 123 Fake St, Springfield, IL 62704 | `VC-0025` | Street is not present in USPS data. | | PO Box 1001, Austin, TX 78701 | Rejected | PO boxes are not accepted by the card issuer. | | 221B Baker Street, London NW1 6XE, United Kingdom | `VC-0025` | Non-US address — virtual card and bank account addresses are US-only. (Valid for bank card billing, KYC, and KYB — see the International test addresses above.) | ### Example failing request ```json theme={null} { "input": { "billingAddress": { "streetAddressLine1": "2581 Oakwood Avenue", "country": "United States", "city": "New York", "state": "NY", "postalCode": "10605" } } } ``` Returns `VC-0025`. Retrying with the same values will keep failing — the address must be corrected before it can be saved. ## Next steps You've now got test data for every part of the sandbox — cards, bank accounts, identities, merchants, and addresses. Time to put it to work. > **[Quickstart: your first gift card purchase](/quickstart/first-gift-card)** > > Run the full happy path end to end — deposit funds, buy a gift card, and reveal it. > **[Address formatting requirements](/concepts/address-formatting-requirements)** > > The full validation rules behind these outcomes, with worked examples and error handling. # Test issuing virtual cards Source: https://docs.fluz.app/test-issuing-virtual-cards Ready-to-use offer IDs for issuing virtual cards in the staging environment, plus how to discover offers dynamically. Use these offer IDs to issue virtual cards in the **staging** environment. Every virtual card is created against an *offer*, which determines the card program (network, BIN, and rewards). The offer IDs below are enabled for staging and can be passed straight to `createVirtualCard`. > [!WARNING] > > **Complete KYC first** > > Virtual cards can only be issued on an account that has completed identity verification. Until the account — and, for a business, its beneficial owners — passes KYC/KYB, `getVirtualCardOffers` may return empty and `createVirtualCard` will fail. Run verification before you test. See [User KYC Verification](/user-kyc-verification) for how to submit it, and pull test identities from [Testing KYC Flows](/test-kyc-flows). > [!NOTE] > > **Staging only** > > These offer IDs are accepted only on the staging endpoint (`https://transactional-graph.staging.fluzapp.com/api/v1/graphql`). They will be rejected in production, where you must retrieve a live offer ID from [Get Virtual Card Offers](/features/get-card-offers). ## Staging offer IDs | Offer ID | Program | Issuer | Network | | - | - | - | - | | `387a57ce-5fc9-400f-a610-1991d5748ea5` | Virtual Card - Mastercard Prepaid | TransPecos Banks | Mastercard | | `bc7f66ab-199c-4a39-bba8-defde1348d73` | Brand Locked Virtual Card - Mastercard Prepaid | TransPecos Banks | Mastercard | | `41b8c025-2b3c-4ede-81f7-efd34c170b76` | Single Load - Virtual Card - Mastercard Prepaid | — | Mastercard | | `490032ad-2e67-4ef0-92ff-cd7071332716` | Reloadable Virtual Card - Mastercard Prepaid | — | Mastercard | > [!NOTE] > > **Offer IDs change.** Staging card programs are added and retired over time, so treat the table above as a starting point rather than a fixed list. If an ID is rejected, query the current set instead: > > ```bash theme={null} > curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ > -H "Authorization: Bearer " \ > -H "Content-Type: application/json" \ > -d '{"query":"query { getVirtualCardOffers { offerId programName bankName } }"}' > ``` > > Staging currently exposes more than 40 offers, including Visa and debit programs not listed here. ## Which offer should you choose? Each program issues a Mastercard prepaid card, but they differ in where the card can be spent and how it's funded. Pick the one that matches the behavior you want to test: > **Virtual Card** > > The general-purpose program — a standard multi-use prepaid card that can be spent anywhere Mastercard is accepted, within the spend limits you set. Start here if you just want to run the happy path end to end. > **Brand Locked Virtual Card** > > A card restricted to a single merchant or brand. Transactions at any other merchant are declined. Use this to test merchant-restricted spend controls — for example, a card that only works at one vendor. > **Single Load Virtual Card** > > A card funded once, at creation. The balance is loaded up front and spent down — it can't be topped up afterward. Use this to test fixed-value, disposable card flows like one-off vendor payments or payouts. > **Reloadable Virtual Card** > > A card whose balance can be topped up after creation. Use this to test long-lived cards that get refunded repeatedly — recurring allowances, team cards, or ongoing subscription spend. ## Recommended flow Rather than hard-coding an offer ID, discover offers at runtime — the returned list reflects exactly what your account is entitled to. It's a quick two-call sequence. **1. List available offers** Call `getVirtualCardOffers` and grab an `offer_id` from the response. **2. Create the card** Pass that `offer_id` to `createVirtualCard`. A successful response returns a `virtual_card_id` with a status of `ACTIVE`. ```graphql Step 1 — Get offers theme={null} query { getVirtualCardOffers(input: {}) { offerId programName bankName rewardValue } } ``` ```graphql Step 2 — Create a card theme={null} mutation { createVirtualCard(input: { offerId: "387a57ce-5fc9-400f-a610-1991d5748ea5" spendLimit: 5.00 idempotencyKey: "3f1a9c72-5b84-4e2d-9a06-1c7d8e4b2f50" billingAddress: { streetAddressLine1: "1600 Amphitheatre Pkwy" city: "Mountain View" state: "CA" postalCode: "94043" country: "United States" } }) { virtualCardId status initialAmount } } ``` > [!TIP] > > Keep `spendLimit` at or below your available balance — the card is funded from your Fluz balance, so a higher limit fails with an insufficient-balance error. A \$5 test is the safest first attempt. > > **A billing address is required.** If your account has none on file, `createVirtualCard` fails with *"The address on your account couldn't be verified"* — pass a `billingAddress` in the input, or a saved `userAddressId`. It must be a real, deliverable US address; a value from [Test Addresses](/test-addresses) works in staging. See [Add Virtual Card Address](/features/add-billing-address). ## Next steps > **[Create Virtual Card](/features/create-virtual-card)** > > The full `createVirtualCard` reference — inputs, funding, and the card lifecycle. > **[Testing KYC flows](/test-kyc-flows)** > > Test identities that return each verification outcome — approved, declined, and duplicate. **Want to learn more?** Contact us at [partnerships@fluz.app](mailto:partnerships@fluz.app). Speak with our experts for more info or to request a demo. # Test KYC flows Source: https://docs.fluz.app/test-kyc-flows Run identity verification against staging test identities, and learn which outcomes count as verified. Fluz requires users to be KYC verified before they can perform certain transactions or unlock higher limits. In the **staging** environment you can run the entire verification flow against known test identities. That lets you confirm your integration handles every outcome without using a real person's information. For the full mutation contract and response reference, see [User KYC Verification](/user-kyc-verification). > [!NOTE] > > **Staging only** > > The identities on this page exist only on the staging endpoint (`https://transactional-graph.staging.fluzapp.com/api/v1/graphql`). They are synthetic and will not verify in production. Never submit a real person's name, address, date of birth, or SSN to staging. > [!NOTE] > > **Before you start, you'll need:** > > * an **OAuth Integration** application that Fluz has moved to `ACTIVE`, with the `VERIFY_KYC` scope enabled (see [Prepare Your Accounts](/get-started/prepare-accounts)); > * a test user who has **completed the OAuth grant** for that application (see [Client-Facing OAuth Grant Flow](/client-facing-o-auth-grant-flow)); > * a **user access token for that user** (see [Generate a User Access Token](/recipes/generate-user-access-token)). > > The token decides which staging user the verification applies to. See [How accounts work](/get-started/accounts-and-applications). ## Step 1 — Create and connect a test user Verification applies to one user, and you can only verify a user your application is authorized to act for. 1. Create a fresh staging user with the [`registerUser`](/user-registration) mutation. Leave out `deferSeatAssignment`. 2. Send that user through the [OAuth grant flow](/client-facing-o-auth-grant-flow). In staging, no SMS is sent: enter `123456` as the sign-in code. 3. Generate a user access token for the user with the `VERIFY_KYC` scope. > [!WARNING] > > Registering a user doesn't authorize your application. If you skip step 2, `generateUserAccessToken` and `getAccountsByUserId` fail with `AUTH-0031`. > [!NOTE] > > **Restricted access** > > `registerUser` must be enabled on your application by Fluz. If it isn't, calls fail with `AUTH-0022`. If the application isn't active yet, calls fail with `AUTH-0030 … current status: PERSONAL`. Contact your account manager or [partnerships@fluz.app](mailto:partnerships@fluz.app) for either. ## Step 2 — Use the approved test identity The identity below returns an **APPROVED** result on staging **the first time it is used**. Submit it exactly as shown. | Field | Value | | - | - | | First name | John | | Last name | Smith | | Street address | 222333 Peachtree Place | | City | Atlanta | | State | GA | | ZIP / postal code | 30318 | | Date of birth | 02/28/1975 | | SSN | 112-22-3333 | > [!NOTE] > > The API only consumes the **last 4 digits** of the SSN (`ssnLast4`). For this identity that's `3333`. Date of birth is formatted as `MM/DD/YYYY`. > [!WARNING] > > **One approved identity verifies one user.** Submitting the same identity for a second user returns `DUPLICATE`, and that second user is **not** verified. To verify more test users with the same identity, see [Verifying more than one test user](#verifying-more-than-one-test-user). ## Step 3 — Run the verification Call `verifyUserInformation` with the approved identity. Replace `` with the token for the user being verified. The token must carry the **`VERIFY_KYC`** scope. It isn't self-serve: Fluz has to enable it on your application first. Without it, the call fails with `AUTH-0031`. ```curl cURL theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation VerifyUserInformation { verifyUserInformation(firstName: \"John\", lastName: \"Smith\", streetLine1: \"222333 Peachtree Place\", streetLine2: \"\", city: \"Atlanta\", state: \"GA\", postalCode: \"30318\", country: \"United States\", dateOfBirth: \"02/28/1975\", ssnLast4: \"3333\") { status message } }" }' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://transactional-graph.staging.fluzapp.com/api/v1/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query: ` mutation VerifyUserInformation( $firstName: String! $lastName: String! $streetLine1: String! $streetLine2: String $city: String! $state: String! $postalCode: String! $country: String! $dateOfBirth: String! $ssnLast4: String! ) { verifyUserInformation( firstName: $firstName lastName: $lastName streetLine1: $streetLine1 streetLine2: $streetLine2 city: $city state: $state postalCode: $postalCode country: $country dateOfBirth: $dateOfBirth ssnLast4: $ssnLast4 ) { status message } } `, variables: { firstName: "John", lastName: "Smith", streetLine1: "222333 Peachtree Place", streetLine2: "", city: "Atlanta", state: "GA", postalCode: "30318", country: "United States", dateOfBirth: "02/28/1975", ssnLast4: "3333", }, }), } ); const data = await response.json(); console.log(data.data.verifyUserInformation); ``` ```json Approved response theme={null} { "status": "APPROVED", "message": "User verification successful" } ``` There is no query that returns a user's KYC status. Take the result from this response, and from the `KYC_STATUS_UPDATE` webhook, which needs `VERIFY_KYC` on both your application and the user's grant. ## Step 4 — Understand each outcome Only `APPROVED` makes a user verified. Card issuing, and acting as a `registerBusiness` applicant, both require a successful verification on file. | Status | Message | Counts as verified? | How it happens on staging | | - | - | - | - | | `APPROVED` | User verification successful | **Yes** | The approved test identity, on a user that has never used it | | `DECLINED` | Verification declined | No | Any identity that doesn't match the approved one — for example, a different SSN last 4, date of birth, or name | | `DUPLICATE` | Duplicate user verification | **No** | The identity already verified a different user | | `ERROR` | Exceeded user verification limit | No | A 4th verification attempt for the same user | | `ERROR` | User is already KYC verified | Already verified | The user was verified earlier; nothing more to do | > [!NOTE] > > **Applicants for a US business need SSN verification.** If this user will register a business as an owner with `isUsPerson: true`, they must pass the SSN path above. A user verified another way shows up on the business as an owner stuck at `PENDING_CIP` with `verificationType: SSN`. See [KYB Testing](/KYB-testing). ## Attempts, retries, and resets * **Three attempts per user.** A user can be submitted at most 3 times; the next submission returns `ERROR`. Plan your tests so you don't use up attempts on a user you still need. * **"Already verified" isn't an attempt.** An `ERROR` with `User is already KYC verified` doesn't count toward the 3. Treat it as success in your integration, not a failure. * **`forceRetry: true`** on `verifyUserInformation` re-runs verification for a user and bypasses the duplicate check. * **Resets.** There is no API to reset a staging user's attempts. Register a fresh test user, or ask your Fluz contact to reset one. * **Tokens are per user.** The access token decides which user is verified. Make sure it belongs to the test user whose identity you're submitting. ### Verifying more than one test user The approved identity verifies one user per normal submission. To get another test user to `APPROVED` with the same identity, submit it for that user with `forceRetry: true`, which bypasses the duplicate check. Make sure the user isn't already verified first. ## Next steps > **[KYB testing](/KYB-testing)** > > Test tax IDs and owner verification for business registration. > **[User KYC verification](/user-kyc-verification)** > > The full mutation contract: document verification links, statuses, and responses. # Test gift card purchasing Source: https://docs.fluz.app/test-merchants The staging environment is preloaded with merchants chosen to exercise every offer shape your integration may encounter — fixed- and variable-value gift cards, boosted rates, vouchers, card-linked offers (CLOs), out-of-stock states, and group-exclusive rates. Use them to confirm your catalog, offer, and purchase flows handle each case correctly. > [!NOTE] > > **Staging only** > > These merchants and their offer configurations exist only in the staging environment. Catalogs and offers in production differ — always pull the live catalog before testing there. See [Get Merchant Catalog](/get-catalog). ## How to use these merchants **1. Resolve the merchant and its offers from the catalog** Use [Get Merchant Catalog](/get-catalog), [Get Gift Card Offers](/get-gift-card-offers), or [Get Virtual Card Offers](/features/get-card-offers). **2. Pick the merchant that matches the case you want to test** The tables below map each offer shape to a staging merchant. **3. Run your purchase or card-creation flow** See [Purchase Gift Cards](/purchase-gift-card) or [Create Virtual Card](/features/create-virtual-card). To test out-of-stock handling, see [Get Inventory On Stocked Offers](/get-inventory). **Legend** — ✅ verified against the staging API on 2026-08-06 · ⏳ no merchant in staging currently demonstrates this case; the row is kept so the gap is visible, but there is nothing to test against yet. > [!NOTE] > > Merchants change. Every ✅ row below was confirmed by querying `getMerchants` directly, but staging catalog data is rebuilt periodically — an earlier version of this page listed 24 merchants, of which 15 no longer existed. If a merchant here returns an empty array, list the current catalog rather than assuming your call is wrong: > > ```bash theme={null} > curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ > -H "Authorization: Bearer " \ > -H "Content-Type: application/json" \ > -d '{"query":"query { getMerchants(offerTypes: { giftCardOffer: true, cardLinkedOffer: true }) { name slug } }"}' > ``` ## Gift card offers | Test case | Merchant | Slug | Staging | | - | - | - | - | | Fixed value | Amazon | `amazon` | ✅ | | Fixed value, with boosted rate | TacoTime | `tacotime` | ✅ | | Fixed value, with voucher | Burger King | `burger-king` | ✅ | | Fixed value, with boosted rate **and** voucher | Chuck E. Cheese's | `chuck-e-cheeses` | ✅ | | Variable value | Shake Shack | `shake-shack` | ✅ | | Variable value, with boosted rate | Lowe's | `lowes` | ✅ | | Variable value, whole dollars only | Sephora | `sephora` | ✅ | | Variable and fixed value | Belk | `belk` | ✅ | | Variable value, with voucher | — | — | ⏳ | | Variable and fixed value, different rates per fixed denomination | — | — | ⏳ | | Variable and fixed value, different rates per denomination, with voucher | — | — | ⏳ | | Redeemable with balance only | — | — | ⏳ | | Redeemable at more than one store | — | — | ⏳ | | Delivered as a QR code | — | — | ⏳ | > [!NOTE] > > **Delivery formats currently in staging** are `CODES`, `URL`, `PIN_WITH_URL`, and `PIN_AS_CODE`. No merchant is configured with a QR-code delivery format, so that case cannot be exercised here today. ## Card-linked and virtual card offers These merchants carry a card-linked offer (CLO), a gift card offer, or both. Use them to test offer precedence, 0% offers, multi-program CLOs, spend-capped offers, and out-of-stock fallback. > [!WARNING] > > **Card-linked offers are hidden by default.** `getMerchants` returns only gift card offers unless you ask for CLOs explicitly: > > ```graphql theme={null} > query { > getMerchants(offerTypes: { giftCardOffer: true, cardLinkedOffer: true }) { > name > offers { type cloDetails { regularRate promoRate promoMaxCap } } > } > } > ``` > > Without `offerTypes`, every offer comes back as `GIFT_CARD_OFFER` and CLOs look as though they do not exist in staging. They do — 16 merchants carry them. | Test case | Merchant | Slug | Offer types | Staging | | - | - | - | - | - | | Gift card and CLO — promo rate with a spend cap | Amazon | `amazon` | Gift Card + CLO | ✅ | | Gift card and CLO — CLO is 0% | Burger King | `burger-king` | Gift Card + CLO | ✅ | | CLO only | eBay | `ebay` | CLO | ✅ | | Gift card and CLO — multiple CLOs on one merchant | Adidas | `adidas` | Gift Card + CLO | ✅ | | Gift card and CLO — gift card is the primary offer | — | — | — | ⏳ | | Gift card and CLO — CLO primary (single card program) | — | — | — | ⏳ | | Gift card and CLO — CLO primary, offer ends at lifetime spend cap | — | — | — | ⏳ | | Gift card and CLO — CLO primary, offer ends at monthly spend cap | — | — | — | ⏳ | | Gift card and CLO — different CLO per descriptor | — | — | — | ⏳ | | Gift card and CLO — gift card is out of stock | — | — | — | ⏳ | Amazon is the richest CLO fixture: `regularRate` 3%, `promoRate` 6%, `promoMaxCap` 1000. Burger King's CLO has `regularRate` 0 with a 6% promo, which is the case to use for zero-base-rate handling. ## Exclusive (group-based) rates These merchants offer an **exclusive rate** that is only visible to accounts in a specific user group. Add your test account to the listed user group to see the rate. | User group | Merchant(s) | Tier | Staging | | - | - | - | - | | Silver\_Business | Build-A-Bear Workshop, adidas | Silver | ✅ | | Gold\_Business | Build-A-Bear Workshop, adidas | Gold | ✅ | | Platinum\_Business | Build-A-Bear Workshop | Platinum | ✅ | ## Next steps > **[Test addresses](/test-addresses)** > > Known-good and known-bad addresses for exercising validation on cards and KYC. > **[Purchase a gift card](/purchase-gift-card)** > > Put a test merchant to work — run the purchase flow end to end. # View gift cards Source: https://docs.fluz.app/view-gift-card getGiftCards, revealGiftCardByGiftCardId After purchasing a gift card, you need to make an API call to reveal the gift card details. Follow these steps to retrieve the necessary information: 1. **Retrieve the Gift Card Order List** — You can skip this step if you just purchased a gift card and have the `giftCardId` already. 2. **Reveal gift card details** — Use the `revealGiftCardByGiftCardId` mutation to reveal the gift card redemption details. ## Step 1: Retrieve the Gift Card Order List > 📘 Reconciling gift cards to your orders > > `getGiftCards` returns `purchaseId` (the same UUID returned by `purchaseGiftCard` and `getUserPurchases`), `purchaseDisplayId` (the short Fluz transaction ID, e.g. `1047283`), and the order value via `purchaseValue` / `currentValue` / `currency`. Use these to map each `giftCardId` back to its originating order and amount without having to reveal every card. To obtain a list of gift cards ordered on your account, use the `getGiftCards` query. This call returns a list of gift cards with basic details, including the `giftCardId`. You will need this ID to reveal a gift card's details. ### Sample Query ```graphql theme={null} query GetGiftCards { getGiftCards( paginate: { limit: 20, offset: 0 } ) { giftCardId purchaseId purchaseDisplayId purchaserUserId endDate status purchaseValue currentValue currency createdAt deliveryFormat termsAndConditions merchant { merchantId name slug } } } ``` ### Query Arguments The following arguments are used to run this query: * `status` — If you want to filter the gift cards by status. * `userCashBalanceId` — If you want to filter gift cards by the cash balance (spend account). * `paginate` — Pagination fields * `paginate.limit` — Maximum amount of gift cards per page * `paginate.offset` — The amount of records that should be offset ### Sample Response ```json theme={null} { "data": { "getGiftCards": [ { "giftCardId": "7c57c381-4b19-49e4-bbb0-404a45166ee4", "purchaseId": "785f57ff-f756-4f5c-afca-b896881e3e87", "purchaseDisplayId": "1047283", "purchaserUserId": "7c57c381-4b19-49e4-bbb0-404a45166ee4", "endDate": "2007-12-03T10:15:30Z", "status": "ACTIVE", "purchaseValue": 25, "currentValue": 25, "currency": "USD", "createdAt": "2007-12-03T10:15:30Z", "deliveryFormat": "PIN_WITH_URL", "termsAndConditions": "Except as required by law, Gift Cards cannot be transferred for...", "merchant": { "merchantId": "123", "name": "Merchant", "slug": "merchant" } } ] } } ``` ### Response Fields `purchaseId` — The UUID of the purchase that created this gift card. Use this to reconcile a gift card against the `purchaseId` you stored when calling `purchaseGiftCard` / from `getUserPurchases`. Returns `null` if no associated purchase is found. `purchaseDisplayId` — The short, human-readable Fluz transaction ID for the purchase (e.g. `1047283`). This is the ID Fluz support references for manual review and in exports. Returns `null` if no associated purchase is found. `purchaseValue` — The value (denomination / face value) the gift card was purchased at, in the card's currency. Use this for reconciling order amounts. `currentValue` — The remaining balance on the gift card, in the card's currency. For single-use cards this typically equals `purchaseValue`. `currency` — The ISO currency code of the gift card's value (e.g. `USD`). `deliveryFormat` — The delivery format of the offer this gift card was purchased under. One of `URL`, `CODES`, `PIN_AS_CODE`, `PIN_WITH_URL`, or `CODE_WITH_PREFIX`. `termsAndConditions` — The terms and conditions text for the offer this gift card was purchased under (legal language, restrictions, expiration policies). This is the offer-time T\&C, which may differ from the merchant's currently active offer's T\&C — same reasoning as `deliveryFormat`. > 📘 Prefer `deliveryFormat` from `getGiftCards` over `getMerchants` > > A merchant's active offer can change over time. The format the gift card was purchased under may differ from the merchant's current active offer, so always use the `deliveryFormat` returned by `getGiftCards` when deciding how to render the redemption details from `revealGiftCardByGiftCardId`. ## Step 2: Reveal Gift Card Details Once you have retrieved the full gift card order list, you can reveal the details of each gift card, including the gift card code and other information, using the `revealGiftCardByGiftCardId` mutation. ### Input The `revealGiftCardByGiftCardId` mutation only requires an input argument of `giftCardId` to return your gift card redemption details as a response. ### Sample Mutation ```graphql theme={null} mutation RevealGiftCardByGiftCardId { revealGiftCardByGiftCardId( giftCardId: "86dd5fcf-fea3-4531-aa5d-1d196957f7d7" ) { code pin url termsAndConditions } } ``` ### Sample Response ```json theme={null} { "data": { "revealGiftCardByGiftCardId": { "code": "9877890000000000", "pin": "2014", "url": null, "termsAndConditions": "Except as required by law, Gift Cards cannot be transferred for..." } } } ``` > 📘 Not all merchant gift cards have codes, pins and URLs > > Depending on the merchant and the partner, the gift card details may vary. Some merchants use an alphanumeric code and do not provide a PIN with their gift card. Some merchants or partners only provide a URL instead of a code and pin. > > Fluz will always pass all information that we are provided from the merchants on each gift card order. ## Recommended Polling Strategy If gift card details are not immediately returned, you can poll the Reveal Gift Card endpoint using an exponential backoff strategy. Starting recommendation: * **Initial delay:** 300ms * **Backoff strategy:** Exponential (e.g. 300ms → 600ms → 1200ms → 2400ms → …) * **Maximum delay:** 180000ms (3 minutes) * Stop polling once gift card details are successfully returned This approach balances responsiveness with system load and helps prevent unnecessary timeouts or excessive requests. # Card status, locks & limits Source: https://docs.fluz.app/virtual-cards/card-status-and-limits What each VirtualCardStatus means, why a locked card still reads ACTIVE, how to read a card's current limit, and how cards are closed. ## Status and lock are separate `status` describes where the card is in its lifecycle. Whether it can authorize right now is a separate question, answered by its **lock** fields. A locked card still reports `status: ACTIVE`. | `VirtualCardStatus` | Meaning | | :- | :- | | `PENDING` | Card created; issuance not yet complete | | `ACTIVE` | Issued and open. Check the lock fields to see whether it can authorize | | `USED` | Used; applies mainly to single-use cards | | `CANCELLED` | Closed permanently | | `REFUNDED` | The card's underlying purchase was refunded | | `EXPIRED` | Closed by the expiry process | | `DISPUTED` | In a dispute | `LOCKED` and `CLOSED_EXPIRED` are not status values. ### Telling a locked card from an open one Read `authorizationSetting`: | Field | Meaning | | :- | :- | | `lockedByUser` | Locked with `lockVirtualCard` | | `lockedBySystem` | Locked by Fluz, for example by risk controls | | `lockCardNextUse` | Locks itself after its next transaction | | `lockDate` | Locks itself at this time | A card is usable only when `status` is `ACTIVE` and no lock applies. ### After `lockDate` passes The card stays `ACTIVE` but declines as locked. It doesn't become `EXPIRED`. You can reopen a multi-use card: * call `unlockVirtualCard`, optionally passing a new lock date; or * call `setVirtualCardLockDate` with a future date, or clear it. ## Reading a card's current state and limit Query `getVirtualCards`, filtered to the card: ```graphql theme={null} query ($filter: VirtualCardFilterInput) { getVirtualCards(filter: $filter) { rows { virtualCardId status authorizationSetting { dailySpendLimit weeklySpendLimit monthlySpendLimit annualSpendLimit lifetimeSpendLimit lockDate lockedByUser lockedBySystem lockCardNextUse } } } } ``` > [!NOTE] > > `initialAmount` is the amount requested when the card was created. It never changes, so it isn't the current limit. Read the current limit from the `authorizationSetting` limit that matches the card's duration. ### Changing the limit Send `spendLimit` **and** `spendLimitDuration` together on `editVirtualCard`. Sending only `spendLimit` may not apply the change. Confirm it by re-reading `authorizationSetting`, not `initialAmount`. ## Closing a card permanently There is no public mutation to cancel a card. For a temporary stop, use `lockVirtualCard`. To cancel permanently, contact support with the `virtualCardId`. `VC-0008 UNABLE_TO_CANCEL` comes from that support-side process. When a card is cancelled, its unspent funded balance goes back through the refund process. Authorizations that haven't cleared are cleaned up with the card's purchase; timing depends on the network. Locking a card doesn't release its balance. # API connectors Source: https://docs.fluz.app/API-connectors-overview Point an integration you have already built at Fluz — without rewriting it. If you already send gift card orders to Tango, Runa, or InComm, you do not have to rebuild anything to buy from Fluz. Change your base URL, change your credential, and keep the code you already wrote. That is what an **API adapter** does. ## What an API adapter is We mapped our API calls onto theirs. Every operation your vendor exposes — create an order, check an order, read a balance — has a Fluz operation behind it, and the adapter sits in between doing the translation in both directions. The result is that the integration you already built against Tango, Runa, or InComm now buys from Fluz instead. You keep your request shapes, and a successful response comes back in the shape your parser already expects. Two things are Fluz's rather than your vendor's: error responses and order status values. Both are covered in [Connector behaviour](/connector-behaviour). ![How the Fluz API Adapter works](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTgwIDY2MCIgd2lkdGg9IjExODAiIGhlaWdodD0iNjYwIiBmb250LWZhbWlseT0iUG9wcGlucywgSW50ZXIsIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZiI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJyYW5kIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM4NEJFRkYiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMjFCQTRDIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPG1hcmtlciBpZD0iYXJyb3ciIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPgogICAgICA8cGF0aCBkPSJNIDAgMCBMIDEwIDUgTCAwIDEwIHoiIGZpbGw9IiM5QTk2OEEiLz4KICAgIDwvbWFya2VyPgogICAgPG1hcmtlciBpZD0iYXJyb3dHcmVlbiIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI5IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSI+CiAgICAgIDxwYXRoIGQ9Ik0gMCAwIEwgMTAgNSBMIDAgMTAgeiIgZmlsbD0iIzBFN0EzNCIvPgogICAgPC9tYXJrZXI+CiAgPC9kZWZzPgoKICA8cmVjdCB3aWR0aD0iMTE4MCIgaGVpZ2h0PSI2NjAiIGZpbGw9IiNGQkY5RjUiLz4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTE4MCIgaGVpZ2h0PSI1IiBmaWxsPSJ1cmwoI2JyYW5kKSIvPgoKICA8IS0tIFRpdGxlIC0tPgogIDx0ZXh0IHg9IjYwIiB5PSI3MiIgZm9udC1zaXplPSIyNyIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzE0MTQwRiI+V2hhdCBhbiBBUEkgYWRhcHRlciBkb2VzPC90ZXh0PgogIDx0ZXh0IHg9IjYwIiB5PSIxMDIiIGZvbnQtc2l6ZT0iMTUiIGZpbGw9IiM2QjZCNjAiPllvdXIgaW50ZWdyYXRpb24ga2VlcHMgc3BlYWtpbmcgdGhlIHZlbmRvciYjODIxNztzIGxhbmd1YWdlLiBGbHV6IGRvZXMgdGhlIHRyYW5zbGF0aW5nLjwvdGV4dD4KCiAgPCEtLSA9PT09PT09PT09PT0gUk9XIDEgOiBUT0RBWSA9PT09PT09PT09PT0gLS0+CiAgPHJlY3QgeD0iNjAiIHk9IjE0MCIgd2lkdGg9IjEwNjAiIGhlaWdodD0iMTg4IiByeD0iMTQiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZT0iI0U3RTNEOSIgc3Ryb2tlLXdpZHRoPSIxIi8+CiAgPHRleHQgeD0iODgiIHk9IjE3NCIgZm9udC1zaXplPSIxMiIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzlBOTY4QSIgbGV0dGVyLXNwYWNpbmc9IjEuNiI+VE9EQVk8L3RleHQ+CgogIDwhLS0gYXBwIC0tPgogIDxyZWN0IHg9Ijg4IiB5PSIxOTYiIHdpZHRoPSIyNTIiIGhlaWdodD0iOTQiIHJ4PSIxMCIgZmlsbD0iI0Y0RjFFQSIgc3Ryb2tlPSIjREREOENDIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8dGV4dCB4PSIyMTQiIHk9IjIzOCIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzE0MTQwRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+WW91ciBhcHBsaWNhdGlvbjwvdGV4dD4KICA8dGV4dCB4PSIyMTQiIHk9IjI2MiIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzZCNkI2MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+QWxyZWFkeSBidWlsdC4gQWxyZWFkeSBsaXZlLjwvdGV4dD4KCiAgPGxpbmUgeDE9IjM0OCIgeTE9IjI0MyIgeDI9IjQyNCIgeTI9IjI0MyIgc3Ryb2tlPSIjOUE5NjhBIiBzdHJva2Utd2lkdGg9IjEuNiIgbWFya2VyLWVuZD0idXJsKCNhcnJvdykiLz4KICA8dGV4dCB4PSIzODYiIHk9IjIzMCIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjOUE5NjhBIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj52ZW5kb3Itc2hhcGVkPC90ZXh0PgogIDx0ZXh0IHg9IjM4NiIgeT0iMjQ1IiBmb250LXNpemU9IjExLjUiIGZpbGw9IiM5QTk2OEEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnJlcXVlc3Q8L3RleHQ+CgogIDwhLS0gdmVuZG9yIGFwaSAtLT4KICA8cmVjdCB4PSI0MzIiIHk9IjE5NiIgd2lkdGg9IjI1MiIgaGVpZ2h0PSI5NCIgcng9IjEwIiBmaWxsPSIjRjRGMUVBIiBzdHJva2U9IiNEREQ4Q0MiIHN0cm9rZS13aWR0aD0iMSIvPgogIDx0ZXh0IHg9IjU1OCIgeT0iMjM4IiBmb250LXNpemU9IjE2IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjMTQxNDBGIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UYW5nbyAvIFJ1bmEgLyBJbkNvbW08L3RleHQ+CiAgPHRleHQgeD0iNTU4IiB5PSIyNjIiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2QjZCNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRoZWlyIEFQSTwvdGV4dD4KCiAgPGxpbmUgeDE9IjY5MiIgeTE9IjI0MyIgeDI9Ijc2OCIgeTI9IjI0MyIgc3Ryb2tlPSIjOUE5NjhBIiBzdHJva2Utd2lkdGg9IjEuNiIgbWFya2VyLWVuZD0idXJsKCNhcnJvdykiLz4KCiAgPCEtLSBzdXBwbHkgLS0+CiAgPHJlY3QgeD0iNzc2IiB5PSIxOTYiIHdpZHRoPSIyNTIiIGhlaWdodD0iOTQiIHJ4PSIxMCIgZmlsbD0iI0Y0RjFFQSIgc3Ryb2tlPSIjREREOENDIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8dGV4dCB4PSI5MDIiIHk9IjIzOCIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzE0MTQwRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+VGhlaXIgZ2lmdCBjYXJkIHN1cHBseTwvdGV4dD4KICA8dGV4dCB4PSI5MDIiIHk9IjI2MiIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzZCNkI2MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+VGhlaXIgcmF0ZXM8L3RleHQ+CgogIDwhLS0gPT09PT09PT09PT09IFJPVyAyIDogV0lUSCBGTFVaID09PT09PT09PT09PSAtLT4KICA8cmVjdCB4PSI2MCIgeT0iMzYwIiB3aWR0aD0iMTA2MCIgaGVpZ2h0PSIxODgiIHJ4PSIxNCIgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSIjQzhFOEQyIiBzdHJva2Utd2lkdGg9IjEuNSIvPgogIDxyZWN0IHg9IjYwIiB5PSIzNjAiIHdpZHRoPSIxMDYwIiBoZWlnaHQ9IjQiIHJ4PSIyIiBmaWxsPSJ1cmwoI2JyYW5kKSIvPgogIDx0ZXh0IHg9Ijg4IiB5PSIzOTQiIGZvbnQtc2l6ZT0iMTIiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiMwRTdBMzQiIGxldHRlci1zcGFjaW5nPSIxLjYiPldJVEggVEhFIEZMVVogQVBJIEFEQVBURVI8L3RleHQ+CgogIDwhLS0gYXBwICh1bmNoYW5nZWQpIC0tPgogIDxyZWN0IHg9Ijg4IiB5PSI0MTYiIHdpZHRoPSIyNTIiIGhlaWdodD0iOTQiIHJ4PSIxMCIgZmlsbD0iI0Y0RjFFQSIgc3Ryb2tlPSIjREREOENDIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8dGV4dCB4PSIyMTQiIHk9IjQ1MiIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzE0MTQwRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+WW91ciBhcHBsaWNhdGlvbjwvdGV4dD4KICA8cmVjdCB4PSIxNjQiIHk9IjQ2NiIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIyNCIgcng9IjEyIiBmaWxsPSIjRTNGNEU4Ii8+CiAgPHRleHQgeD0iMjE0IiB5PSI0ODIiIGZvbnQtc2l6ZT0iMTIiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiMwRTdBMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnVuY2hhbmdlZDwvdGV4dD4KCiAgPGxpbmUgeDE9IjM0OCIgeTE9IjQ2MyIgeDI9IjQyNCIgeTI9IjQ2MyIgc3Ryb2tlPSIjMEU3QTM0IiBzdHJva2Utd2lkdGg9IjEuNiIgbWFya2VyLWVuZD0idXJsKCNhcnJvd0dyZWVuKSIvPgogIDx0ZXh0IHg9IjM4NiIgeT0iNDUwIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMwRTdBMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnNhbWU8L3RleHQ+CiAgPHRleHQgeD0iMzg2IiB5PSI0NjUiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzBFN0EzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+cmVxdWVzdDwvdGV4dD4KCiAgPCEtLSBhZGFwdGVyIC0tPgogIDxyZWN0IHg9IjQzMiIgeT0iNDE2IiB3aWR0aD0iMjUyIiBoZWlnaHQ9Ijk0IiByeD0iMTAiIGZpbGw9IiMwRTdBMzQiLz4KICA8dGV4dCB4PSI1NTgiIHk9IjQ1MiIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Rmx1eiBBUEkgQWRhcHRlcjwvdGV4dD4KICA8dGV4dCB4PSI1NTgiIHk9IjQ3NiIgZm9udC1zaXplPSIxMyIgZmlsbD0iI0I3RThDNiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+dHJhbnNsYXRlcyBpbiwgdHJhbnNsYXRlcyBiYWNrPC90ZXh0PgoKICA8bGluZSB4MT0iNjkyIiB5MT0iNDYzIiB4Mj0iNzY4IiB5Mj0iNDYzIiBzdHJva2U9IiMwRTdBMzQiIHN0cm9rZS13aWR0aD0iMS42IiBtYXJrZXItZW5kPSJ1cmwoI2Fycm93R3JlZW4pIi8+CgogIDwhLS0gZmx1eiBzdXBwbHkgLS0+CiAgPHJlY3QgeD0iNzc2IiB5PSI0MTYiIHdpZHRoPSIyNTIiIGhlaWdodD0iOTQiIHJ4PSIxMCIgZmlsbD0iI0Y0RjFFQSIgc3Ryb2tlPSIjREREOENDIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8dGV4dCB4PSI5MDIiIHk9IjQ1MiIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzE0MTQwRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Rmx1eiBnaWZ0IGNhcmQgc3VwcGx5PC90ZXh0PgogIDx0ZXh0IHg9IjkwMiIgeT0iNDc2IiBmb250LXNpemU9IjEzIiBmaWxsPSIjNkI2QjYwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5GbHV6IHJhdGVzPC90ZXh0PgoKICA8IS0tID09PT09PT09PT09PSBGT09URVI6IHdoYXQgeW91IGNoYW5nZSA9PT09PT09PT09PT0gLS0+CiAgPHRleHQgeD0iNjAiIHk9IjU5NiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzlBOTY4QSIgbGV0dGVyLXNwYWNpbmc9IjEuMiI+QUxMIFlPVSBDSEFOR0U8L3RleHQ+CgogIDxyZWN0IHg9IjYwIiB5PSI2MDgiIHdpZHRoPSIzMzYiIGhlaWdodD0iMzYiIHJ4PSI4IiBmaWxsPSIjRkZGRkZGIiBzdHJva2U9IiNFN0UzRDkiLz4KICA8dGV4dCB4PSI3OCIgeT0iNjMxIiBmb250LXNpemU9IjEzLjUiIGZpbGw9IiMxNDE0MEYiPjx0c3BhbiBmb250LXdlaWdodD0iNjAwIj4xLjwvdHNwYW4+ICBUaGUgYmFzZSBVUkw8L3RleHQ+CgogIDxyZWN0IHg9IjQyMiIgeT0iNjA4IiB3aWR0aD0iMzM2IiBoZWlnaHQ9IjM2IiByeD0iOCIgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSIjRTdFM0Q5Ii8+CiAgPHRleHQgeD0iNDQwIiB5PSI2MzEiIGZvbnQtc2l6ZT0iMTMuNSIgZmlsbD0iIzE0MTQwRiI+PHRzcGFuIGZvbnQtd2VpZ2h0PSI2MDAiPjIuPC90c3Bhbj4gIFRoZSBBdXRob3JpemF0aW9uIGhlYWRlcjwvdGV4dD4KCiAgPHJlY3QgeD0iNzg0IiB5PSI2MDgiIHdpZHRoPSIzMzYiIGhlaWdodD0iMzYiIHJ4PSI4IiBmaWxsPSIjRkZGRkZGIiBzdHJva2U9IiNFN0UzRDkiLz4KICA8dGV4dCB4PSI4MDIiIHk9IjYzMSIgZm9udC1zaXplPSIxMy41IiBmaWxsPSIjMTQxNDBGIj48dHNwYW4gZm9udC13ZWlnaHQ9IjYwMCI+My48L3RzcGFuPiAgWW91ciBicmFuZCBpZGVudGlmaWVyczwvdGV4dD4KPC9zdmc+Cg==) Think of a travel plug adapter. The appliance does not change. The wall socket does not change. Something in between makes the two fit. ## How the mapping works Your request arrives shaped like the vendor's. The adapter translates the route and the fields into the equivalent Fluz operation, executes it against Fluz supply, then translates the result back into the response shape your code already expects. ![Request and response mapping through the Fluz API Adapter](https://mintcdn.com/fluz-dd3e0eaa/ntRBNWQmiqBrvQt4/images/fluz-api-adapter-technical.svg?fit=max\&auto=format\&n=ntRBNWQmiqBrvQt4\&q=85\&s=75f36d0c5db48f78ea6ee25b2df40848) ### Operation mapping Each connector page publishes the full table for its vendor. Tango Card, as an example: | Operation | Your existing call | Fluz endpoint | | :- | :- | :- | | Create order | `POST /v2/orders/` | `POST /v2/orders/` | | Get order | `GET /v2/orders/:referenceOrderID` | `GET /v2/orders/:referenceOrderID` | | Get all orders | `GET /v2/orders` | `GET /v2/orders` | | Get balance, all accounts | `GET /v2/accounts` | `GET /v2/accounts` | | Get balance, one account | `GET /v2/accounts/:accountId` | `GET /v2/accounts/:id` | Paths and verbs are preserved deliberately. The point of the adapter is that your routing layer does not have to know anything changed. ### Field mapping Tango Card again, as an example: | Field in your request | What Fluz does with it | | :- | :- | | `utid` / brand code | **Resolved against the Fluz catalog. See the warning below** | | `amount` | Purchase amount | | `externalRefID` | Accepted. Correlate the order using `referenceOrderID` from the response | | `accountIdentifier` | Orders are funded from the account your API key is issued for. Request one key per account if you split funding across several | | `customerIdentifier` | Kept for compatibility with your existing request shape | **Tango and InComm return complete results synchronously.** The call does not return until the purchase has finished, which can take up to 150 seconds, so set your client's read timeout above that. Runa is asynchronous by default: it returns a reference ID immediately and you read the order once it completes. Send `X-Execution-Mode: sync` to wait for the full result instead. Both are covered in [Connector behaviour](/connector-behaviour). ### Per-vendor request constraints Your vendor's request shape is accepted as it stands, but a few values are fixed. Email delivery to the recipient must be off, orders are single-recipient and single-product, and Runa orders must be funded from the account balance in USD. A request that breaks one of these is rejected. The full list is on [Connector behaviour](/connector-behaviour). Read it alongside your connector page's request examples before you cut over. ## What changes, and what does not | | Before | After | | :- | :- | :- | | Request shapes | Vendor's schema | **Unchanged** | | Successful response shapes | Vendor's schema | **Unchanged** | | Base URL | Vendor's host | Fluz adapter host | | Credential | Vendor's key | Your Fluz API key | | Order status values | Vendor's | **Fluz's. Update your status checks** | | Error responses | Vendor's schema | **Fluz's envelope** | | Brand identifiers (`utid`, brand slug, product code) | Vendor's catalog | **Fluz's catalog — must be remapped** | | Rates and discounts | Vendor's | Fluz's | | Supplier of record, invoicing, reconciliation | Vendor | Fluz | **Brand identifiers are the one thing you must change.** The adapter does not translate the vendor's product catalog into Fluz's. A Tango `utid` of `U561593` is not the same card as a Fluz `utid` of `1800FL-US`. Map your brand list against the Fluz catalog before you cut over, or your orders will succeed and deliver the wrong card. ## Switching in three steps **1. Change the base URL** Point your existing client at the Fluz adapter host for your vendor. For Tango Card that is `https://api-adapter.staging.fluzapp.com/tangocard` in staging and `https://api-adapter.fluzapp.com/tangocard` in production, instead of `https://integration-api.tangocard.com/rass`. Both hosts are listed on every connector page. **2. Change the Authorization header** Every connector uses HTTP Basic authentication with your Fluz API key: `Authorization: Basic `. Drop the vendor's username and password. **3. Remap your brand identifiers and send the same calls** Swap your vendor product codes for their Fluz equivalents. Everything else — paths, verbs, request bodies, response parsing — stays as it is. ## Available connectors > **[Gift cards](#pick-your-vendor)** > > **Documented and available.** Tango Card, Runa, InComm — each with a published endpoint comparison and request examples. > **Payment processing** > > **Beta.** Stripe, Worldpay, TSYS, Checkout, Braintree, TabaPay, Finix, Authorize.net, Sola / Cardknox. Contact sales for early access and endpoint details. > **Wallet withdrawals** > > **Beta.** Venmo and PayPal. Contact sales for early access and endpoint details. Gift card connectors are the mature family and the only one with published operation-level documentation today. Each gift card connector page lists the exact operations that are implemented, alongside the original vendor endpoint they correspond to. ## Coverage is per-operation **The adapter implements the operations listed on each connector page, and only those.** Each one is a purpose-built translation for that operation rather than a blanket pass-through. If an operation does not appear in the endpoint comparison table on a connector page, it has not been built yet. Before you begin, list every vendor endpoint your integration calls today and check each one against the connector page. If something you depend on is missing, tell your Fluz contact before you start — most gaps are small additions, but they need to be scoped. ## Connectors are separate from the Fluz GraphQL API This trips people up, so it is worth stating plainly. Fluz has two distinct developer surfaces, and they do not share credentials. | | API Connectors | Fluz API | | :- | :- | :- | | Protocol | REST, shaped like the vendor | GraphQL | | Authentication | Basic, with a Fluz API key | Bearer, with a User Access Token | | Credential source | Issued by Fluz on request | Self-serve in the Developers section of the dashboard | | Scope | The operations your old vendor supported | The full Fluz platform | **Connector keys are not dashboard keys.** The API key you generate in the Developers section of the Fluz dashboard will not authenticate against a connector, and vice versa. Connector credentials are provisioned by Fluz. Contact your Fluz representative to request one. ## When to use a connector, and when not to > **Use a connector when** > > You have a working vendor integration in production, you want Fluz supply and Fluz rates quickly, and you do not want to spend an engineering cycle on the switch. Time to live is measured in days. > **Use the Fluz API when** > > You want the full platform: virtual cards, wallets and spend accounts, transaction data, webhooks, cashback rates, offer quoting, and the complete merchant catalog. None of that is reachable through a vendor-shaped interface. A common path is to start with a connector to get volume flowing, then migrate to the [Fluz API](/overview) as you adopt capabilities the old vendor never had. ## Getting access API Connectors are currently offered as a beta program. Contact your Fluz representative or email sales to request connector credentials and confirm operation coverage for your vendor. ## Pick your vendor > **[Tango Card](/tango)** > > Orders and account balances. Basic auth, synchronous responses. > **[Runa](/runa)** > > Orders and currency balances. Synchronous and asynchronous execution modes. > **[InComm](/in-comm)** > > Immediate orders, order detail, card retrieval, and program balance. # Authorize.net Source: https://docs.fluz.app/authorize-net Email sales to request access to our BETA program of Payment Processing API connectors. # Braintree Source: https://docs.fluz.app/braintree Email sales to request access to our BETA program of Payment Processing API connectors. # Overview Source: https://docs.fluz.app/build-a-platform Register or connect your customers, get a token scoped to each of them, and run the exact same APIs you already use on your own account. ## Two token paths, one API > **[Your own account](/get-started/api-credentials)** > > `API credentials` → `Your token` > > You use API keys to mint a User Access Token for your account. > **[A customer's account](/client-facing-o-auth-grant-flow)** > > `OAuth grant` → `Their token` > > Customers authorize your app, and Fluz returns a token scoped to their account. > [!NOTE] > > From there, `createVirtualCard` is `createVirtualCard` — no separate platform API to learn. ## Onboard and connect customers **1. Create your OAuth app** Configure your app, redirect URIs, and scopes in the dashboard. [Create an OAuth app →](/create-an-o-auth-app) **2. Register or connect the customer** New customers: register individuals or businesses with built-in KYC and KYB. Existing Fluz users: send them through the grant flow. [User registration →](/user-registration) · [KYC verification →](/user-kyc-verification) **3. Exchange the grant for a token** The customer authorizes your app; you exchange the code for a token scoped to their account. [Grant flow →](/client-facing-o-auth-grant-flow) · [Exchange the code →](/exchange-an-o-auth-authorization-code) **4. Run the APIs you already know** Send the customer-scoped token instead of your own. Everything else is identical. [Refresh tokens →](/refresh-o-auth-access-token) ## What you can do on connected accounts Every capability applies as written when you use a customer-scoped token. > **[Virtual cards](/features/virtual-cards)** > **[Gift cards](/purchase-gift-card)** > **[Wallets & transfers](/features/spend-accounts)** > **[Funding sources](/features/funding-sources)** > **[Transactions](/features/get-all-transactions)** > **[Authorized users](/features/create-authorized-users)** ## Take your app live Anything you build against your **own** account needs nothing from us. Certifying a **public** app — one that operates on other verified Fluz accounts — requires a due diligence review first. **1. Handle account opening and management** Register and verify your customers, or connect existing Fluz users through the grant flow. [User registration →](/user-registration) · [KYC verification →](/user-kyc-verification) · [Business registration →](/business-registration) **2. Build your core capabilities** Implement the capabilities your app needs in staging using a customer-scoped token. Every capability behaves exactly as documented on your own account. **3. Complete the due diligence form** Submit the form or forms below that apply to your program. Our team reviews the submission and approves your application. **4. Go live** We certify your public app and release production credentials. [Deploying to production →](/deploying-to-production) ## Due diligence > [!WARNING] > > **Platform due diligence is only required to operate on other verified accounts. You do not need it to build an application on your own account for your own activity.** Specialized verticals are the exception — those programs are reviewed regardless of whose account you operate on. > [!NOTE] > > **Three ways to work with card details, and only one needs a PCI certification.** > > | You are… | What you get | PCI certification | > | :- | :- | :- | > | Building on **your own account** (a personal or private application) | Full card data over the API | Not required — ask us to enable `PCI_COMPLIANCE` on your app | > | Using [**Secure Elements Card Reveal**](/build-a-platform/card-reveal) | Card details rendered to your user in a Fluz-hosted frame, inline in your page | Not required — the data never reaches your servers or your page's JavaScript | > | Taking **raw card data onto your own servers**, on accounts that aren't yours | `revealVirtualCardByVirtualCardId` returns the PAN, expiry, and CVV in the API response | Required — submit your certification below for review | > **[Operating on behalf of your customers](https://form.jotform.com/261725180124046)** > > Required if you are embedding Fluz to operate on behalf of your customers. Complete the platform due diligence form → > **[Specialized verticals](/specialized-verticals)** > > Gaming, prediction markets, sweepstakes, and money services businesses follow an enhanced review path before going live. See the process, the timeline, and the form for your vertical → > **[Direct card interaction (PCI)](https://form.jotform.com/262074914005047)** > > Only required if raw card data must reach your own servers — either the full PAN, expiry, and CVV of cards we issue, or your users' existing cards passed to us. Most integrations no longer need this: on your own account it isn't required at all, and [Secure Elements Card Reveal](/build-a-platform/secure-elements-overview) shows a user their card details without your systems ever entering PCI scope. If you do need the raw data, submit your PCI DSS certification here → > **[Custom card art](/custom-card-art)** > > Required if cards will carry your brand instead of the standard Fluz design. Bank and network approval adds 6–8 weeks — see the specifications and the submission process → We need these completed before we can certify your public app to go live. # Card reveal Source: https://docs.fluz.app/build-a-platform/card-reveal Mount a Fluz-hosted viewer in your page to show a user their PAN, expiry, and CVV, without the data touching your servers or your page's JavaScript. > [!NOTE] > > This page assumes you've read [Secure Elements > Overview](/build-a-platform/secure-elements-overview) — it covers minting a > client token, loading the SDK, and styling fields, all of which apply here. ## See it live [Fluz Secure Elements — Card Reveal demo](https://demo.secure.fluz.app/) The demo mints its own token and mounts the viewer automatically. Use **Remint token & remount** if the reveal buttons stop responding, and **Reveal**, **Reveal CVV only**, or **Mask** to try the field-level controls covered below. [Open it in its own tab →](https://demo.secure.fluz.app/) ## Mint a reveal token Call [`POST /v1/client-token`](/build-a-platform/secure-elements-overview#mint-a-client-token) with `"purpose": "reveal"` and the `virtualCardId` you want to show: ```json theme={null} { "purpose": "reveal", "virtualCardId": "c107e50b-10f3-449c-92c0-609d9a8cfa2a" } ``` Pass the `clientToken` and `loadToken` it returns straight into `createCardViewer` below. ## Create the viewer ```js theme={null} const viewer = createCardViewer({ clientToken, loadToken, frameHostOrigin: "https://staging.secure.fluz.app", fields: ["pan", "expiry", { field: "cvv", individualReveal: false }], }); ``` `fields` controls which pieces of the card render, in order — omit it and you get `["pan", "expiry", "cvv"]`. Each entry is either a bare field name or a `{ field, individualReveal }` object; `"pan"` is shorthand for `{ field: "pan", individualReveal: true }`. `pan`, `expiry`, and `cvv` are the only valid field names — anything else throws a `FluzElementsError` (`error.code === "INVALID_FIELD"`) synchronously, from `createCardViewer` itself, before you ever call `mount()`. | Field | Masked placeholder, before any reveal | Notes | | :- | :- | :- | | `pan` | `•••• •••• •••• {last4}` | Last 4 digits come from the card metadata on your client token | | `expiry` | The real `MM/YYYY` — not masked | Not treated as sensitive | | `cvv` | `•••` | Supports `individualReveal: false` (below) | > [!NOTE] > > `individualReveal` defaults to `true` on every field. Setting it to `false` > prevents that one field from being revealed on its own — see [Reveal a single > field](#reveal-a-single-field). It's independent of `reveal()`, which always > reveals every field regardless of this setting. ## Mount it ```js theme={null} await viewer.mount(document.getElementById("card-viewer")); ``` `mount()` appends one sandboxed iframe per configured field into the container element you pass it — three separate frames for the default `fields`, not one combined frame — and returns a promise that resolves once every frame has completed its handshake. It rejects with a `FluzElementsError` if: * the `style` you passed to `createCardViewer` fails validation (`error.code === "INVALID_STYLE"`) — see [Styling fields](/build-a-platform/secure-elements-overview#styling-fields) * a frame doesn't complete its handshake within `mountTimeoutMs` (`error.code === "MOUNT_TIMEOUT"`; defaults to 10 seconds, and is configurable via `createCardViewer({ ..., mountTimeoutMs })`) * a frame fails to load at all, or this viewer is already mounted (`error.code === "MOUNT_FAILED"`) — each `CardViewer` instance can only be mounted once; create a new one with `createCardViewer` if you need to mount again ## Reveal and mask fields ```js theme={null} await viewer.reveal(); // fetch and show every configured field at once await viewer.reveal("cvv"); // fetch and show one field, if that field allows it viewer.setMask("cvv", true); // re-mask a field that's already been revealed viewer.setMask("cvv", false); // un-mask it again -- see below ``` * **`reveal(field?)`** — async. Fetches the real value from Fluz and displays it. Called with no argument, it fetches and shows every configured field regardless of `individualReveal`. Called with a field name, it fetches and shows just that field — unless that field was configured with `individualReveal: false`, in which case it rejects with `error.code === "INDIVIDUAL_REVEAL_DISABLED"`. An unmounted viewer, or a field name that isn't one of `fields`, rejects with `MOUNT_FAILED`. * **`setMask(field, masked, options?)`** — synchronous, not async. It never fetches anything — it only toggles what's currently displayed: * `setMask(field, true)` re-masks the field back to its placeholder, whether or not it was ever revealed. * `setMask(field, false)` un-masks it — but only shows the real value if `reveal()` already fetched one for that field. Call it before any `reveal()` and the field just stays on its placeholder, since there's no fetched value yet to show. * `setMask(field, true, { hidden: true })` blanks the field completely (empty, not even a placeholder) instead of showing dots/last4/expiry. `hidden` only has an effect while `masked` is `true`. * There's no bulk "mask all" call — call `setMask` once per field in `fields` if you need to reset the whole viewer. * **`destroy()`** — tears down every frame and detaches the viewer. Call this on unmount so you don't leak mounted frames when your component goes away. ### Reveal a single field To reveal just one field on its own (e.g. a "Show CVV" button next to that field), call `reveal(field)` — every field defaults to allowing this, so no config is needed for the common case. If a field should *never* be revealed on its own, and only ever appear as part of the whole-card `reveal()` call, opt it out with `{ field, individualReveal: false }` in `fields`: ```js theme={null} const viewer = createCardViewer({ clientToken, loadToken, frameHostOrigin: "https://staging.secure.fluz.app", fields: ["pan", "expiry", { field: "cvv", individualReveal: false }], }); await viewer.reveal("cvv"); // rejects — error.code === "INDIVIDUAL_REVEAL_DISABLED" await viewer.reveal(); // succeeds — reveals pan, expiry, and cvv together ``` `reveal(field)` against an opted-out field rejects with `FluzElementsError` (`code: "INDIVIDUAL_REVEAL_DISABLED"`) without contacting `frame-host`. Either way, `reveal()` with no argument always reveals every mounted field — `individualReveal` has no effect on it. > [!NOTE] > > This is a client-side integration choice, not a server-enforced capability — > it controls what your own UI is allowed to trigger, not what data the grant > can return. Don't rely on `individualReveal: false` as a security boundary. ## Handle events ```js theme={null} const unsubscribeMount = viewer.onMount(() => { // all configured fields have finished rendering }); const unsubscribeError = viewer.onError((error) => { // error.code, error.message }); ``` `onMount` fires once, after every configured field has rendered inside the frame. `onError` fires for problems that happen *inside* an already-mounted frame — a failed `reveal()` or a rate limit — rather than problems with `mount()` or `createCardViewer()` itself, which reject or throw directly instead (see below). Both `onMount` and `onError` return an unsubscribe function. Every `FluzElementsError` this capability can produce, and where it surfaces: | Code | Where it surfaces | Meaning | | :- | :- | :- | | `INVALID_FIELD` | Thrown by `createCardViewer` | A `fields` entry isn't `pan`, `expiry`, or `cvv` | | `INVALID_FRAME_HOST_ORIGIN` | Thrown by `createCardViewer` | `frameHostOrigin` isn't a recognized Fluz frame host | | `INVALID_STYLE` | Rejected by `mount()` | A `style` value failed validation | | `MOUNT_TIMEOUT` | Rejected by `mount()` | A field's frame didn't complete its handshake within `mountTimeoutMs` | | `MOUNT_FAILED` | Rejected by `mount()`, or thrown by `reveal()` / `setMask()` | A frame failed to load; the viewer was already mounted; or `reveal()` / `setMask()` was called with an unmounted or unknown field | | `INDIVIDUAL_REVEAL_DISABLED` | Rejected by `reveal(field)` | That field was configured with `individualReveal: false` | | `FIELD_ERROR` | Delivered to `onError` | A `reveal()` failed inside the frame after mount (network error, or reveal not yet available) | | `RATE_LIMITED` | Delivered to `onError` | Too many reveal attempts for this grant — `error.message` includes the retry-after time | ## Full example ```html theme={null}
``` `/mint-reveal-token` is your own backend route — the one that calls `POST /v1/client-token` with your Fluz OAuth access token, as described in [Mint a client token](/build-a-platform/secure-elements-overview#mint-a-client-token). ## Next steps > **[Secure Elements Overview](/build-a-platform/secure-elements-overview)** > > Token minting, SDK loading, styling, and CSP. > **[Secure Card Input](/build-a-platform/secure-card-input)** > > Collect a physical card instead of revealing one. > **[VAN Reveal](/build-a-platform/van-reveal)** > > The same capability, applied to a virtual account number's account and routing number instead of a card. > **[Live demo](https://demo.secure.fluz.app/)** > > Reveal, reveal CVV only, and mask, running against staging. > **[Example integrations](https://github.com/fluz-app/secure-elements-examples)** > > Runnable plain-HTML and React reveal examples with a token-minting server. # OAuth applications overview Source: https://docs.fluz.app/build-a-platform/oauth-applications-overview How your application gets permission to act on a Fluz user's account — the credentials involved, the permission model, the token lifecycle, and which page to read next. ## Why OAuth exists here Your application can already do everything the Fluz API offers **on your own account**. You mint a token with your API key and go — see [Authentication](/concepts/authentication) and [API credentials](/get-started/api-credentials). An OAuth application is what you need when the account isn't yours. The moment you want to issue a card on a customer's wallet, pull from their linked bank account, read their transactions, or pay them out, you need that person's explicit permission — and you need it in a form Fluz can verify, scope, expire, and revoke. That's what an OAuth application is: **a registered identity for your software, plus a consent mechanism that turns a user's approval into a token your server can use.** > [!NOTE] > > Once you hold a customer-scoped token, the API is identical. `createVirtualCard` on your own token creates a card on your wallet; the same mutation on a customer token creates it on theirs. OAuth changes *whose account you're touching*, not what you can do. *** ## Do you need one? **No — your own account only** You're issuing cards, buying gift cards, or moving money **within your own Fluz account**: a disbursement engine, a bulk card run, an internal spend tool, an ERP sync. Use your application API key to call `generateUserAccessToken` directly. No OAuth app, no consent screen, no redirect. Start at [API credentials](/get-started/api-credentials). **Yes — customer accounts** You're building a platform where **your users have their own Fluz accounts** and you act on their behalf. You need an OAuth application, and each user has to grant your app scopes once. Then you hold a refreshable, customer-scoped token. Start with [Create an OAuth App](/create-an-o-auth-app), then see [Build a platform](/build-a-platform). **You already have one** You're embedding a [Fluz Widget](/developers/widgets). A widget **is** an OAuth application — one that ships with a hosted front end for the consent step instead of making you build a redirect flow. The credentials, scopes, and token mechanics on this page all apply to it. See [Configure App Widget](/developers/configure-app-widget). *** ## Three sets of credentials, three different jobs The most common source of confusion in this section is that a Fluz application carries more than one credential pair, and they are not interchangeable. | Credential | Where it lives | What it's for | Ever leaves your server? | | :- | :- | :- | :- | | **API Key** / **API Secret** | Overview tab of your app | Identifies your *application*. Mints tokens on your own account (`Authorization: Basic `) and signs widget pre-approved transaction tokens. | Never | | **Client ID** / **Client Secret** | Overview tab of your app | Identifies your app to the *authorization server*. Used in the authorize URL and to exchange or refresh codes (`Authorization: Basic base64(client_id:client_secret)`). | Client ID is public; secret never | | **Access Token** / **Refresh Token** | Returned per user, per grant | Acts on one specific user's account with one specific set of scopes. | Sent as `Authorization: Bearer ` | > [!WARNING] > > Every secret here mints authority. An `apiSecret` leak lets someone sign transactions as your platform; a `client_secret` leak lets someone exchange codes as your app. Keep both server-side, out of browser bundles, out of mobile binaries, out of source control. *** ## The permission model Fluz enforces permissions at two levels, and an application's effective access is the **intersection** of the two. **1. App-level grant — the ceiling** Set on the **Permissions** tab of your app. This is the maximum your application may ever request, independent of any user. A scope you haven't enabled here is silently ignored if you put it in an authorize URL — the request won't error, the scope simply won't be granted. Some scopes are administered by Fluz rather than self-selected. `PCI_COMPLIANCE` is granted at the application level and cannot be requested when generating a token. Applications operating only on their own account are granted it on request. Applications that take raw card data onto their own servers on accounts belonging to other people must submit a PCI DSS certification first — see [Due diligence](/build-a-platform#due-diligence). If you only need to show a user their own card details, [Secure Elements](/build-a-platform/secure-elements-overview) does that without the scope at all. **2. User-level grant — the floor** Set by the end user on the consent screen. They see the scopes you requested — grouped under readable top-level headers rather than raw enum values — and approve them. Anything they decline is not granted. **3. Both must be live** Validated at `generateUserAccessToken`, not at call time. Both grants must exist and be unexpired. A revoked or lapsed grant therefore surfaces as a **token generation failure**, not as a permission error midway through a flow — which is usually the first place to look when a previously working integration stops working. Scopes by capability: | Area | Scopes | | :- | :- | | Funding sources | `LIST_PAYMENT`, `MANAGE_PAYMENT` | | Deposits & withdrawals | `MAKE_DEPOSIT`, `MAKE_WITHDRAW` | | Gift cards | `LIST_OFFERS`, `PURCHASE_GIFTCARD`, `REVEAL_GIFTCARD`, `LIST_PURCHASES` | | Virtual cards | `CREATE_VIRTUALCARD`, `EDIT_VIRTUALCARD`, `REVEAL_VIRTUALCARD`, `CREATE_SHARE_LINK` | | Card data | `PCI_COMPLIANCE` (application-level, Fluz-administered — [when you need it](/build-a-platform#due-diligence)) | Use `getApplicationScopes` to read what's currently granted. Full reference: [Application Scopes](/fluz-dashboard/application-scopes). > [!NOTE] > > **Ask for less.** A shorter consent screen converts better, and a narrower token limits the damage if it leaks. Request what the flow in front of you needs and mint a new token when you need more. *** ## The lifecycle, end to end Each step below is a deep-dive page in this section. This is the map; the pages are the territory. **1. Create the app** From the developer dashboard, choose **Browse templates** and add the **OAuth Integration** template. Name it, subtitle it, describe it — those three fields are what your users will see on the consent screen, so write them for a human, not for your issue tracker. → [Create an OAuth App](/create-an-o-auth-app) **2. Configure it** On the **Permissions** tab, select your scope ceiling. On the **OAuth** tab, set your **Redirect URIs** (public, no query parameters, any number of them) and your **Webhook URLs** (each optionally subscribed to specific events; a URL with none selected becomes a catch-all). Add an avatar and logomark on **Overview** — the consent screen looks unfinished without them. → [Configure OAuth App](/configure-o-auth-app) **3. Send the user to authorize** Redirect to `/authorize` with `response_type=code`, your `client_id`, a registered `redirect_uri`, a space-delimited `scopes` list, and an optional `state` value you want handed back to you untouched. → [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow) **4. Receive the code** On approval, Fluz redirects to your `redirect_uri` with `code` and your original `state`. On a misconfiguration, the redirect carries an error message describing what didn't match. **5. Exchange the code for tokens** Call `/token/exchange` with the `code` and **the exact same `redirect_uri`**, authenticated with `Authorization: Basic base64(client_id:client_secret)`. You get back an `accessToken`, a `refreshToken`, expiry timestamps, and the confirmed scope array. → [Exchanging an OAuth authorization code](/exchange-an-o-auth-authorization-code) **6. Refresh, don't re-prompt** Call `/token/refresh` with the `refresh_token` and the same Basic auth header. Access tokens are deliberately short-lived — on the order of ten minutes — while refresh tokens last roughly a month. Refresh silently in the background; only send a user back through consent when the refresh token itself has expired or the grant has been revoked. → [Refreshing an OAuth accessToken](/refresh-o-auth-access-token) **7. Go live** Staging and production are separate environments with separate applications and separate credentials. Nothing carries over — you register the app, the redirect URIs, and the webhook endpoints again against production hosts. → [Deploying to Production](/deploying-to-production) *** ## Rules that bite Worth internalizing before you start, because each of these fails quietly or confusingly. > **Redirect URIs must match exactly — twice** > > The `redirect_uri` you send to `/authorize` must be registered on your app, and the one you send to `/token/exchange` must be byte-identical to the one you used at `/authorize`. Trailing slashes, `http` vs `https`, and host casing all count. Register no query parameters on the URI itself — use `state` to carry context instead. > **Unenabled scopes are ignored, not rejected** > > Request a scope you haven't checked on the Permissions tab and the authorize request still succeeds — the scope is dropped. Always read the `scope` array in the exchange response and treat it, not your request, as the truth about what you can do. > **An authorization code is single-use and short-lived** > > Exchange it immediately, server-side, once. If your redirect handler can be replayed — a user refreshing the callback page, a link prefetcher — make sure a second attempt doesn't corrupt state. > **Basic auth is base64 of the pair, not of each part** > > `Authorization: Basic `. Encode the joined string. Most integration failures at the exchange step are this. > **`state` is your only channel back** > > The redirect is a fresh browser navigation. If you need to know which user, which flow, or which page to return to, put a signed or server-looked-up reference in `state`. Don't put anything sensitive in it — it travels through the user's browser. > **Token failures are usually grant failures** > > If `generateUserAccessToken` starts failing for a user who worked yesterday, check whether the app-level grant or the user-level grant expired or was revoked, before you go looking at your code. *** ## OAuth apps vs. widgets Both are applications. Both use the permission model above. The difference is who builds the consent surface. | | OAuth application | Widget | | :- | :- | :- | | Consent UI | You build the redirect flow | Fluz renders it in a modal on your page | | User leaves your site | Yes, to `/authorize` | No | | Sensitive data (PAN, SSN, documents, PIN) | You handle it, and you're in scope for it | Fluz collects and encrypts it | | Registration and KYC | Yours to build, or via API | Included in the flow | | Control over presentation | Complete | Limited to branding | | Time to first working flow | Days | Hours | You can mix them: register and KYC users over the API, then open a widget only for consent and sensitive capture. See [Embedded Widgets](/developers/widgets) for the hybrid patterns. > [!NOTE] > > Showing a user their own card details is no longer an all-or-nothing choice between these two columns. [Secure Elements](/build-a-platform/secure-elements-overview) mounts a Fluz-hosted frame inline in your own layout — you keep full control of presentation and stay out of PCI scope at the same time. *** ## Next steps > **[Create an OAuth app](/create-an-o-auth-app)** > > Register your application from the OAuth Integration template. > **[Configure OAuth app](/configure-o-auth-app)** > > Scopes, redirect URIs, webhooks, branding. > **[Client-facing grant flow](/client-facing-o-auth-grant-flow)** > > Build the authorize URL and handle the callback. > **[Exchange an authorization code](/exchange-an-o-auth-authorization-code)** > > Turn a code into an access token and refresh token. > **[Refresh an access token](/refresh-o-auth-access-token)** > > Stay authorized without re-prompting the user. > **[Deploy to production](/deploying-to-production)** > > Re-register against live hosts and go live. > [!NOTE] > > Building a platform where every one of your customers has a Fluz account? [Build a platform](/build-a-platform) walks the whole pattern end to end, and every capability on [API Features](/features) works identically on a connected account. # Secure card input Source: https://docs.fluz.app/build-a-platform/secure-card-input Collect a physical card and add it as a payment method on a user's Fluz account, without the PAN or CVV ever touching your servers or your page's JavaScript. > [!NOTE] > > This page assumes you've read [Secure Elements > Overview](/build-a-platform/secure-elements-overview) — it covers loading the > SDK and the shared client-token flow, both of which apply here. ## See it live [Fluz Secure Elements — Secure Card Input demo](https://demo.secure.fluz.app/collect/) The demo mints its own token and mounts the fields automatically. Enter any card number that passes a Luhn check, fill in the cardholder name, and submit. Use **Remint token & remount** if the form stops responding. [Open it in its own tab →](https://demo.secure.fluz.app/collect/) ## Mint a tokenization token Call [`POST /v1/client-token`](/build-a-platform/secure-elements-overview#mint-a-client-token) with `"purpose": "tokenization"`: ```json theme={null} { "purpose": "tokenization" } ``` Unlike a reveal token, this one doesn't need a `virtualCardId`. It does need a different scope on your access token — `MANAGE_PAYMENT`, not `CREATE_VIRTUALCARD` — and it's longer-lived (30 minutes by default) since a user filling out a card form takes longer than a reveal click. ## Render the fields ```js theme={null} const inputs = renderFieldsForTokenization({ clientToken, loadToken, frameHostOrigin: "https://staging.secure.fluz.app", }); ``` `renderFieldsForTokenization` has no `fields` option — PAN, expiry, and CVV always mount together as one combined frame, because CVV validation is brand-aware (an Amex CVV is 4 digits; every other brand is 3), which only works if the field knows the card number typed into the frame next to it. You can't mount them independently the way `createCardViewer`'s fields can be. | Option | Required | Details | | :- | :- | :- | | `clientToken` / `loadToken` | Yes | From the mint call above | | `frameHostOrigin` | No | Same allowlisted-origin rules as [Card Reveal](/build-a-platform/card-reveal) — defaults to production if omitted | | `style` | No | Typography is the same `{ color, fontSize, fontFamily, fontWeight }` as `createCardViewer` — see the caveat below. Also accepts the box-model properties `{ width, height, border, backgroundColor }` — see [Box-model styling](#box-model-styling-width-height-border-background-color) | | `layout` | No | `"row"` (default) or `"stacked"` — see [Layout](#layout) | | `placeholders` | No | `{ pan, expiry, cvv }` hint text shown inside each empty field — see [Placeholders](#placeholders) | | `mountTimeoutMs` | No | Defaults to 10 seconds | | `submitTimeoutMs` | No | Defaults to 15 seconds — see [Submit](#submit) | | `excludedCardBrands` | No | e.g. `["amex"]` — see [Track field state](#track-field-state) | > [!WARNING] > > A Google Font passed in `style.fontFamily` renders in [Card > Reveal](/build-a-platform/card-reveal)'s fields but is silently skipped here — > these fields render inside a vendor-hosted vault iframe with no hook for > loading external CSS into it. Only the system-font allowlist (`system-ui`, > `Arial`, `Georgia`, `monospace`, and so on) actually applies a font in this > capability. ### Box-model styling: width, height, border, background color Four more `style` properties, specific to `renderFieldsForTokenization` — they style each field's visible box (the bordered rectangle around the input), not the input text itself: | Property | Accepts | | :- | :- | | `width` | A number followed by `px`, `%`, `em`, or `rem` (e.g. `"100%"`, `"240px"`). Applies to every field's box equally — combine with `layout: "stacked"` (below) to make pan and expiry/cvv different widths instead. | | `height` | Same format as `width` (e.g. `"44px"`) | | `border` | `"none"`, or `"px ``` `/mint-identity-token` is your own backend route — the one that calls `POST /v1/client-token` with `"purpose": "identity"` and the access token of the user being verified. ## Next steps > **[Secure Elements Overview](/build-a-platform/secure-elements-overview)** > > Token minting, SDK loading, styling, and CSP. > **[Verify by SSN](/verify-customers-by-ssn)** > > The verification this element runs, called directly from your server. > **[Secure Card Input](/build-a-platform/secure-card-input)** > > Collect a physical card and add it as a payment method. > **[Live demo](https://demo.secure.fluz.app/identity/)** > > Try the identity field running against staging. # VAN reveal Source: https://docs.fluz.app/build-a-platform/van-reveal Mount a Fluz-hosted viewer in your page to show a user the account and routing number of their own virtual account number, without the data touching your servers or your page's JavaScript. > [!NOTE] > > This page assumes you've read [Secure Elements > Overview](/build-a-platform/secure-elements-overview) — it covers minting a > client token, loading the SDK, and styling fields, all of which apply here. VAN Reveal is the same capability as [Card Reveal](/build-a-platform/card-reveal), applied to a [virtual account number](/features/virtual-account-numbers) instead of a virtual card: it shows a user the account number and routing number for one of their own VANs, inside Fluz-hosted frames your page can't read into. ## Mint a reveal token Call [`POST /v1/client-token`](/build-a-platform/secure-elements-overview#mint-a-client-token) with `"purpose": "van-reveal"` and the `virtualAccountNumberId` you want to show: ```json theme={null} { "purpose": "van-reveal", "virtualAccountNumberId": "9c1f6b2e-4d7a-4c3b-9f11-2a5e8b0d6c74" } ``` Pass the `clientToken` and `loadToken` it returns straight into `createAccountViewer` below. Minting this token requires the `LIST_PAYMENT` OAuth scope — the same scope [`getSpendAccountVirtualAccountNumbers`](/features/virtual-account-numbers#retrieve-a-user-s-virtual-account-numbers) requires, so an integration already listing VANs needs no new provisioning to reveal one. This is distinct from Card Reveal's `CREATE_VIRTUALCARD` and Secure Card Input's `MANAGE_PAYMENT`. ## Create the viewer ```js theme={null} const viewer = createAccountViewer({ clientToken, loadToken, frameHostOrigin: "https://staging.secure.fluz.app", fields: ["accountNumber", "routingNumber"], }); ``` `fields` controls which pieces of the VAN render, in order — omit it and you get `["accountNumber", "routingNumber"]`. Each entry is either a bare field name or a `{ field, individualReveal }` object, the same shape as `createCardViewer`'s `fields`. `accountNumber` and `routingNumber` are the only valid field names — anything else throws a `FluzElementsError` (`error.code === "INVALID_FIELD"`) synchronously, from `createAccountViewer` itself, before you ever call `mount()`. | Field | Masked placeholder, before any reveal | Notes | | :- | :- | :- | | `accountNumber` | `••••{last4}` | Last 4 digits come from the VAN metadata on your client token | | `routingNumber` | The real routing number — not masked | Not vault data — renders in cleartext as soon as the field mounts, before any `reveal()` call | > [!NOTE] > > Only `accountNumber` is vault data. `routingNumber` is cleartext metadata > Fluz already has at grant-mint time, so it renders immediately and > `reveal()`/`reveal("routingNumber")` has no effect on it — there's nothing > to fetch. `individualReveal` on `accountNumber` behaves exactly like it does > on [Card Reveal](/build-a-platform/card-reveal#create-the-viewer)'s fields: > it defaults to `true`, and setting it to `false` prevents that field from > being revealed on its own via `reveal("accountNumber")` — see [Reveal a > single field](/build-a-platform/card-reveal#reveal-a-single-field) for the > same pattern applied to a card field. ## Mount it ```js theme={null} await viewer.mount(document.getElementById("account-viewer")); ``` `mount()` appends one sandboxed iframe per configured field into the container element you pass it, and returns a promise that resolves once every frame has completed its handshake. It rejects with a `FluzElementsError` the same way `createCardViewer`'s viewer does — see [Card Reveal's Mount it](/build-a-platform/card-reveal#mount-it) for the full list of `INVALID_STYLE` / `MOUNT_TIMEOUT` / `MOUNT_FAILED` cases. ## Reveal and mask fields ```js theme={null} await viewer.reveal(); // fetch and show every configured field at once await viewer.reveal("accountNumber"); // fetch and show just the account number viewer.setMask("accountNumber", true); // re-mask a field that's already been revealed viewer.setMask("accountNumber", false); // un-mask it again -- see below ``` `reveal()`, `reveal(field)`, and `setMask()` behave exactly as they do on [Card Reveal](/build-a-platform/card-reveal#reveal-and-mask-fields) — the same async/sync split, the same `{ hidden: true }` option to blank a field completely instead of showing its masked placeholder, and the same `destroy()` to tear down every frame. Calling either against `routingNumber` is a no-op beyond toggling its own masked/unmasked display state, since that field is never fetched from the reveal endpoint. ## Handle events ```js theme={null} const unsubscribeMount = viewer.onMount(() => { // all configured fields have finished rendering }); const unsubscribeError = viewer.onError((error) => { // error.code, error.message }); ``` Same shape, and the same error codes, as [Card Reveal's Handle events](/build-a-platform/card-reveal#handle-events) — `INVALID_FIELD`, `INVALID_FRAME_HOST_ORIGIN`, `INVALID_STYLE`, `MOUNT_TIMEOUT`, `MOUNT_FAILED`, `INDIVIDUAL_REVEAL_DISABLED`, `FIELD_ERROR`, and `RATE_LIMITED`. ## Full example ```html theme={null}
``` `/mint-van-reveal-token` is your own backend route — the one that calls `POST /v1/client-token` with `"purpose": "van-reveal"` and your Fluz OAuth access token, as described in [Mint a client token](/build-a-platform/secure-elements-overview#mint-a-client-token). ## Next steps > **[Secure Elements Overview](/build-a-platform/secure-elements-overview)** > > Token minting, SDK loading, styling, and CSP. > **[Card Reveal](/build-a-platform/card-reveal)** > > The same capability, applied to a virtual card's PAN, expiry, and CVV. > **[Virtual Account Numbers](/features/virtual-account-numbers)** > > How a VAN is provisioned, and how to list the ones on a spend account. > **[Secure Card Input](/build-a-platform/secure-card-input)** > > Collect a physical card and add it as a payment method. # Bulk API Source: https://docs.fluz.app/bulk-api Read and act across many connected Fluz users in a single call, instead of looping over one user token at a time. The Bulk API lets your application act across **many connected Fluz users in a single call** — instead of looping over one user token at a time. It's built for OAuth application developers who need to read or (soon) move funds on behalf of the users who have connected to your app. > [!NOTE] > > This is different from the "Create Virtual Card Bulk Order" operation under Virtual Cards, which creates many cards for a *single* account. The Bulk API operates *across your connected users*. ## Who can use it The Bulk API is gated at the application level. Your app must have the **bulk API capability** enabled by Fluz — reach out to us to request access. Calls that aren't from a bulk-enabled application are rejected with `BulkApiAccessDenied`. ## Authentication All Bulk API calls use **Basic authentication with your application API key** (your `client_id` and `client_secret`), not a user access token: ``` Authorization: Basic ``` Endpoint (staging): ``` https://transactional-graph.staging.fluzapp.com/api/v1/graphql ``` Endpoint (production): ``` https://transactional-graph.fluzapp.com/api/v1/graphql ``` ## Authorization comes from existing connections The Bulk API never creates a new consent surface. A user is only reachable if they have an **active OAuth grant** with your application, and each operation is allowed only where the user has granted the scopes that operation requires. Bulk access is never broader than the equivalent single-user operation. * Disconnecting removes the grant, so a disconnected user simply stops being reachable. * If a user's grant is scoped to specific spend accounts, bulk results for that user are automatically restricted to those accounts. ## Selecting target users Every bulk operation takes a `targetSpec`: | Mode | Meaning | | - | - | | `ALL_CONNECTED` | Every user currently connected to your application. | | `SELECTED` | Only the users whose `externalReferenceId`s you list in `targets`. | * Users are addressed by the `externalReferenceId` you supplied when they connected — not by `accountId`. * A synchronous request addresses at most **100 targets**. * Users connected without an `externalReferenceId` can't be selected individually; they're only reachable via `ALL_CONNECTED`. Use [Discover Connected Users](/discover-connected-users) to see who's connected and which scopes they granted. ## Per-target failure contract One target's failure **never fails the whole request**. Each result carries `success` and, when `success` is `false`, an `error`: | Code | Meaning | | - | - | | `TARGET_NOT_CONNECTED` | The id isn't a user currently connected to your app (unknown, revoked, or never connected). | | `INVALID_TARGET_IDENTIFIER` | The id isn't a valid identifier. | | `INSUFFICIENT_SCOPE` | The user hasn't granted the scopes this operation requires. | | `ACCOUNT_NOT_PERMITTED` | The user's grant doesn't permit this operation on the requested account. | The whole request is only rejected for application-level problems: not bulk-enabled, an invalid request, exceeding the 100-target cap, or when *every* requested target is unresolvable. Each response also summarizes `targetCount`, `successCount`, and `failureCount`. ## Synchronous vs. asynchronous * **Bounded reads are synchronous** and served inline: up to 100 targets, with per-target limits (e.g. 20 transactions each, a 90-day window). These are the three queries documented in this section. * **Writes and large exports are asynchronous** (coming soon): you submit a job, poll its status, and download results. `hasNextPage`/`totalCount` on the sync reads tell you when to switch to an export rather than silently truncating. ## Operations in this section * [Discover Connected Users](/discover-connected-users) — `getBulkConnectedOAuthUsers` * [Get Bulk Balances](/get-bulk-balances) — `getBulkBalances` * [Get Bulk Transactions](/get-bulk-transactions) — `getBulkTransactions` # Bulk gift card purchasing Source: https://docs.fluz.app/bulk-gift-card-purchasing A single `purchaseGiftCard` call buys **exactly one** gift card, for **one offer** at **one rate**. There is no `quantity` field, and a single call is never split across multiple offers or rates. To buy several cards, send the mutation multiple times — once per card, each with its own unique `idempotencyKey`. How each call chooses its offer and rate depends on whether you pass `offerId` or `merchantSlug`. ### Purchase flow at a glance ![](https://files.readme.io/bf6ad80159ab3e0beecb76d51f498a4093b793bf7368288abe62e5c81b2b36c5-diagram.svg) ### `offerId` — pin a specific offer and rate When you pass `offerId`, the purchase is locked to that exact offer and its rate. If that offer can no longer be fulfilled (for example, a stock-tracked offer that has sold out), the call **fails** — it will **not** silently substitute a different offer. See the out-of-stock error below. ### `merchantSlug` — auto-select the best available offer When you pass `merchantSlug` (without `offerId`), the system selects the **best available in-stock offer** for that merchant **at the moment of each call**. Because selection happens per call, repeated purchases for the same merchant can resolve to **different offers** as availability changes. ### Fixed vs. variable offers and stock * **Fixed-denomination offers** are stock-tracked per denomination. Once a denomination is depleted, it is no longer selectable, and any further calls pinned to it (via `offerId`) return an out-of-stock error. * **Variable offers** are not stock-limited in the same way. They typically remain available and act as the fallback when a fixed/stocked offer runs out. A variable offer often carries a **different (frequently lower) reward rate** than the fixed offer it replaces. > 📘 What happens when you buy more cards than are in stock > > Suppose a merchant's best offer is a fixed, stock-tracked offer with only **8** units left, and you want **10** cards (i.e. 10 separate `purchaseGiftCard` calls): > > * **Using** `offerId` (pinned to the fixed offer): the first 8 calls succeed; the 9th and 10th calls **fail** with an out-of-stock error. No automatic fallback to another offer or rate occurs. > * **Using** `merchantSlug` (auto-select): the first 8 calls purchase on the fixed offer; once it is depleted, the remaining calls auto-select the **next-best available offer** — which may be a **variable offer at a lower reward rate**. > > In every case each card is purchased atomically at the offer and rate resolved for that individual call — there is no blended or partially-fulfilled order. ### Protecting your rate with `minRewardRate` When you buy with `merchantSlug`, use `minRewardRate` to set a reward-rate floor. Before purchasing, the system checks the best available rate for the merchant, amount, and payment method; if that rate is **below your** `minRewardRate` (or no rate can be quoted), the call **fails** instead of buying at the lower rate. This is the recommended way to avoid unintentionally purchasing the remaining cards on a lower-rate variable offer after a higher-rate stocked offer sells out. > 🚧 `minRewardRate` only applies to `merchantSlug` purchases. > > If you provide `offerId`, `minRewardRate` is ignored (the offer — and its rate — is already fixed). The floor is evaluated per call, so include it on every call when buying multiple cards. ### Out-of-stock error When an offer can no longer be fulfilled due to stock, the mutation returns: ```json theme={null} { "code": "GC-0009", "message": "This offer is currently out of stock. Please select a different amount or try again later." } ``` Re-quote with `getOfferQuote` / `getMerchants` to find the current best available offer before retrying. > 📘 > > ### What happens when you buy more cards than are in stockSuppose a merchant's best offer is a fixed, stock-tracked offer with only **8** units left and you want **10** cards (10 separate `purchaseGiftCard` calls): > > * **Using** `offerId` (pinned to the fixed offer): the first 8 calls succeed at the stocked rate; the 9th and 10th calls **fail** with `GC-0009`. No automatic fallback to another offer or rate occurs. > * **Using** `merchantSlug` (auto-select): the first 8 calls purchase on the fixed offer at the higher rate; once it is depleted, the remaining calls auto-select the **next-best available offer** — which may be a **variable offer at a lower reward rate**. > 🚧 > > ### Protect your rate with `minRewardRate`en buying with `merchantSlug`, set `minRewardRate` to a reward-rate floor. Before each purchase the system checks the best available rate for the merchant, amount, and payment method; if that rate is **below** your `minRewardRate` (or no rate can be quoted), the call **fails** instead of buying at the lower rate. This is the recommended way to avoid unintentionally purchasing the remaining cards on a lower-rate variable offer after a higher-rate stocked offer sells out. > > `minRewardRate` is **ignored** when you provide `offerId` (the rate is already fixed), and it is evaluated **per call** — include it on every call when buying multiple cards. To check stock before you buy, see [Get Inventory On Stocked Offers](/get-inventory). # Business accounts in OAuth Source: https://docs.fluz.app/business-accounts-in-o-auth Let a user authorize your app against a business account — or pre-approve permissions for a business they are about to register — and understand which account an authorization code actually resolves to. A Fluz user can hold a personal account and one or more business accounts. Until now the OAuth flow always resolved to the personal account. It no longer does. If your application serves businesses, this page is the difference between an integration that works and one that quietly attaches every grant to the wrong account. > [!NOTE] > > Nothing here changes the authorize URL, the callback contract, or the token exchange. What changes is **which account the returned code is bound to**, and **which permission list was used to build the consent screen**. *** ## The two flows | Flow | When it runs | What the grant is against | | :- | :- | :- | | **Standard authorization** | The user selects a personal or an existing business account | That account | | **Business registration** | The user chooses to apply for a new business account, or your app is business-only and they don't have one yet | Their personal account, plus a set of permissions pre-approved for the business that will be created shortly afterwards | The second flow exists because the business doesn't exist yet at the moment of consent. The user grants permission to register a business on their personal account and, in the same step, pre-approves the business permissions your app will need once that business exists. Those pre-approved permissions are held against the user until the business is created. > [!NOTE] > > A user can run the business-registration flow more than once from the same personal account, pre-approving permissions for several businesses over time. *** ## Enabling business accounts on your app The two permission lists are on the **Permissions** tab of your app, and you edit them yourself. See [Configure OAuth App](/create-an-o-auth-app). Whether the flow is allowed to settle on a personal account is not on that tab — Fluz configures it on your application. **1. Fill in the Business permissions list** Edited independently of the consumer **Permissions** list. **A non-empty business list is what makes your app business-enabled** — leave it empty and users are never offered the option to apply for a business account. Permission to register a business is not available on this list. It's granted by the personal account, so it belongs on the consumer list. **2. Enable the register-a-business permission on the consumer Permissions list** Required for the business-registration flow. Without it that flow cannot complete. **3. Ask Fluz to restrict the app to business accounts, if you need it** Fluz can configure your application so the flow never settles on a personal account. It is not self-serve — contact your Fluz account manager if your app should only ever operate on business accounts. Skip this if your app serves both consumers and businesses. Without the restriction, a user with no business account is offered their personal account alongside the option to apply for a business one, and some of them will pick the personal account. *** ## How the account gets chosen Fluz decides between skipping the picker and rendering it. Your app doesn't drive this — but you need to know what your users will hit. | Situation | What happens | | :- | :- | | `external_id` already resolves to an account from a previous authorization | Picker skipped. That account is reused, and if the earlier authorization was a business-registration flow, that flow resumes | | No business account, and the app is restricted to business accounts | Picker skipped. Straight into business registration | | No business account, personal accounts allowed | Picker shows the personal account and, if the app is business-enabled, an option to apply for a business account | | Has business account(s) | Picker shows them. The personal account is included only when personal accounts are allowed | | A business application is already in progress | The "apply for a business account" option is hidden | > [!WARNING] > > The account selection step is why you should not assume the code you receive is bound to the user's personal account. Read the account off the exchange response and persist it — do not infer it from your own records of who started the flow. *** ## Which permission list applies The consent screen is built from your app configuration, never from the authorize URL. Which of the two lists it uses depends on the flow: | Flow | Consent screen shows | | :- | :- | | Personal account | Your consumer **Permissions** | | Existing business account | Your **Business permissions** | | Business registration | Both, as two distinct groups — the consumer permissions being granted now, and the business permissions being pre-approved for the future business | In every case the list is read-only. The user accepts all of it or does not complete the authorization. Permissions are validated on our side against the list that applies to the flow. Anything outside that list is rejected, not silently dropped. *** ## Using `external_id` with businesses `external_id` is unique per application and binds to a single Fluz account on first use. With businesses in the picture, that has a consequence worth planning for: * If a user authorizes a personal account under `external_id=acct_123`, that ID is bound to the personal account. You cannot later reuse it for their business account. * Give each account you want to track its own external ID. If you model businesses separately in your own system, use your business identifier — not the owning user's. See [Managing External Reference IDs](/managing-external-reference-ids) for the general rules. *** ## Business accounts in the widget Where your integration uses the [embedded widget](/developers/widgets), a business account can be the active context for **deposit, payout, and pay-in**. Two behaviors differ from the consumer context: * **Consumer identity verification (KYC) does not apply.** A business context is verified through business verification (KYB) instead, and no consumer KYC prompt is shown. * **PIN is set at the user level.** A user who hasn't set a PIN can set one from either context, and it applies to both. *** ## Troubleshooting | Symptom | Almost always | | :- | :- | | No option to apply for a business account | **Business permissions** is empty, so the app isn't business-enabled — or the user already has a business application in progress | | Business-registration flow fails to complete | Permission to register a business isn't enabled on the consumer **Permissions** list | | `registerBusiness` returns `AUTH-0008` | The user never completed the business-registration flow. A token minted with `generateUserAccessToken` alone carries no pre-approved business permissions — see [Applicant authorization](/kyb-overview#applicant-authorization) | | Authorization rejected over an unrecognized permission | Something was sent that isn't on the list applying to that flow. Personal-account authorizations are checked against **Permissions**, business ones against **Business permissions** | | Reusing an `external_id` fails | It's already bound to a different account on this app | | The grant landed on a personal account when you expected a business | The user picked the personal account. Ask your Fluz account manager to restrict the app to business accounts if it should never accept one | *** ## Next steps > **[Client-facing grant flow](/client-facing-o-auth-grant-flow)** > > The authorize URL, `state`, and the callback. > **[Configure OAuth app](/create-an-o-auth-app)** > > Where the two permission lists live. > **[External reference IDs](/managing-external-reference-ids)** > > Choosing identifiers you won't regret. > **[Exchange an authorization code](/exchange-an-o-auth-authorization-code)** > > Turn the code into tokens. # Business categories Source: https://docs.fluz.app/business-categories Fetch the business category and sub-category IDs required by registerBusiness. ## Overview The **getBusinessCategories** query returns every active business category with its sub-categories. Both IDs are required by [registerBusiness](/business-registration), and a sub-category is only valid alongside the category it belongs to. Let the user pick a category and one of that category's sub-categories in your UI, then pass both UUIDs through. > [!WARNING] > > **Do not hardcode these UUIDs.** Only currently-active categories are returned, the list differs between environments, and it changes over time. Fetch at runtime — a sub-category from a different category is rejected at registration with `BS-0006`. ## Required scopes | Property | Value | | - | - | | Endpoint | GraphQL API | | Authentication | OAuth Bearer Token, account type `CONSUMER` | | Required Scopes | `REGISTER_BUSINESS` | Use the same consumer token you will use for `registerBusiness`. A business-account token is rejected, and Basic authentication is not accepted. ## Basic query structure ```graphql theme={null} query GetBusinessCategories { getBusinessCategories { businessCategoryId categoryName categoryDescription businessSubCategories { businessSubCategoryId subCategoryName } } } ``` This query takes no arguments. ## Response details | Field | Type | Description | | - | - | - | | `businessCategoryId` | `UUID` | Category ID — pass as `businessCategoryId` on `registerBusiness` | | `categoryName` | `String` | Category display name | | `categoryDescription` | `String` | Category description. May be `null` | | `businessSubCategories` | `[BusinessSubCategory]` | Sub-categories that belong to this category | ### BusinessSubCategory | Field | Type | Description | | - | - | - | | `businessSubCategoryId` | `UUID` | Sub-category ID — pass as `businessSubCategoryId` on `registerBusiness` | | `subCategoryName` | `String` | Sub-category display name | Categories are returned sorted by `categoryName`, ascending. ## cURL Example ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "query GetBusinessCategories { getBusinessCategories { businessCategoryId categoryName categoryDescription businessSubCategories { businessSubCategoryId subCategoryName } } }" }' ``` ## Example Response ```json theme={null} { "data": { "getBusinessCategories": [ { "businessCategoryId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "categoryName": "Technology", "categoryDescription": "Software and IT services", "businessSubCategories": [ { "businessSubCategoryId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "subCategoryName": "SaaS" } ] } ] } } ``` ## Error Codes This query returns all failures in the top-level `errors` array — there is no `success: false` payload. | Code | Name | Description | How to resolve | | - | - | - | - | | `AUTH-0002` | InvalidCredentials | Basic auth was used, or the token's account type is not `CONSUMER` | Use a consumer-account Bearer token | | `AUTH-0031` | InvalidScope | The token is missing the `REGISTER_BUSINESS` scope | Add the scope in [Application Scopes](/fluz-dashboard/application-scopes) and re-mint the token | | `G-0001` | InternalError | Internal error while reading categories | Retry once. If it persists, contact support with the timestamp | ## Notes * Categories a user selects here are read by a human reviewer during KYB, so surface `categoryDescription` in your UI where it helps them choose accurately. * Sending one ID without the other is rejected at registration with `ARG-0002`. Always pass both. ## Related pages > **[KYB overview](/kyb-overview)** > > Prerequisites and the end-to-end registration flow. > **[Register a business](/business-registration)** > > The mutation that consumes these IDs. # Register a business Source: https://docs.fluz.app/business-registration Submit the legal entity record and beneficial ownership roster with the registerBusiness mutation, and open a KYB case. ## Overview The **registerBusiness** mutation creates a business account on the Fluz platform. In a single call it: 1. Creates a business account tied to the applicant — the Fluz user whose token you submit with, 2. Stores the legal entity record — legal name, structure, tax ID, state of incorporation, legal address, category, and intended use of the account, 3. Stores the beneficial ownership information for each owner you supply, 4. Opens a **KYB** case for compliance review, and 5. Links the new business account to your application's OAuth grant. The mutation returns an `accountId` immediately, with a `kybStatus` of `SUBMITTED`. > [!NOTE] > > This page is the parameter and error reference for the mutation itself. For prerequisites, the surrounding sequence of calls, and how to track a case to a decision, read [Register & Verify Businesses](/kyb-overview) first. ## Required scopes | Property | Value | | - | - | | Endpoint | GraphQL API | | Authentication | OAuth Bearer Token, account type `CONSUMER` | | Required Scopes | `REGISTER_BUSINESS` | A business-account token is rejected by the schema before the mutation runs. Basic authentication is not accepted. ## Basic mutation structure ```graphql theme={null} mutation RegisterBusiness($input: RegisterBusinessInput!) { registerBusiness(input: $input) { accountId kybStatus success error { message code } } } ``` ## Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `businessName` | `String` | Yes | The legal business name, exactly as it appears on the entity's formation documents and tax filings. Max 255 characters | | `dbaName` | `String` | No | The "Doing Business As" name. Max 255 characters | | `businessStructure` | `BusinessStructure` | Yes | Must be one of: `LLC`, `CORPORATION`, `PARTNERSHIP`, `SOLE_PROPRIETORSHIP`, `COOP` | | `businessLegalAddress` | `BusinessLegalAddress` | Yes | The business legal address object (see below). Verified against an address-validation provider | | `stateOfIncorporation` | `String` | Yes | The US state where the business is incorporated, as the full state name (e.g. `Delaware`) | | `taxId` | `String` | Yes | Tax ID / EIN Number. Format: `XX-XXXXXXX` (2 digits, hyphen, 7 digits) | | `authorizedSignerDocumentUrl` | `String` | Conditional | Required when the applicant owns less than 25% **and** is not the control person. [Upload the document](/submit-business-documents) first | | `soleProprietorshipDocumentUrl` | `String` | No | Optional supporting documentation for a sole proprietorship. No longer required for `SOLE_PROPRIETORSHIP` | | `businessCategoryId` | `UUID` | Yes | Business category ID (from **getBusinessCategories** query) | | `businessSubCategoryId` | `UUID` | Yes | Business sub-category ID (from **getBusinessCategories** query). Must belong to the supplied category | | `natureOfBusiness` | `String` | No | Brief description of the nature of business. Strongly recommended — it speeds up manual review. Max 255 characters | | `websiteUrl` | `String` | No | The business website URL. Strongly recommended for online sellers. Max 255 characters | | `businessAccountUsage` | `[BusinessAccountUsage]` | Conditional | Array of usage types. Required if `businessAccountUsageOther` is not provided | | `businessAccountUsageOther` | `String` | Conditional | Required if `businessAccountUsage` is empty or not provided. Max 255 characters | | `externalReferenceId` | `String` | No | Your own identifier for this business. Stored on the business OAuth grant and returned on the `KYB_STATUS_UPDATE` webhook. Max 255 characters | | `confirm` | `RegisterBusinessConfirm` | Yes | Beneficial-ownership certifications (see below) | | `owners` | `[BusinessOwner]` | Yes | List of business owners. At least one required. Exactly one must be the applicant, and exactly one must be the control person | > [!NOTE] > > **Address formatting** > > Format `businessLegalAddress` and each owner `address` using the structured fields below, with a real, deliverable address and a consistent city / state / postal code. Both may be international (country name, ISO 3166; some countries are restricted, e.g. Russia or Iran). The **business legal address is additionally verified against an address-validation provider** and rejected with `BS-0002` if it cannot be confirmed; owner addresses receive field and state/province checks only. See [Address Formatting Requirements](/concepts/address-formatting-requirements). ### BusinessLegalAddress | Field | Type | Required | Description | | - | - | - | - | | `streetAddressLine1` | `String` | Yes | Street address line 1. Max 255 characters | | `streetAddressLine2` | `String` | No | Street address line 2 | | `city` | `String` | Yes | City | | `state` | `String` | Yes | For the US, the state name (`New York`) or its two-letter code (`NY`). Elsewhere, the ISO 3166-2 subdivision name (`Ontario`) | | `postalCode` | `String` | Yes | Postal code | | `country` | `String` | Yes | Country name (ISO 3166). `US` is normalized to `United States`. Country codes are not accepted, and restricted countries are rejected | The stored address is the provider's normalized version of what you sent, with `city`, `state`, and `postalCode` upper-cased — not the exact strings you submitted. ### RegisterBusinessConfirm | Field | Type | Required | Set to `true` when | | - | - | - | - | | `allOwnersWith25PercentageOwnershipListed` | `Boolean` | Yes | At least one owner has `ownershipPercentage` of 25 or more | | `noOwnersMoreThan25Percentage` | `Boolean` | Yes | No owner has `ownershipPercentage` of 25 or more | Both fields are non-nullable, so send both. Only the one matching your roster is enforced — set it to `true` and the other to `false`. ### BusinessOwner | Field | Type | Required | Description | | - | - | - | - | | `firstName` | `String` | Yes | Owner's first name | | `lastName` | `String` | Yes | Owner's last name | | `emailAddress` | `String` | Conditional | At least one of `emailAddress` or `phoneNumber` is required. For the applicant, must match the email on their Fluz account | | `phoneNumber` | `String` | Conditional | Must include a country calling code and be a real, dialable number (e.g. `+14155551234`) | | `title` | `String` | Yes | Owner's title in the company | | `ownershipPercentage` | `Int` | Yes | Percentage of ownership (0–100). Total across all owners cannot exceed 100 | | `isControlPerson` | `Boolean` | Yes | Exactly one owner in the roster must be `true` | | `isInvited` | `Boolean` | Yes | When `true`, Fluz emails the owner and they complete verification themselves. The applicant cannot be invited | | `isUsPerson` | `Boolean` | Yes | Required on **every** owner. Selects the SSN path (`true`) or the document path (`false`) | | `lastFourSsnDigits` | `String` | Conditional | Exactly 4 digits. Required when `isUsPerson` is `true` for owners who need full identity data. Omit for non-US persons | | `dob` | `String` | Conditional | Date of birth as an ISO 8601 date (e.g. `1975-02-28`). Required for owners who need full identity data. Cannot be in the future; the owner must be at least 18 | | `address` | `OwnerAddress` | Conditional | Required for owners who need full identity data | > [!WARNING] > > **`isUsPerson` is required on every owner.** The GraphQL schema types it as a nullable `Boolean`, but validation rejects a missing or `null` value for *all* owners — including the applicant, invited owners, and owners below the beneficial-ownership threshold. The error reads `owner N is invalid: isUsPerson is required`. ### OwnerAddress | Field | Type | Required | Description | | - | - | - | - | | `streetAddressLine1` | `String` | Yes | Street address line 1 | | `streetAddressLine2` | `String` | No | Street address line 2 | | `city` | `String` | Yes | City | | `state` | `String` | Yes | For the US, the state name (`California`) or its two-letter code (`CA`). Elsewhere, the ISO 3166-2 subdivision name | | `postalCode` | `String` | Yes | Postal code | | `country` | `String` | Yes | Country name (ISO 3166). Restricted countries are rejected | ### BusinessStructure (enum) | Value | Notes | | - | - | | `LLC` | Single- or multi-member | | `CORPORATION` | C-corp or S-corp | | `PARTNERSHIP` | General or limited | | `SOLE_PROPRIETORSHIP` | Exactly one owner, 100% ownership, who must be the control person | | `COOP` | Cooperative | Any structure not on this list is rejected with `BS-0005`. Trusts, non-profits, and other entity types are handled case by case — contact your account manager before building against them. ### BusinessAccountUsage (enum) | Value | Use for | | - | - | | `CORPORATE_GIFTING` | Buying gift cards for employees, clients, or incentive programs | | `CORPORATE_SPENDING_ADMIN` | Managing employee cards and controlled spend | | `REWARDS_MAXIMIZER` | Optimizing rewards on business spend | | `GIFT_CARD_RESELLING` | Reselling gift cards | | `PURCHASE_GOODS_FOR_BUSINESS_USE` | General procurement of goods and services | | `ONLINE_SELLER_RETAIL_PURCHASING` | Sourcing inventory for online retail | Send every value that applies. If none fit, omit `businessAccountUsage` and describe the intended use in `businessAccountUsageOther`. Sending a value outside the enum returns `BS-0004`. *** ## Beneficial ownership requirements KYB review depends on getting the ownership picture right the first time. Collect and submit: * **Every individual who owns 25% or more** of the entity, directly or indirectly. * **A control person** — an individual with significant responsibility for managing the entity (CEO, CFO, managing member, general partner, or similar) — even if they hold no equity. Send them with `isControlPerson: true`. Exactly one owner must carry that flag. * **The applicant** — the user your Bearer token belongs to — matched by `emailAddress` or `phoneNumber`. Exactly one owner must match, and that owner cannot be `isInvited: true`. Practical notes: * Total `ownershipPercentage` across the array must not exceed 100, but it does **not** need to equal 100. If a business is 40/35/25 across three individuals plus a non-owner CEO, submit all four with percentages of 40, 35, 25, and 0. * Where an entity (rather than a person) holds equity, look through to the individuals behind it and submit those individuals. * Owner emails and phone numbers must be unique across the roster. * `title` is a free-text field, but it is read by a human reviewer. Use recognizable titles ("Chief Executive Officer", "Managing Member") rather than internal shorthand. ### Who needs full identity data How much you send per owner depends on their role. Every owner needs the base fields; only some need identity data on top. | Owner role | Fields required | | - | - | | The applicant | Base fields only — their identity was verified before registration | | Invited (`isInvited: true`) | Base fields only — they verify themselves after accepting | | Owns under 25% and is not the control person | Base fields only | | Owns 25% or more, or is the control person — and is neither the applicant nor invited | Base fields **plus** full identity data | **Base fields:** `firstName`, `lastName`, `title`, `ownershipPercentage`, `isControlPerson`, `isInvited`, `isUsPerson`, and at least one of `emailAddress` / `phoneNumber`. **Full identity data:** `dob` and `address`, plus `lastFourSsnDigits` when `isUsPerson` is `true`. When `isUsPerson` is `false`, omit `lastFourSsnDigits` — that owner completes document verification after registration via [requestOwnerDocumentVerificationLink](/owner-verification-link). ## Validation quick reference Most errors come down to formatting. Check these before submitting: | Field | Rule | | - | - | | `taxId` | `XX-XXXXXXX` — 2 digits, hyphen, 7 digits. Strip any other punctuation | | `stateOfIncorporation` | Full US state name. A two-letter code is rejected here, even though address `state` allows one | | `owners[].isUsPerson` | Required on every owner. `true` or `false`, never `null` | | `owners[].dob` | ISO 8601 date, e.g. `1975-02-28`. Not in the future; owner at least 18 | | `owners[].phoneNumber` | Must include a country calling code and be a real number, e.g. `+14155551234` | | `owners[].lastFourSsnDigits` | Exactly 4 digits, as a string. Only for `isUsPerson: true` owners needing full identity data | | `owners[].ownershipPercentage` | Integer 0–100; sum across owners must not exceed 100 | | `owners[].isControlPerson` | Exactly one owner is `true` | | `businessLegalAddress` | A real, deliverable address — it is checked against an address-validation provider | | `businessLegalAddress.country` | ISO 3166 country name; restricted countries are rejected | | `businessSubCategoryId` | Must belong to the supplied `businessCategoryId`; send both or neither | | `businessAccountUsage` / `...UsageOther` | At least one of the two must be populated | | `confirm` | Both booleans present; the one matching your roster set to `true` | | Document URLs | Absolute `http`/`https` URLs, max 255 characters | > [!WARNING] > > **Two formats changed from earlier versions of this API.** Owner `dob` is now an **ISO 8601** date (`1975-02-28`), not `MM/DD/YYYY`. Owner `phoneNumber` now **requires a country calling code** — a bare 10-digit US number is rejected. Note also that `+15551234567` fails because `555` is not an assigned US area code; use a real area code in test data. ## Documents An authorized-signer document is required when the applicant is not a beneficial owner or control person, and any structure may be asked for additional documentation during KYB review. Both paths are covered on one page: > **[Submit business documents](/submit-business-documents)** > > Upload endpoint, accepted documents, and what to do when compliance requests more information. ## Response details | Field | Type | Description | | - | - | - | | `accountId` | `UUID` | The account ID of the newly created business | | `kybStatus` | `String` | The KYB status of the business — `SUBMITTED` on success | | `success` | `Boolean` | Indicates whether the registration succeeded | | `error` | `RegisterBusinessError` | Error information if registration failed | ### RegisterBusinessError | Field | Type | Description | | - | - | - | | `message` | `String` | Brief informational error message | | `code` | `String` | Predefined code listed in the table below | > [!NOTE] > > Validation and business-rule failures are returned **inside the response payload**, not as top-level GraphQL errors. Branch on the presence of `error` (or on `success === false`) rather than relying on the HTTP status. > > Permission failures are the exception: a wrong scope, a wrong account type, or Basic auth is rejected before the resolver runs and appears in the top-level `errors` array with `data.registerBusiness` set to `null`. > [!WARNING] > > **OAuth linkage can fail without failing the mutation.** After the business is created and submitted, the API links the new business account to your application's OAuth grant. If that step fails it is logged server-side only — the mutation still returns `success: true` with the `accountId`, but the business grant, and any `externalReferenceId`, may be missing. Contact support with the `accountId` rather than resubmitting; a retry is blocked by `BS-0007`. ## cURL Example ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation RegisterBusiness($input: RegisterBusinessInput!) { registerBusiness(input: $input) { accountId kybStatus success error { message code } } }", "variables": { "input": { "businessName": "Acme Corporation", "dbaName": "Acme Co", "businessStructure": "LLC", "businessLegalAddress": { "streetAddressLine1": "123 Main Street", "streetAddressLine2": "Suite 100", "city": "San Francisco", "state": "California", "postalCode": "94102", "country": "United States" }, "stateOfIncorporation": "California", "taxId": "12-3456789", "businessCategoryId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "businessSubCategoryId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "natureOfBusiness": "E-commerce retail", "websiteUrl": "https://acme.example.com", "businessAccountUsage": ["CORPORATE_SPENDING_ADMIN", "PURCHASE_GOODS_FOR_BUSINESS_USE"], "externalReferenceId": "your-partner-user-id-123", "confirm": { "allOwnersWith25PercentageOwnershipListed": true, "noOwnersMoreThan25Percentage": false }, "owners": [ { "firstName": "John", "lastName": "Doe", "emailAddress": "john@example.com", "phoneNumber": "+14155551234", "title": "Chief Executive Officer", "ownershipPercentage": 60, "isControlPerson": true, "isInvited": false, "isUsPerson": true }, { "firstName": "Jane", "lastName": "Smith", "emailAddress": "jane@example.com", "phoneNumber": "+14155555678", "title": "Chief Financial Officer", "ownershipPercentage": 40, "isControlPerson": false, "isInvited": false, "isUsPerson": true, "lastFourSsnDigits": "5678", "dob": "1980-07-15", "address": { "streetAddressLine1": "789 Pine Street", "city": "San Francisco", "state": "California", "postalCode": "94104", "country": "United States" } } ] } } }' ``` John is the applicant and the control person, so he needs base fields only — his identity was verified before registration. Jane is a beneficial owner who is neither the applicant nor invited, so she needs full identity data. ## Example Response ### Success ```json theme={null} { "data": { "registerBusiness": { "success": true, "accountId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "kybStatus": "SUBMITTED", "error": null } } } ``` ### Error ```json theme={null} { "data": { "registerBusiness": { "success": false, "accountId": null, "kybStatus": null, "error": { "message": "The entered tax id taxId must be in format XX-XXXXXXX (2 digits, hyphen, 7 digits)", "code": "BS-0001" } } } } ``` ## Error Codes Returned inside the payload, with `success: false`: | Code | Name | Description | How to resolve | | - | - | - | - | | `BS-0001` | InvalidTaxId | The tax ID must be in format `XX-XXXXXXX` (2 digits, hyphen, 7 digits) | Reformat the EIN. Strip spaces and any punctuation other than the single hyphen | | `BS-0002` | InvalidBusinessLegalAddress | Business legal address is incorrect, restricted, or failed address verification | Validate against [Address Formatting Requirements](/concepts/address-formatting-requirements). Check the postal code matches the city and state | | `BS-0004` | InvalidBusinessAccountUsage | Selected business account usage is not allowed | Send only values from the `BusinessAccountUsage` enum, or describe the use in `businessAccountUsageOther` | | `BS-0005` | InvalidBusinessStructure | Selected business structure is not allowed | Use one of the five supported enum values, or contact your account manager for other entity types | | `BS-0006` | InvalidBusinessCategory | Category and sub-category are not valid or not properly connected | Re-fetch [getBusinessCategories](/business-categories) and pass a sub-category from the same category | | `BS-0007` | RegistrationNotAllowed | The user already has an open application | Wait for approval or decline before resubmitting | | `ARG-0001` | InvalidArguments | A field failed validation. The message names the field — owner roster problems, missing `isUsPerson`, an invalid phone number, ownership over 100%, a missing authorized-signer document, an underage or future `dob`, duplicate owner contacts, or an unverified applicant | Read `message` for the field name | | `ARG-0002` | MissingArguments | A required field is missing | Read `message` for the field name | | `AUTH-0008` | InvalidUserAccess | The applicant hasn't pre-approved every business scope your application requires — most often because their authorization was only ever created with `generateUserAccessToken`, which skips the consent step — or the `externalReferenceId` already belongs to another business | Send the user through the [business-registration flow](/kyb-overview#applicant-authorization), or use a unique reference | | `AUTH-0025` | UnsuccessfulRegistration | Submission to KYB screening failed. The local records are rolled back | Resubmit the same application | | `G-0001` | InternalError | Account provisioning or tax ID tokenization failed | Retry once. If it persists, contact support with the timestamp | Returned in the top-level `errors` array, with `data.registerBusiness` set to `null`: | Code | Name | Description | | - | - | - | | `AUTH-0002` | InvalidCredentials | Basic auth was used, or the token's account type is not `CONSUMER` | | `AUTH-0031` | InvalidScope | The token is missing the `REGISTER_BUSINESS` scope | > [!NOTE] > > Owner-roster problems are reported as `ARG-0001`, not `BS-0003`. `BS-0003` (InvalidOwnerInformation) exists in the shared error catalogue but this API does not return it. ## Testing in staging * Register against the staging GraphQL endpoint shown in the examples above. See [Staging vs. Live Environment](/docs/staging-vs-live-environment). * Use [Test Addresses](/docs/test-addresses) for addresses that pass validation deterministically — the legal address goes through a real address-validation provider, so invented streets will fail. * EINs in staging must still satisfy the `XX-XXXXXXX` format, but do not need to correspond to a real entity. * Owner phone numbers must be *possible* numbers for their country. `+15551234567` fails because `555` is not an assigned US area code; use a real area code with a `555` exchange, e.g. `+14155551234`. * Because a user cannot hold two open applications (`BS-0007`), test repeated registration paths with distinct test users. ## Best practices * **Validate client-side first.** Nearly every error code is a formatting or selection problem you can catch before the network call. Doing so materially improves onboarding completion rates. * **Expect one error at a time.** Validation stops at the first problem it finds and the legal address is checked early, so a rejected submission may have more than one thing wrong with it. * **Fetch categories at runtime.** Never hardcode category UUIDs. * **Send `isUsPerson` on every owner.** It is the single most common cause of a rejected roster. * **Do not auto-retry on a declined KYB.** Resubmission will not change the outcome and creates duplicate cases. * **Store the `accountId` immediately.** It is your only handle on the application and the reference support will ask for. * **Communicate the pending state honestly.** Tell the user their business is under review and roughly how long it takes, rather than dropping them into a business dashboard that cannot yet transact. * **Collect ownership completely the first time.** Missing beneficial owners is the most common cause of a review stalling for additional documentation. ## Notes * Either `businessAccountUsage` or `businessAccountUsageOther` must be provided. * Users cannot register a new business while they already have an ongoing application. * There is no idempotency key. Duplicate submissions are blocked by the open-application check instead. * Validation runs before anything is written, and the entity, application, charter, and address records are created in a single transaction — a rejected submission leaves nothing behind. * The `taxId` you submit is tokenized before storage and is never returned by any read operation. ## Related pages > **[KYB overview](/kyb-overview)** > > Prerequisites, the end-to-end flow, and how to track a case to a decision. > **[Business categories](/business-categories)** > > Fetch the category and sub-category IDs required by this mutation. > **[Submit business documents](/submit-business-documents)** > > Upload authorization documents and respond to KYB documentation requests. > **[Business KYB status](/business-status)** > > Read KYB status and per-owner verification progress after submitting. > **[Owner verification link](/owner-verification-link)** > > Generate an identity-verification link for owners on the document path. > **[Address formatting requirements](/concepts/address-formatting-requirements)** > > Rules that govern the legal and owner address objects. # Cancel a bulk operation Source: https://docs.fluz.app/cancel-a-bulk-operation > Stop a bulk job that hasn't finished. Items not yet started are cancelled; items already running are allowed to finish. `cancelBulkOperation` stops a job you [submitted](/submit-a-bulk-operation). It is **best-effort and irreversible**: items still `QUEUED` are cancelled, while items already `RUNNING` are allowed to finish and record their results. It does not roll back work that has already completed. > [!NOTE] > > Available in `staging` only at this point. ## Requirements * `Authorization: Basic ` * The bulk API capability on your application. * The job must belong to your application — an unknown or other-app job id returns not-found. ## Mutation ```graphql theme={null} mutation CancelBulkOperation($bulkJobId: UUID!) { cancelBulkOperation(bulkJobId: $bulkJobId) { jobId status succeededItemCount failedItemCount skippedItemCount } } ``` ```json theme={null} { "bulkJobId": "9c1e6f2a-1d4b-4a2e-8f0c-2b7e5a9d1234" } ``` ## Response ```json theme={null} { "data": { "cancelBulkOperation": { "jobId": "9c1e6f2a-1d4b-4a2e-8f0c-2b7e5a9d1234", "status": "CANCELLED", "succeededItemCount": 4, "failedItemCount": 0, "skippedItemCount": 0 } } } ``` ## Arguments | Parameter | Type | Description | | - | - | - | | `bulkJobId` | `UUID!` | The job id returned by `submitBulkOperation`. | ## Response fields | Field | Description | | - | - | | `status` | `CANCELLED` once the job has stopped accepting new work. Items that had already succeeded or failed keep their outcome. | | `succeededItemCount` / `failedItemCount` / `skippedItemCount` | Final per-item counters, including any items that finished after the cancel call landed. | ## Errors | Error | HTTP | When | | - | - | - | | `BulkApiAccessDenied` | 403 | Your application does not have the bulk API capability. | | `BulkJobNotFound` | 404 | The job id is unknown **or** belongs to another application (deliberately indistinguishable). | Cancelling an already-finished or already-cancelled job is **not** an error — it returns the job unchanged (see below). > [!NOTE] > > Cancel is **idempotent and safe to retry**. Calling it on a job that is already finished (`COMPLETED`, `COMPLETED_WITH_ERRORS`, `FAILED`) or already `CANCELLED` returns the job unchanged — it never errors and never reverses completed items. Because in-flight items are allowed to finish, poll the job with [Track a Bulk Job](/track-a-bulk-job) if you need the settled counts after cancelling. # Checkout Source: https://docs.fluz.app/checkout Email sales to request access to our BETA program of Payment Processing API connectors. # Client-facing OAuth grant flow Source: https://docs.fluz.app/client-facing-o-auth-grant-flow Send a user to Fluz to approve your scopes, and handle the authorization code that comes back — including the state validation that makes the flow safe. This is the step where a real person decides whether to let your application touch their Fluz account. You redirect them to Fluz, they approve, and Fluz redirects them back to you with a short-lived authorization code. Everything before this page is configuration. Everything after it is token handling. This is the only step your user sees. > [!NOTE] > > **Every user goes through this step — including users you created.** Registering someone with `registerUser` doesn't grant your application any access. Until that user completes this flow, calls on their behalf fail with `AUTH-0031`. See [Register Customers](/user-registration). ## Where this sits ```mermaid theme={null} sequenceDiagram participant U as User participant Y as Your server participant F as Fluz U->>Y: Clicks "Connect Fluz" Y->>Y: Generate and store `state` Y->>U: 302 to /authorize U->>F: Signs in (SMS one-time code) F->>U: Account selection (when applicable) U->>F: Picks an account F->>U: Consent screen — the permissions your app is configured for U->>F: Accepts F->>U: 302 to your redirect_uri (code, state) U->>Y: Hits your callback Y->>Y: Validate `state` Y->>F: POST /token/exchange (code) F->>Y: accessToken, refreshToken, scope ``` > [!NOTE] > > Note what Fluz absorbs in the middle of that diagram: account creation, sign-in, two-factor authentication, and identity verification if the user hasn't been verified yet. You don't build any of it, and you never see the credentials. ## Before you start **1. Your app is configured** Permissions selected on the **Permissions** tab, redirect URI registered on the **OAuth** tab, `client_id` and `client_secret` in your secret store. See [Configure OAuth App](/configure-o-auth-app). **2. Your app is an active OAuth Integration application** Applications created with **Create new app** in the developer portal can only call the API as you, the developer — they can't run this flow. You need an **OAuth Integration** application that Fluz has moved to `ACTIVE`. See [Prepare Your Accounts](/get-started/prepare-accounts). **3. Your callback route exists and can reach your session store** It needs to read `state` and compare it against something you persisted before the redirect. **4. Your Permissions tab holds exactly the scopes this flow needs** The consent screen is built from your app configuration, not from the authorize request. Whatever is selected on the **Permissions** tab is what the user is asked to approve, so trim it before you send anyone through — this is your highest-drop-off step and its length comes entirely from that tab. See [Application Scopes](/application-scopes) for the catalog. *** ## Step 1 — Build the authorize URL Direct the user to the `/authorize` endpoint with the following query parameters. | Parameter | Required | Description | | :- | :- | :- | | `response_type` | Yes | Always `code` for a permissions request. | | `client_id` | Yes | Your app's OAuth client ID, from the **Overview** tab. | | `redirect_uri` | Yes | A URI already registered on your app. Must match exactly — scheme, host, path, and trailing slash — and be reused byte-for-byte at exchange. | | `state` | Technically optional — **treat it as required** | An unguessable value returned to you unmodified. See [Step 2](#step-2-%E2%80%94-protect-the-flow-with-state). | | `external_id` | No | Your stable identifier for the end user. Bound to the Fluz account on first authorization and reused afterwards to resolve the same account and skip account selection. Must be unique per application — reusing an ID already linked to a different Fluz account fails the request. | > [!NOTE] > > **There is no `scopes` parameter.** Earlier versions of this flow accepted a `scopes` query parameter and intersected it with your app configuration. That parameter is now ignored: the permissions shown on the consent screen are derived entirely from your app's **Permissions** tab. Removing `scopes` from an existing authorize URL produces an identical consent screen — you can drop it whenever convenient. ### Environments | Environment | Authorize endpoint | | :- | :- | | Staging | `https://uni.staging.fluzapp.com/authorize` | | Production | Issued during production onboarding — see [Deploying to Production](/deploying-to-production) | ### Encoding rules URL-encode the `redirect_uri` and `external_id` values. Build the query string with your language's URL encoder rather than string concatenation and this takes care of itself. A complete staging example: ```text theme={null} https://uni.staging.fluzapp.com/authorize?response_type=code&client_id=dab5c80e-0321-4c3a-988a-ffedfd64d8db&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Ffinalize&state=8f14e45fceea167a5a36dedd4bea2543&external_id=user1 ``` ```javascript Node.js theme={null} import crypto from 'crypto'; const AUTHORIZE_URL = 'https://uni.staging.fluzapp.com/authorize'; const REDIRECT_URI = 'https://app.example.com/oauth/finalize'; // one canonical constant export function buildAuthorizeUrl(session) { const state = crypto.randomBytes(32).toString('hex'); session.oauthState = state; // persist server-side, bound to this session const url = new URL(AUTHORIZE_URL); url.searchParams.set('response_type', 'code'); url.searchParams.set('client_id', process.env.FLUZ_CLIENT_ID); url.searchParams.set('redirect_uri', REDIRECT_URI); url.searchParams.set('state', state); url.searchParams.set('external_id', session.userId) return url.toString(); } ``` ```python Python theme={null} import os import secrets from urllib.parse import urlencode AUTHORIZE_URL = "https://uni.staging.fluzapp.com/authorize" REDIRECT_URI = "https://app.example.com/oauth/finalize" # one canonical constant def build_authorize_url(session): state = secrets.token_hex(32) session["oauth_state"] = state # persist server-side, bound to this session params = { "response_type": "code", "client_id": os.environ["FLUZ_CLIENT_ID"], "redirect_uri": REDIRECT_URI, "state": state, "external_id": session["userId"] } return f"{AUTHORIZE_URL}?{urlencode(params)}" ``` ```go Go theme={null} package fluz import ( "crypto/rand" "encoding/hex" "net/url" "os" ) const ( authorizeURL = "https://uni.staging.fluzapp.com/authorize" redirectURI = "https://app.example.com/oauth/finalize" ) // BuildAuthorizeURL returns the authorize URL and the state to persist server-side. func BuildAuthorizeURL(userID string) (authURL string, state string, err error) { b := make([]byte, 32) if _, err = rand.Read(b); err != nil { return "", "", err } state = hex.EncodeToString(b) u, err := url.Parse(authorizeURL) if err != nil { return "", "", err } q := u.Query() q.Set("response_type", "code") q.Set("client_id", os.Getenv("FLUZ_CLIENT_ID")) q.Set("redirect_uri", redirectURI) q.Set("state", state) q.Set("external_id", userID) u.RawQuery = q.Encode() return u.String(), state, nil } ``` ### Signing in The user signs in to Fluz before they see anything about your app. * **Users created with `registerUser`** have no password. They sign in with a **one-time code sent by SMS** to the phone number you registered. * **Existing Fluz users** sign in the way they normally do, followed by two-factor authentication. The sign-in code is an SMS code, not a TOTP code — authenticator apps such as Authy or Okta Verify can't complete it. > [!NOTE] > > **Staging:** no SMS is sent. Enter `123456` as the code for any user created with `registerUser`. ### What the user sees ![Sample OAuth permissions page](https://storage.googleapis.com/fluz-fluz-file-uploads-staging-wlfprelricuyxowb/assets/oauth-widget-permissions.png) Your app name, avatar, and description come straight from the **Overview** tab, and the permission lines are your selected scopes grouped under readable headers. If this screen looks wrong, the fix is on [Configure OAuth App](/configure-o-auth-app), not in your code. The permission list is **read-only**. The user reviews it and accepts the whole set, or they don't complete the authorization — there are no per-scope checkboxes to opt out of. Spend-account selection, where your app uses it, is a separate control and still appears. ### Account selection Before the consent screen, the user may be asked which Fluz account this authorization applies to — their personal account, an existing business account, or a new business they want to register (**Apply for a business account**). Which of those happens depends on the app's configuration and on what accounts the user already has. For a consumer-only integration nothing changes: the user has one account, the picker is skipped, and the flow looks exactly as it always did. If you send `external_id` and it already resolves to an account from a previous authorization, the picker is skipped too. If your app is business-enabled, read [Business Accounts in OAuth](/business-accounts-in-o-auth) before you build — the authorize URL is the same, but the account the code resolves to may not be the user's personal one. *** ## Step 2 — Protect the flow with `state` The reference table calls `state` optional. In a redirect-based authorization flow it is your only defense against having someone else's authorization code planted in your user's session, so build it in from the first commit rather than adding it later. **1. Generate an unguessable value** At least 128 bits from a cryptographically secure source. Not a timestamp, not a user ID, not a counter. **2. Store it server-side, bound to the browser session** Session store, signed cookie, or short-TTL cache keyed to the session. Not in a global. **3. Compare on the way back, and reject on mismatch** Missing, unrecognized, or already-used `state` means abandon the request — do not exchange the code. Use a constant-time comparison. **4. Consume it** Delete it after a successful match so the same callback can't be replayed. > [!NOTE] > > `state` travels through the user's browser. It's fine to use it to carry a lookup key — which user, which flow, which page to return to — but never put anything sensitive or trusted in the value itself. *** ## Step 3 — Handle the callback On approval, Fluz redirects the user to your `redirect_uri` with: | Parameter | Description | | :- | :- | | `code` | The authorization code tied to this user's scope grant. Single-use, short-lived. Exchange it server-side. | | `state` | The exact value you sent, unmodified. | If the request was misconfigured, the redirect carries an error message describing what didn't match. ```javascript Node.js theme={null} import crypto from 'crypto'; app.get('/oauth/finalize', async (req, res) => { const { code, state } = req.query; const expected = req.session.oauthState; delete req.session.oauthState; // single use, regardless of outcome if (!code || !state || !expected) { return res.status(400).send('Incomplete authorization response'); } const a = Buffer.from(String(state)); const b = Buffer.from(String(expected)); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(400).send('State mismatch — request abandoned'); } // Exchange server-side. Never send the code back to the browser. const tokens = await exchangeAuthorizationCode(code); await persistTokens(req.session.userId, tokens); // Trust the returned scope array, not what you requested. res.redirect('/settings/connections'); }); ``` ```python Python theme={null} import hmac from flask import request, session, abort, redirect @app.route("/oauth/finalize") def oauth_finalize(): code = request.args.get("code") state = request.args.get("state") expected = session.pop("oauth_state", None) # single use if not code or not state or not expected: abort(400, "Incomplete authorization response") if not hmac.compare_digest(state, expected): abort(400, "State mismatch - request abandoned") # Exchange server-side. Never send the code back to the browser. tokens = exchange_authorization_code(code) persist_tokens(session["user_id"], tokens) # Trust the returned scope array, not what you requested. return redirect("/settings/connections") ``` > [!WARNING] > > Exchange the code **immediately, once, from your server**. It is single-use and short-lived. Make your callback route idempotent — a user refreshing the page, a link prefetcher, or a browser retry will hit it twice, and the second attempt must not corrupt state or surface an error to the user who already succeeded. Next: [Exchange an OAuth authorization code](/exchange-an-o-auth-authorization-code). *** ## Step 4 — Reconcile what you actually got The exchange response includes the scope array the user approved. **That array, not your assumptions, is the truth about what your integration can do.** Consent is now all-or-nothing, so the common cause of a missing scope is no longer a user opting out — it's that the scope isn't enabled on your app's Permissions tab, in which case it was never offered. The flow still completes successfully and your API calls fail later. A grant issued before you widened the Permissions tab also won't carry the newer scopes until that user re-authorizes. Read the returned scopes, persist them alongside the tokens, and gate your features on them. If something essential is missing, tell the user plainly and offer to re-run the flow. *** ## Designing the moment The consent screen converts far better when the user understands why they're seeing it. * **Explain before you redirect.** One sentence on your own page — "Connect your Fluz account so we can send your payouts" — outperforms dropping someone cold onto a permissions screen. * **Trigger it in context.** At the point of first payout or first card, not buried in account settings. * **Full-page redirect over a popup.** Popups get blocked, and the flow includes 2FA and possibly identity verification, which is uncomfortable in a small window. If you need to stay in-page, use an [embedded widget](/developers/widgets) instead, which is built for exactly that. * **Handle the return trip.** Land the user where they were, with the thing they were trying to do now working. `state` is how you know where that was. * **Have a re-authorization path.** Refresh tokens expire and users revoke access. Build the "reconnect" flow at the same time as the connect flow, not after the first support ticket. * **Consider skipping it.** If your users don't already have Fluz accounts, a widget handles registration, verification, and consent in one hosted flow with no redirect. See [Embedded Widgets](/developers/widgets). *** ## Troubleshooting | Symptom | Almost always | | :- | :- | | `/authorize` errors instead of rendering consent | `redirect_uri` isn't registered on the app (it must match exactly, including the trailing slash), or `client_id` is from a different app or environment | | `AUTH-0030 … current status: PERSONAL` on API calls | The app was created with **Create new app** and can't run OAuth. Use an OAuth Integration application that Fluz has activated | | User created with `registerUser` can't get past sign-in | They're using an authenticator app. The code is sent by SMS to the registered phone; in staging, enter `123456` | | `AUTH-0031` when acting for a user you registered | The user hasn't completed this flow yet. Registration grants nothing | | Consent screen shows fewer permissions than needed | Those scopes aren't checked on the Permissions tab. The consent screen is built from that tab, so nothing you put in the authorize URL will add to it | | Consent screen shows more permissions than you wanted to ask for | Same cause, other direction — trim the Permissions tab | | User lands on an account picker you didn't expect | The app is business-enabled, or the user has more than one account. See [Business Accounts in OAuth](/business-accounts-in-o-auth) | | Authorization fails when reusing an `external_id` | That ID is already bound to a different Fluz account on this app. External IDs are unique per application | | Callback arrives with no `state` | It wasn't sent on the authorize request | | `state` never matches | Session isn't sticky across the redirect, or it's being stored per-process behind a load balancer | | Exchange fails right after a clean consent | `redirect_uri` at exchange doesn't byte-match the one used at authorize | | Exchange returns HTTP 500 | The token endpoints currently return every error as a generic 500 — an invalid or expired code, a bad refresh token, and wrong client credentials all look the same. Check each in turn | | Second callback hit throws an error at the user | Code already consumed — make the route idempotent | | Consent screen shows a placeholder name or blank avatar | **Overview** tab was never filled in | *** ## Next steps > **[Exchange an authorization code](/exchange-an-o-auth-authorization-code)** > > Turn the code into an access token and refresh token. > **[Refresh an access token](/refresh-o-auth-access-token)** > > Stay connected without sending the user back through consent. > **[Configure OAuth app](/configure-o-auth-app)** > > Fix anything the consent screen got wrong. > **[Embedded widgets](/developers/widgets)** > > Skip the redirect entirely with a hosted in-page flow. # Address formatting requirements Source: https://docs.fluz.app/concepts/address-formatting-requirements Addresses are submitted in several places across the Fluz API — user identity verification (KYC), business registration (KYB), funding sources, and virtual card issuance. This page defines how to format address fields so they are accepted, and the additional validation that applies specifically to card issuance. > [!TIP] > > **Where this applies** > > Address fields use the same structure across the API. The **formatting rules** below apply everywhere. The **card issuance requirements** (US-only, no PO boxes, Smarty validation) apply only where noted. ## Address fields Every address object uses the same structured fields. Depending on the call, the object may be named `billingAddress`, `businessLegalAddress`, or an owner/individual address, but the field names are consistent. | Field | Required | Format | | - | - | - | | `streetAddressLine1` | Yes | Primary street address (building number + street). Must be a real, deliverable street address. | | `streetAddressLine2` | No | Apartment, suite, unit, or floor. Use a standard secondary designator (`Apt`, `Ste`, `Unit`, `Rm`, `Fl`, `#`). | | `city` | Yes | City name. Must be the city USPS associates with the ZIP code. | | `state` | Yes | Two-letter US state code (e.g. `NY`) or full state name. | | `postalCode` | Yes | 5-digit US ZIP code. Must belong to the city and state provided. | | `country` | Yes | Country name. Constraints vary by context — see [Country by context](#country-by-context). | ## Formatting rules These apply to **every** address you submit, in any section of the API: 1. **Use the structured fields.** Put the street on `streetAddressLine1` and the unit on `streetAddressLine2` — don't combine everything into one line. 2. **Use a real, deliverable address.** The building number must exist on the named street. A real street is not enough — `500 Main St` is invalid if the highest number on Main St is `480`. 3. **Keep city, state, and ZIP consistent.** The ZIP code must be the one assigned to that city and state. A mismatched ZIP is the most common cause of a rejected address. 4. **Use USPS abbreviations.** Standard suffixes (`St`, `Ave`, `Blvd`, `Dr`, `Ln`, `Rd`, `Ct`, `Pkwy`) and directionals (`N`, `S`, `E`, `W`, `NE`, `NW`, `SE`, `SW`) match most reliably. 5. **Put unit / apartment / suite in** `streetAddressLine2` using a recognized designator. If USPS requires a secondary unit for a building and none is provided, the address can fail to validate. 6. **Send a 5-digit ZIP** for US addresses. ZIP+4 is added automatically during standardization. ## Additional requirements for card issuance Virtual card billing addresses are validated and standardized through [Smarty](https://www.smarty.com/) (USPS Delivery Point Validation) before they are saved. This applies to [Add Virtual Card Address](/features/add-billing-address), [Create Virtual Card](/features/create-card), and [card issuance for authorized users](/features/create-virtual-card-for-authorized-user). * **US addresses only.** Set `country` to `United States`. * **No PO boxes.** The card issuer does not accept PO box addresses, even if USPS considers them deliverable. * **Must be Smarty-verified.** If the address cannot be matched to a real, deliverable US delivery point, the request is rejected with `VC-0025` and **no address is saved**. Retrying with the same values will fail again — the address itself must change. Bank card billing addresses ([Add Funding Sources](/features/add-bank-card)) share the same field structure and should match the address on file with the card issuer. A mismatched billing address can cause the card to be declined during the address verification (AVS) check. ## Country by context Not every address is US-only. Use the right constraint for the call you are making: | Context | Country constraint | | - | - | | Virtual card billing address | US only (`United States`). | | Bank card billing address | Should match the card issuer's records (typically US). | | Business registration (KYB) `businessLegalAddress` | International addresses accepted (country name, ISO 3166). Some countries are restricted for KYB (e.g. Russia, Iran). | | User KYC verification | The user's residential address. | ## Why an address is rejected (`VC-0025`) For card issuance, an address fails when Smarty cannot confirm it as a deliverable US delivery point. The most common reasons: | Reason | What it means | | - | - | | City / State / ZIP mismatch | The ZIP code does not belong to the city or state provided. | | Invalid building number | The street exists, but the primary (building) number does not exist on it. | | Address not found | The street or address is not present in USPS data. | | Missing / invalid secondary unit | USPS requires an apartment or suite number for this building and it is missing or wrong. | | Non-deliverable point | The address maps to a vacant or otherwise undeliverable delivery point. | | PO box or non-US address | Rejected by the card issuer (PO boxes) or unsupported (outside the US). | ## Worked examples ### Example 1 — City / ZIP mismatch ```text theme={null} 2581 Oakwood Avenue, New York, NY 10605 ``` **Rejected (**`VC-0025`**).** ZIP code `10605` belongs to **White Plains, NY** (Westchester County), not New York City — so the city and ZIP do not match. In addition, `2581 Oakwood Avenue` is not a confirmed delivery point in that ZIP. Smarty returns no valid match, so the address is not saved. **Fix:** submit the ZIP that USPS assigns to the city (or the city that matches the ZIP), and a building number that exists on the street. ### Example 2 — Invalid building number ```text theme={null} 896 S State St, Dover, DE 19904 ``` **Rejected (**`VC-0025`**).** `S State St` exists in Dover, DE, but `896` is not a confirmed USPS delivery point for that street and ZIP. Because the specific building number cannot be validated, Smarty returns no deliverable match. **Fix:** confirm the exact building number and the ZIP that covers that block of the street. ### A valid submission ```json theme={null} { "billingAddress": { "streetAddressLine1": "1600 Amphitheatre Pkwy", "streetAddressLine2": "", "country": "United States", "city": "Mountain View", "state": "CA", "postalCode": "94043" } } ``` Saved and normalized (city, state, and postal code stored in uppercase; ZIP+4 added): ```json theme={null} { "streetAddressLine1": "1600 Amphitheatre Pkwy", "streetAddressLine2": null, "country": "United States", "city": "MOUNTAIN VIEW", "state": "CA", "postalCode": "94043" } ``` ## Handling `VC-0025` in your integration * **Collect the address in structured fields** rather than a single free-text field, so you can prompt for the specific part that needs fixing. * **Do not blind-retry.** The same address will keep returning `VC-0025`. Surface the error and ask the user to correct the address. * **Validate before you submit (optional).** Running an address autocomplete or verification step in your own UI reduces failed round-trips. * **Nothing is saved on failure**, so there is no cleanup before retrying with corrected values. ## Error reference | Error | Code | Message | | - | - | - | | `INVALID_BILLING_ADDRESS` | `VC-0025` | The billing address could not be verified. Please check the street, city, state, and ZIP code and try again. | See [Virtual Card Error Codes](/features/virtual-card-error-codes) for the full list. ## Next steps > **[Test addresses](/test-addresses)** > > Known-good and known-bad addresses for exercising every validation outcome in staging. > **[General API error codes](/get-started/general-api-error-codes)** > > Where address errors fit in the broader error code namespace. *** **Want to learn more?** Contact us at [partnerships@fluz.app](mailto:partnerships@fluz.app). Speak with our experts for more info or to request a demo. # Staging vs. live environment Source: https://docs.fluz.app/concepts/environments The two Fluz environments, what's separate between them, how to fund a staging wallet, and how to move to live. Fluz runs two fully separate environments. Knowing which one you're in, and that nothing crosses between them, avoids most first-run authentication problems. ## Staging environment > [!NOTE] > > The Fluz staging environment lets you explore the platform's features. Use it to experiment, build integrations, and train your team. **Purpose:** Staging mirrors the capabilities and transactions of the live environment, but without real money. Use it to test your integration thoroughly before moving to production. **Data usage:** Only use dummy or test data in staging, created explicitly for testing. It must not include production data such as customer details, financial data, or personally identifiable information (PII). See the [sandbox test data](/test-addresses) pages. ## Production environment > [!WARNING] > > The live environment is where real transactions occur, and actions are final. To protect your business data, don't use this environment for testing. Production data includes sensitive information such as customer details, financial data, and PII. **Purpose:** The live environment is where you make purchases, earn cashback, and manage your account. Everything here uses real data and has real-world consequences. **Data usage:** Only use real, verified data in the live environment. Actions here affect your actual account and can't be undone. ## What's separate between them Staging and live don't share anything. Each has its own: * **users and logins** — sign up separately in each; * **accounts** — personal and business accounts, balances, cards and transactions; * **applications and API keys** — a key from one environment is unknown to the other; * **OAuth grants and webhook configuration.** | | Staging (sandbox) | Live (production) | | :- | :- | :- | | Web app | `https://uni.staging.fluzapp.com` | `https://fluz.app` | | Developers area | `https://uni.staging.fluzapp.com/for-developers` | `https://fluz.app/for-developers` | | GraphQL endpoint | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | `https://transactional-graph.fluzapp.com/api/v1/graphql` | > [!WARNING] > > An application created while logged in to `fluz.app` is a **live** application, even if you only mean to test with it. Create test applications at `uni.staging.fluzapp.com`. From the live dashboard you can reach staging under **Resources → Open Staging**. A live key sent to the staging endpoint (or the reverse) fails with `401 Error verifying API basic token`. See [API credentials](/get-started/api-credentials#if-the-token-request-returns-401). Application activation and restricted capabilities, such as `registerUser` and `VERIFY_KYC`, are granted per application. That means **separately in each environment**. See [How accounts work](/get-started/accounts-and-applications#application-status). ## Funding a staging wallet A new staging account has a zero balance, so card issuing and gift card purchases fail until you add funds. **1. Find the spend account** Call `getUserCashBalances` and take the `userCashBalanceId` of the spend account you want to fund. `getWallet.cashBalance` may only reflect the default spend account. **2. Ask Fluz to seed it** Send your staging account ID, the `userCashBalanceId`, and the amount to your Fluz contact. This is the reliable way to fund staging today. **3. Or deposit from a test funding source** Deposit from a [test bank card](/test-adding-bank-cards) or [test bank account](/test-adding-bank-accounts), passing the `userCashBalanceId` from step 1. ## Moving from staging to live Before you switch, test every flow thoroughly in staging and resolve any bugs. Then: 1. sign up (or log in) at `fluz.app`; 2. recreate your application there, from the same template, and ask Fluz to activate it and enable the same capabilities; 3. swap in the live API key, OAuth client credentials, and endpoint; 4. re-register webhooks and redirect URIs. The full checklist is in [Deploying to Production](/deploying-to-production). ## Next steps > **[Prepare your accounts](/get-started/prepare-accounts)** > > Sign up in staging and create the right type of application. > **[Idempotency](/concepts/idempotency)** > > Safely retry money-moving mutations without creating duplicates, in either environment. # How the GraphQL API works Source: https://docs.fluz.app/concepts/graphql One endpoint, queries and mutations, and the response format you can expect from every call. Fluz exposes a single GraphQL endpoint per environment. There are no per-capability base URLs — you send a query or mutation to `/api/v1/graphql`, and the token decides whose account you're operating on. ## Endpoints | Environment | URL | | - | - | | Staging | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | | Live | `https://transactional-graph.fluzapp.com/api/v1/graphql` | Access tokens are minted at the same endpoint via the `generateUserAccessToken` mutation, authorized with your API Key — see [Authentication](/concepts/authentication). ## Anatomy of a request ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "query GetSpendAccount($id: UUID!) { getUserCashBalanceById(userCashBalanceId: $id) { nickname availableCashBalance } }", "variables": { "id": "9c1f6b2e-4d7a-4c3b-9f11-2a5e8b0d6c74" } }' ``` * **`query`** — the GraphQL document. Use named operations (`query GetSpendAccount`) for better logs. * **`variables`** — typed inputs, keeping values out of the query string. * **`operationName`** — optional, useful when a document defines multiple operations. ## Queries vs. mutations * **Queries** are read-only (`viewer`, `wallet`, `transactions`). * **Mutations** change state (`createVirtualCard`, `purchaseGiftCard`, `depositCashBalance`, `createTransfer`). Money-moving mutations use request de-duplication — see [Idempotency](/concepts/idempotency). ## Response format A query that runs returns HTTP 200 with a `data` / `errors` envelope, including when it fails with an application error: ```json theme={null} { "data": { "wallet": { "balance": { "value": 12500, "currency": "USD" } } }, "errors": null, "extensions": { "requestId": "req_01H..." } } ``` On failure, `data` may be `null` and `errors` will describe what went wrong — including a machine-readable `code` you can branch on. See [Errors](/api-reference/errors). A variable whose value doesn't match its declared type also returns 200, with `BAD_USER_INPUT`. Other requests rejected before they run get a different status, for example `429` from the rate limiter or `400` for a query over the [query cost limits](/concepts/rate-limits). ## Introspection and schema Introspection is enabled in staging so you can point tools like Apollo Studio or GraphiQL at the endpoint. In live, introspection is disabled; use the published schema from [API reference](/api-reference/overview) instead. ## Next steps > **[Authentication](/concepts/authentication)** > > How the token in that `Authorization` header gets minted — for your account or a customer's. > **[Errors](/api-reference/errors)** > > What the `errors` array contains and how to branch on error codes. # Idempotency requests Source: https://docs.fluz.app/concepts/idempotency An API call or operation is idempotent if it produces the same result regardless of how many times it's executed. This ensures that repeated calls do not cause unintended side effects, protecting against accidental duplicate requests. The Fluz API supports idempotency requests to safeguard against accidental duplication of operations. By using a client-generated, unique idempotency key, the API ensures that the same request is not processed multiple times within a 10-minute window if provided with the same idempotency key. ## How It Works When an idempotency key is provided with a request: * The server caches the initial response body and its status code. * If a duplicate request with the same idempotency key and response code is received, the API will return the cached result instead of re-executing the operation, especially if an error occurs within the operation. This mechanism helps prevent unintended duplicate processing by returning consistent results even in error scenarios. ## Generating Unique Idempotency Keys Clients are responsible for creating and refreshing their idempotency keys. To ensure uniqueness: * We strongly recommend using V4 UUIDs, which can be generated with libraries such as these: [https://github.com/uuidjs/uuid](https://github.com/uuidjs/uuid) * As a best practice, idempotency keys should expire after some time to avoid reuse and ensure the uniqueness of requests. Fluz expires each unique idempotency keys after 10 minutes. ### Clients will need: 1. An operation to generate and store a new idempotency key 2. An operation to remove an old key according to the client's expiration policy ## Required Operations Not all API operations require idempotency keys. Fluz requires idempotency for certain critical operations to ensure data consistency for the following operations: * `purchaseGiftCard` ([View here](/purchase-gift-card)) * `depositCashBalance` ([View here](/features/deposit-from-external-accounts)) For these operations, an `idempotencyKey` input variable is required. ## Example Input ```json theme={null} { "idempotencyKey": "2c29e5ad-bb36-4b7f-b012-b7d6031566e0", "offerId": "2c29e5ad-bb36-4b7f-b012-b7d6031566e0", "amount": 123.45, "balanceAmount": 987.65, "bankAccountId": "0285c162-fb2f-4c32-b076-29166471f570", "bankCardId": "2c29e5ad-bb36-4b7f-b012-b7d6031566e0", "paypalVaultId": "2c29e5ad-bb36-4b7f-b012-b7d6031566e0", "exclusiveRateId": "2c29e5ad-bb36-4b7f-b012-b7d6031566e0", "merchantSlug": "xyz789" } ``` > [!NOTE] > > Fluz requires an idempotency key for this operation. ## Next steps > **[Purchase Gift Card](/purchase-gift-card)** > > See the idempotency key in action on the most common money-moving mutation. > **[Address formatting](/concepts/address-formatting-requirements)** > > The other common cause of rejected requests — how to format addresses so they validate the first time. # Rate limits Source: https://docs.fluz.app/concepts/rate-limits Fluz applies protective, per-service rate limits and GraphQL query cost limits to keep the platform stable. What the limits are, how to tell a rate limit from a cost limit, and how to build clients that stay within them. Fluz doesn't sell per-plan API quotas — there are no "Free = X req/min, Pro = Y req/min" tiers and no per-key quota system. Instead, each service applies a **protective rate limit** to absorb abuse and shield its backend. Normal interactive and application traffic effectively never hits these limits; they exist to catch runaway scripts and abuse. The GraphQL API also limits how much a single query can ask for, measured in cost points. That limit is separate from the request rate and fails differently. See **Query cost limits** below. > [!NOTE] > > There are no published per-plan quotas to request an increase against. If a well-behaved integration is hitting limits, that usually points to a traffic pattern worth fixing (see [Build a well-behaved client](#build-a-well-behaved-client)) rather than a quota to raise. ## How limits are applied Limits are enforced independently **per service** and are evaluated globally across that service's instances — you can't escape a limit by landing on a different server. A request is identified by a combination of: * **Your IP address** — every request counts against a per-IP limit. * **Your access token** — the full `Authorization` header. The token limit applies on top of the IP limit, not instead of it. The IP limit is checked first, so every request from one IP shares that IP's budget whatever token it carries. Requests with no auth header skip the token limiter. * **The endpoint path** — on some public surfaces, specific paths have their own limits. Exceeding a request rate limit returns **HTTP `429 Too Many Requests`**. ## Traffic limits by surface These are sustained per-second limits. When exceeded, the key is blocked for a short cooldown before requests are accepted again. | Surface | Limit | Cooldown after exceeding | | - | - | - | | **GraphQL API** — the transactional graph endpoint (`/api/v1/graphql`) | 20 requests / second | 10 seconds | | **Gift card vendor operations** | 20 requests / second | 10 seconds | | **Social graph** (contacts, invites, follows) | 50 requests / second | 10 seconds | | **Other services** (default) | 10 requests / second | next request rejected | | **Mobile / app gateway** | Protective limits tuned per environment | — | > [!NOTE] > > The GraphQL API endpoint is the one most integrations call for deposits, purchases, transfers, and reveals — plan around **20 requests per second** there. The mobile/app gateway's per-IP, per-token, and per-endpoint limits are configured per environment and aren't published as fixed numbers; treat them as protective and back off on `429`. ## Query cost limits The GraphQL API scores every query before it runs. A query over any of these limits is rejected without running, no matter how slowly you send requests. | Limit | Maximum | | - | - | | **Query cost** | `7,000` points per operation | | **Depth** | `6` levels of nesting | | **Aliases** | `15` per operation | | **Directives** | `50` per operation | | **Tokens** | `1,000` per request | | **Batching** | Off. Send one operation per HTTP request; a JSON array of operations is rejected. | ### How cost is counted Each field you select costs 1 point. A field with its own selection set costs 1 point plus 5 times the cost of everything inside it, and a query costs the sum of its top-level fields. Each level of nesting multiplies what's below it by 5: ```graphql theme={null} query { getWallet { # 1 + 5 * 56 = 281 balances { # 1 + 5 * 11 = 56 userCashBalances { # 1 + 5 * 2 = 11 userCashBalanceId # 1 availableCashBalance # 1 } } } } ``` The deepest fields are the expensive ones. A top-level field costs 1 point, a field at the fifth level costs 625, and a field at the sixth level, the deepest the depth limit allows, costs 3,125. Eleven fields at the fifth level, or two at the sixth, put a query over `7,000`. In practice you reach the cost limit before the depth limit. * Page size doesn't change the cost. Asking for 1 row or 20 costs the same, because cost comes from the fields you select, not the rows returned. * `__typename` costs the same as any other field. Clients that add it to every selection set, as Apollo Client does by default, raise the cost of deep queries. * Fragments add levels. An inline fragment (`... on Type { }`) counts as one level of nesting and a named fragment spread counts as two, so the same fields cost 5 or 25 times more inside a fragment. Write the fields out in place in large queries. * Every operation in the document is checked, including ones `operationName` doesn't select. One operation over the limit rejects the whole request. ### When a query is rejected The response is HTTP `400` with the reason in the `errors` array: ```json theme={null} { "errors": [ { "message": "Syntax Error: Query Cost limit of 7000 exceeded, found 7031.", "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] } ``` The message starts with `Syntax Error` even though the query is valid GraphQL, and the `found` value is your query's cost. Alias and directive limits return the same shape with their own message, for example `Syntax Error: Aliases limit of 15 exceeded, found 16.` A query over the depth limit is always over the cost limit too, so it gets both errors, cost first. A request over the token limit returns HTTP `500` with the same code, and a batched request returns HTTP `400` with the code `BAD_REQUEST` and the message `Operation batching disabled.` Nothing runs when a query is rejected, so no mutation is applied, and sending the same query again returns the same error. ## Authentication & security Sign-in, PIN, and two-factor (2FA) flows are protected separately from general API traffic. Repeated failed attempts — logins, PIN checks, or 2FA verification — and excessive 2FA or SMS requests trigger temporary lockouts that clear on their own after a cooldown. Successful authentication resets these counters. > [!WARNING] > > Don't auto-retry failed logins, PIN checks, or 2FA. Surface a "try again later" state to the user — automatic retries extend the lockout. ## When you exceed a limit Rate limits and query cost limits fail in different ways, so check the status before you retry: | | Rate limited | Cost limited | | - | - | - | | **Status** | `429` | `400` (`500` for the token limit) | | **Body** | `{"message": "Rate limit exceeded"}` | GraphQL `errors` array with `extensions.code` set to `GRAPHQL_VALIDATION_FAILED` (`BAD_REQUEST` for a batched request) | | **Cause** | Too many requests per second from your IP or token | One query selects too much, nests too deep, or uses too many aliases | | **What to do** | Wait and retry. On the GraphQL API the block lasts 10 seconds. | Don't retry. Remove fields or split the query into smaller ones. | Every rate-limited response uses status **`429 Too Many Requests`**. The body and headers vary by surface: ```json GraphQL / web / vendor services theme={null} { "message": "Rate limit exceeded" } ``` ```json Gateway / auth services (standardized error) theme={null} { "success": false, "msg": "Too many requests, please try again later", "errorRecord": { "code": "G-0002", "name": "RatelimitExceeded" } } ``` Header availability is not uniform: * **`Retry-After`** (integer seconds) is returned on the authentication IP limiter and the PIN/2FA flows. It is **not** guaranteed on the general GraphQL/web traffic limiters. * There are currently **no** `X-RateLimit-Limit`, `X-RateLimit-Remaining`, or `X-RateLimit-Reset` headers — don't build logic that depends on them. ## Build a well-behaved client **1. Treat 429 as retryable** Honor `Retry-After` when it's present. When it isn't, back off exponentially starting around 1 second (1s → 2s → 4s…) rather than retrying immediately. **2. Keep sustained rates modest** Don't parallel-blast a surface from a single IP or token. For the GraphQL API, stay comfortably under 20 requests/second and smooth out bursts. **3. Keep queries small** Select only the fields you use, keep nesting shallow, and write fields out in place instead of using fragments in large queries. If a query hits the cost limit, split it into smaller queries instead of retrying it. **4. Don't auto-retry auth failures** Repeated login, PIN, or 2FA failures cause temporary lockouts. Show the user a "try again later" message instead of retrying automatically. **5. Budget per IP, not per token** Every request from one IP counts against the same limit, however many tokens you use. If several workers, services, or customer accounts send traffic through one NAT gateway or proxy, pace them together so their combined rate stays under the limit. Because `Retry-After` isn't guaranteed everywhere, pair your retry logic with [idempotency keys](/concepts/idempotency) so a retried mutation never double-processes a purchase or transfer. For the full error catalog, see [Errors](/api-reference/errors). # Connector behaviour Source: https://docs.fluz.app/connector-behaviour Status values, error responses, timing, limits, and the request constraints that apply to every connector. Connectors accept your vendor's request shape and return your vendor's response shape. A few things are Fluz's rather than the vendor's, and they are the same across all three gift card connectors. Read this page before you cut over. ## Authentication and account scope Every connector uses HTTP Basic authentication with your Fluz API key: ```text theme={null} Authorization: Basic ``` A key is issued for exactly one Fluz account. That account funds every order placed with the key, and every read is scoped to it. Fields your vendor used to route between sub-accounts, such as Tango's `accountIdentifier`, do not select a sub-account here. If you operate several accounts, request one key per account. ## Order status values The `status` field on an order (`OrderStatus` on InComm) carries a Fluz value, not your vendor's: | Status | Meaning | | :- | :- | | `PENDING` | Accepted, not started | | `IN_PROGRESS` | Being fulfilled | | `COMPLETED` | Fulfilled, credentials available | | `FAILED` | Not fulfilled | | `CANCELED` | Cancelled before fulfilment | Your vendor's status strings do not carry over. A check written against a vendor value, for example `status === "COMPLETE"`, will not match `COMPLETED`. Update every status comparison before you cut over. ## Timing **Tango Card and InComm** create an order synchronously, always running to completion. The call does not return until the purchase has finished, which can take up to 150 seconds, so set your client's read timeout above that. **Runa** is asynchronous by default. `POST /v2/order` returns `202` immediately with a reference ID, and you read the order later. Send `X-Execution-Mode: sync` to block until the purchase completes and receive the full result, as Tango and InComm do. ## Reading an order An order becomes readable once its purchase has completed. Reading a reference ID before then returns an error rather than a pending status, so treat an error on a freshly created asynchronous order as still processing and read again shortly. The list endpoints return the 100 most recent orders on the account in a single response, newest first. ## Error responses Errors use the Fluz envelope, not your vendor's error schema: ```json theme={null} { "error": "" } ``` | Code | When | | :- | :- | | `401` | Missing or invalid credentials | | `429` | Rate limit exceeded. Body is `{ "message": "Rate limit exceeded" }` | | `500` | Every other failure, including a request the connector rejects | A rejected request and a server fault both return `500`. Read the `error` message to tell them apart: a rejection means the request needs to change, a fault is safe to retry. ## Rate limits 20 requests per second, applied per API key and per source IP. Exceeding either limit blocks that key or IP for 10 seconds, so back off for at least that long after a `429`. The per-key limit is shared, so several hosts using one key share one budget. ## Balances A balance read reports the available balance of the Fluz account your API key is issued for. Tango returns it as `currentBalance` and Runa as `balance`; InComm returns `availableBalance` alongside `prepaidBalance`, which covers the gift card balance on its own. A balance read is always scoped to the account your key is issued for, so the discriminators in your vendor's balance calls do not narrow it further: Tango's `:accountId`, InComm's `:programId` and Runa's `?currency=` are accepted for compatibility with your existing request shape. One behaviour to watch: a balance call with any query string returns a single object, and a call with none returns an array of one. If your client calls `.map()` over the response, keep the query string off. ## Brand codes All three connectors resolve brand codes against one Fluz catalog, whichever field carries them: `utid` on Tango, `Sku` on InComm, `items[].products.value` on Runa. Your vendor's product codes do not carry over, and a code Fluz does not recognise returns an error. Map your full brand list against the Fluz catalog before you cut over. A code that resolves to the wrong Fluz offer delivers the wrong card without an error. ## Request constraints Connectors accept your vendor's request shape, but some values are fixed. A request that breaks one of these is rejected. | Connector | Constraint | | :- | :- | | Tango Card | `sendEmail` must be present and set to `false` | | Runa | `payment_method` must be `{ "type": "ACCOUNT_BALANCE", "currency": "USD" }` | | Runa | `products.type` must be `SINGLE` | | Runa | Every entry in `items[]` must be identical. Mixed baskets are rejected. Send one order per distinct item | | InComm | Exactly one entry in `Recipients[]` | | InComm | Exactly one entry in `Products[]` | | InComm | `DeliverEmail` must not be `true` | Fluz returns gift card credentials directly in the order response, which is why the email flags above must be off: delivery to the recipient stays under your control. # Configure OAuth app Source: https://docs.fluz.app/create-an-o-auth-app Set up your application's identity, scope ceiling, redirect URIs, and webhook endpoints — everything that has to be right before a user can grant you access. Creating an application registers it. Configuring it is what makes it work. This page walks the four tabs of the app editor in the order you should fill them out, and explains what each field actually controls. Get these right before you build your authorize flow — most OAuth integration failures trace back to a configuration mismatch rather than to code. > [!NOTE] > > Reach the editor from **'Your apps'** in the developer dashboard, or directly at `https://fluz.app/for-developers/overview/{appId}`. Embedded widgets use the same tabs plus an additional **Installation** tab — see [Configure App Widget](/developers/configure-app-widget). ![Selecting an app to configure from Your apps](https://storage.googleapis.com/fluz-fluz-file-uploads-prod-ricuyxowbwlfprel/documentation/Screenshot%202025-11-05%20at%202.02.46%E2%80%AFPM.png) *** ## What's on each tab | Tab | What it controls | Read it before you | | :- | :- | :- | | **Overview** | Your app's public identity, and where your credentials live | Write any code | | **Permissions** | The scopes your app will request during OAuth | Build the authorize URL | | **OAuth** | Origin, redirect URIs, webhook endpoints | Handle the callback | | **Installation** | Generated embed code *(widgets only)* | Embed the widget | *** ## Overview tab Two things live here, and they serve very different audiences. ### Your credentials > [!WARNING] > > The **Client ID** and **Client Secret** are on this tab. Every other page in this section that tells you to authenticate a request with `Authorization: Basic base64(client_id:client_secret)` means these values. The **API Key** and **API Secret** also live here — those are a separate pair with a separate job. See [OAuth Applications](/build-a-platform/oauth-applications-overview) for which credential does what. > > Copy them into your secret manager. Never into a browser bundle, a mobile binary, or source control. ### Your public identity The name, subtitle, description, avatar, and logomark are **what your users see on the consent screen** when they decide whether to hand your application access to their money. Treat these as product copy, not internal labels. | Field | Guidance | | :- | :- | | **App name** | The name your users already know you by. "Acme Payouts," not "acme-oauth-prod-v2." | | **Subtitle** | One line on what the app does for them. | | **Description** | A short, plain-language account of why you're asking for access. | | **Avatar** | Required in practice. A consent screen with a blank avatar looks unfinished, and users hesitate. | | **Logomark** | Add it alongside the avatar. | > [!NOTE] > > Review this tab even if you think you filled it out during creation. The three text fields are collected in the creation wizard, where it's easy to type a placeholder and move on — and then ship it to a consent screen. *** ## Permissions tab This tab defines the permissions your application asks for. It is not what any individual user has granted you — that comes back on the token exchange. Since the consent screen is built from this tab rather than from your authorize URL, what you select here is exactly what every user is asked to approve. There is no per-request narrowing. The tab holds two independent permission lists: | List | Applies to | | :- | :- | | **Permissions** | Authorizations against a personal account | | **Business permissions** | Authorizations against a business account, and the permissions a user pre-approves for a business they're about to register | ![Scope selection on the Permissions tab](https://storage.googleapis.com/fluz-fluz-file-uploads-prod-ricuyxowbwlfprel/documentation/Screenshot%202025-11-05%20at%202.03.06%E2%80%AFPM.png) ### How selected scopes reach the user Users don't see raw enum values. Scopes you select are **grouped under a readable top-level header**, and it's the header that's presented for approval. A scope left unchecked is omitted entirely from what the user is asked to approve — and from what your app can ever request. ![How grouped scopes appear on the consent screen](https://storage.googleapis.com/fluz-fluz-file-uploads-prod-ricuyxowbwlfprel/documentation/Screenshot%202025-11-05%20at%202.04.44%E2%80%AFPM.png) ### Required scopes Some scopes are mandatory for the app or widget type you're configuring — without them the flow physically cannot run. These are gathered at the bottom of the tab. You'll see them there; you don't choose them. > [!NOTE] > > The consent screen presents your selected scopes as a **read-only list**. The user accepts the whole set or does not complete the authorization; there are no per-scope checkboxes. This applies to every scope, not just the mandatory ones, so the distinction that used to matter on the consent screen no longer does — anything you leave checked here, the user must accept. ### Business permissions If your application serves businesses, fill in the **Business permissions** list as well. It is edited independently of the consumer list and carries no selection restrictions — a permission can be on one list, the other, both, or neither. * **Permission to register a business is not offered here.** That one is granted by the personal account — it's what lets a user create a business in the first place — so it belongs on the **Permissions** list. It won't appear as an option on the business list. * **Leaving this list empty means the app is not business-enabled.** Users won't be offered the option to apply for a business account during authorization. * Apps that existed before this list was introduced were backfilled with a copy of their consumer permissions. Review it rather than assuming it's what you want. See [Business Accounts in OAuth](/business-accounts-in-o-auth) for how the two lists are applied at authorization time. ### App capabilities | Capability | Effect | | :- | :- | | **Business accounts only** | The authorization flow will not settle on a personal account. A user with no business account is sent straight into business registration; a user who has one is only offered their business accounts. Leave it off for apps that serve both. | ### Choosing your scopes Start from what the flow in front of you needs, not from what you might need someday. | If your integration... | Request roughly | | :- | :- | | Pays users out from your platform | `MAKE_WITHDRAW`, `LIST_PAYMENT` | | Collects funds from users | `MAKE_DEPOSIT`, `LIST_PAYMENT` | | Issues cards on a user's wallet | `CREATE_VIRTUALCARD`, `EDIT_VIRTUALCARD`, `REVEAL_VIRTUALCARD` | | Distributes cards by link | `CREATE_VIRTUALCARD`, `CREATE_SHARE_LINK` | | Sells or redeems gift cards | `LIST_OFFERS`, `PURCHASE_GIFTCARD`, `REVEAL_GIFTCARD`, `LIST_PURCHASES` | | Manages a user's funding sources | `LIST_PAYMENT`, `MANAGE_PAYMENT` | Full reference: [Application Scopes](/fluz-dashboard/application-scopes). > [!NOTE] > > **Fewer scopes convert better.** The consent screen is the highest-drop-off step in your integration, and its length is set by this tab. A narrower ceiling also limits the blast radius if a token leaks. Ask for what today's flow needs; widen the ceiling when you build the next feature. ### Scopes you can't self-select `PCI_COMPLIANCE` is administered by Fluz at the application level, granted to developers who have demonstrated PCI DSS compliance, and cannot be requested when generating a token. If you need to handle raw card data yourself, talk to your Fluz account manager. If you don't want to, that's what [embedded widgets](/developers/widgets) are for — they keep card capture inside Fluz's PCI scope. ### Changing scopes later The permission model is an **intersection** of the app-level ceiling and each user's grant, which has two practical consequences: * **Adding** a scope here does *not* retroactively grant it on tokens users already issued you. Existing users must re-authorize before the new scope becomes effective for them. * **Removing** a scope here narrows effective access immediately, for every user, regardless of what they previously approved. Plan scope changes like schema migrations, not like config tweaks. *** ## OAuth tab ![Redirect URI configuration on the OAuth tab](https://storage.googleapis.com/fluz-fluz-file-uploads-prod-ricuyxowbwlfprel/documentation/Screenshot%202025-11-05%20at%202.03.18%E2%80%AFPM.png) ### Origin The domain that will host the flow — `example.com`, `app.example.com`. For embedded widgets this is the page the widget renders on, and it must match or the widget won't load. ### Redirect URIs Where our authorization server is permitted to send the user after they approve or decline. > **Rules** > > * Must be a **public** URL our servers can reach. > * **No query parameters** on the registered URI. Use the `state` parameter to carry context instead. > * Register **as many as you need** — one per environment, one per flow variant. > * The URI you use at `/authorize` must be registered here, and the URI you send to `/token/exchange` must be **byte-identical** to the one you used at `/authorize`. > **Byte-identical means byte-identical** > > These are four different URIs as far as the authorization server is concerned: > > ```text theme={null} > https://app.example.com/oauth/finalize > https://app.example.com/oauth/finalize/ > http://app.example.com/oauth/finalize > https://App.Example.com/oauth/finalize > ``` > > Pick one canonical form, store it in a single constant, and use that same constant in both the authorize step and the exchange step. Hard-coding it twice is how mismatches happen. > **Local development** > > Register your local callback explicitly — for example `http://localhost:3035/oauth/finalize`. It won't work unless it's on the list, and localhost URIs should not be left registered on a production app. ### Webhook URLs Public REST endpoints that receive events from Fluz — how you learn that a transfer completed, a user dismissed a modal, or a verification resolved, without polling. * Add **as many URLs as you like**. * Subscribe each URL to **specific events**, so you can route different event families to different services. * A URL with **no events selected becomes a catch-all** and receives everything. Convenient for development, noisy in production. > [!NOTE] > > Your endpoint should acknowledge quickly and process asynchronously. Do the minimum work needed to accept the event, then hand it to a queue — a slow webhook handler turns into a delivery problem. *** ## Verify before you build Five minutes here saves an afternoon of debugging the authorize flow. **1. Credentials are out of the dashboard and into your secret store** Client ID, Client Secret, API Key, API Secret. Confirm nothing landed in a `.env` that's tracked in git. **2. The consent screen reads well** Name, subtitle, description, avatar, logomark all populated and written for your users. **3. Every scope your code calls is checked on Permissions** Walk your intended API calls and confirm each one's scope is enabled — on the **Business permissions** list too, if your app serves businesses. A scope you never enabled here is never offered to the user, so this mistake surfaces later as a confusing permission error. **4. Nothing is checked that you can't justify to a user** The consent screen is now exactly this list, read-only. Every extra scope is a line the user has to accept to finish. **5. Your redirect URI is registered in its exact canonical form** Including protocol, host casing, and trailing slash. **6. Build one authorize URL by hand and open it** Assemble `/authorize` with `response_type=code`, your `client_id`, and your registered `redirect_uri`, then load it in a browser. If the consent screen renders with your branding and the scopes you expect, your configuration is correct. If it errors, the message names what didn't match — and you've found it before writing any code. → [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow) *** ## Staging and production are separate apps Environments do not share configuration. A production application is registered separately, with its own Client ID, Client Secret, API Key, and API Secret, and its own redirect URIs and webhook endpoints pointed at live hosts. Nothing carries over from staging — including scope selections. Re-verify this entire page against your production app before launch. See [Deploying to Production](/deploying-to-production). *** ## Troubleshooting | Symptom | Almost always | | :- | :- | | `/authorize` returns an error instead of a consent screen | `redirect_uri` isn't registered, or `client_id` doesn't match this app | | Consent screen doesn't show the scopes you expected | The consent screen is built from this tab, not from the authorize URL. Check the Permissions tab — and the Business permissions tab if the user picked a business account | | No option to apply for a business account during authorization | **Business permissions** is empty, so the app isn't business-enabled — or the user already has a business application in progress | | Nothing happens when you try to add the register-a-business permission to the business list | It isn't available there by design. Add it to the consumer **Permissions** list instead | | Exchange at `/token/exchange` fails | `redirect_uri` doesn't byte-match the authorize step, or the Basic auth header encodes the parts separately instead of `base64(client_id:client_secret)` | | API call fails on a permission you thought you had | Scope is enabled at app level but wasn't in this user's grant — check the `scope` array returned by the exchange, not what you requested | | `generateUserAccessToken` fails for a user who worked before | The app-level or user-level grant expired or was revoked. Both must be live | | Widget won't load on your page | **Origin** doesn't match the hosting domain | | No webhooks arriving | URL isn't publicly reachable, or it's subscribed to events other than the ones firing | *** ## Next steps > **[Client-facing grant flow](/client-facing-o-auth-grant-flow)** > > Build the authorize URL and handle the callback. > **[Exchange an authorization code](/exchange-an-o-auth-authorization-code)** > > Turn a code into an access token and refresh token. > **[Application scopes](/fluz-dashboard/application-scopes)** > > The full scope reference. > **[Deploy to production](/deploying-to-production)** > > Re-register against live hosts and go live. # Custom card art Source: https://docs.fluz.app/custom-card-art Submit your own branded card design for the cards Fluz issues to your customers, and get it approved by the bank and the network. Programs that issue cards under their own brand run a **co-brand** design through Fluz. You supply the artwork and supporting documents; Fluz files them with our processor, sponsor bank, and the card network, and returns a card profile set ID that ties your design to the cards you create. > [!WARNING] > > Custom card art is reviewed and approved by parties outside Fluz — the sponsor bank and Visa or Mastercard. Plan for **6–8 weeks** from a complete submission to a production card profile. Do not build launch marketing around an unapproved design. ## Before you start **1. Confirm you need it** Cards issued on the standard Fluz design need no submission at all. You only need this process if your customers will see your brand on the card. **2. Pick your card types** Virtual, digital wallet (tokenized), and physical cards each have their own artwork requirements and their own approval steps. Submit only what you will actually issue — every extra type adds review time. **3. Check for reputational risk** Programs in crypto, cannabis, gaming, and similar verticals are subject to enhanced review, and the design itself is reviewed for anything that could be read as misleading or as an implied endorsement. See [specialized verticals →](/specialized-verticals) **4. Complete platform due diligence first** Card art review runs after your entity clears compliance and bank review. Starting the design in parallel is fine; submitting it before due diligence is not. [Due diligence →](/build-a-platform#due-diligence) ## What to submit ### Documents | Document | Completed by | Notes | | - | - | - | | Co-branding due diligence questionnaire | You | [Complete the questionnaire →](https://form.jotform.com/262446130628051) Signed by an authorized officer or principal. Start here — the rest of the review runs after this clears. | | Co-brand card program questionnaire | Fluz | We complete this using details you provide about the program, audience, and launch plan. | | Bank vendor ownership form | You | Waived for publicly traded companies. | | Digital wallet terms and conditions | You | Required only if you are issuing tokenized cards to Apple Pay or Google Wallet. | | Marketing materials | You | Landing pages, user flows, and any customer-facing collateral that shows the card. | ### Artwork files **Virtual cards** | File | Format | Specification | | - | - | - | | Card image with PAN | PNG | 1536 × 969 px, showing the full 16-digit sample PAN, CVV, and expiration date in position | Virtual card art must show a **full 16-digit PAN** — networks do not permit a truncated PAN on a virtual card face. **Digital wallet (tokenized)** | File | Format | Specification | | - | - | - | | Card image, last 4 only | PNG | 1536 × 969 px, displaying only the last four digits | | Card image, no PAN | PNG | 1536 × 969 px, no card number | | App icon | PNG | 100 × 100 px | All three files are required. Wallet providers use them in different surfaces. **Physical cards** | File | Format | Specification | | - | - | - | | Front of card (FOC) | Adobe Illustrator (.ai) | Full 16-digit sample PAN | | Back of card (BOC) | Adobe Illustrator (.ai) | Full 16-digit sample PAN | | Card carrier | High-resolution PDF | The mailer the card arrives on | > [!NOTE] > > Physical issuance is not enabled on every Fluz program. Confirm availability with your Fluz contact before you commission physical artwork. ### Sample PAN convention Artwork proofs must use the network's reserved sample number so the file is never mistaken for a live account. | Network | Sample PAN must end in | | - | - | | Mastercard | `x3456` | | Visa | `x9010` | ## Network requirements Both networks specify what has to appear on the card face and where. A file that omits any required element is rejected at network review, not at bank review — which costs you the full cycle. > **Mastercard** > > | Element | Requirement | > | - | - | > | Brand mark | Mastercard symbol, Mastercard Premium brand mark, Maestro, or Cirrus. Minimum 3 mm from the card edge. | > | Product identifier | 8.8 mm in size, at least 4 mm from the brand mark. | > | PAN | Full 16 digits. | > | CVC/CVV | 3 digits. | > | Expiration date | Required. | > > Additional design references are available in the [Mastercard Design Center library](https://www.mastercard.us/en-us/business/overview/support/design-center.html). > **Visa** > > | Element | Requirement | > | - | - | > | Brand mark | Minimum 3 mm from the card edge, rendered in Title Case. | > | Cardholder name | Required. | > | PAN | Full 16 digits. | > | CVC/CVV | 3 digits. | > | Expiration date | Required. | > | Virtual account marking | Optional. If used, "Visa Virtual Account" or "Virtual Account", in Title Case. | > > **Limited use cards.** If the card is restricted — card-not-present only, for example — that restriction has to be disclosed. Displaying "Limited Use" on the card face is optional, but where it does appear it must be clearly visible; otherwise the disclosure belongs in your cardholder materials or in-app. Fluz reviews your disclosure copy alongside the artwork. > > Visa programs carry one extra approval step (BMAS) that Mastercard programs do not. Budget an additional 1–2 weeks. ## Design rules > [!TIP] > > Design against a neutral background. Card art is rendered in surfaces you do not control — a design that depends on a specific backdrop will not survive them. > [!TIP] > > Keep the PAN, CVV, expiration date, and cardholder name legible against whatever sits behind them. Contrast failures are the most common rejection we see. > [!TIP] > > Hold all brand marks and required elements inside the safe area. Nothing may crowd or overlap the network brand mark. > [!TIP] > > Use only marks you have the right to use. Third-party logos require documented permission from the mark holder. > [!TIP] > > Avoid anything that implies a benefit, guarantee, or affiliation the program does not actually provide. ## Approval timeline Each stage runs in sequence, and a rejection at any stage sends the file back to the start of that stage. **1. Processor compliance review — 5 business days** Basic KYB on the co-brand entity, confirming registration matches the submitted documents and surfacing reputational risk. **2. Bank review, entity — 5 business days** The sponsor bank runs its own KYB scan on the co-brand entity and its own reputational risk assessment. **3. Bank review, collateral — 10 business days** Card art, carrier, and marketing materials are reviewed separately from the entity. This is the longest single stage. **4. Network program approval — 5–10 business days** Filed with the network: a PIF for Visa, an RPP for Mastercard. **5. Network art approval, Visa only — 5–10 business days** Digital card art is submitted to Visa through BMAS. Mastercard programs skip this stage. **6. Card profile creation — 3 business days** Once bank approval lands, a card profile set ID is created for your program. This is what guarantees your cards issue with your art. **Additional setup, where applicable** | Item | Duration | Prerequisite | | - | - | - | | Digital wallet fulfillment profile | 5–10 business days | Bank and network approval of the digital card art | | Physical card profile | 2 business days | Bank and network approval of the physical card art | | Tokenization (Apple Pay / Google Wallet) | Up to 8 weeks | Program approvals and wallet provider setup | > [!NOTE] > > Tokenization is the long pole. If push provisioning is part of your launch, start it as early as the approvals allow. See [digital wallet push provisioning →](/digital-wallet-push-provisioning) ## After approval Your program is assigned a **card profile set ID**. Fluz attaches it to your platform credentials, so cards created on your program automatically carry your approved design — no change to your integration. Any later change to an approved design is a new submission and runs the full bank and network cycle again. Colors, logos, layout, and disclosure copy are all in scope. Get the design right before you submit rather than iterating through review. ## Submit your card art Start with the due diligence questionnaire, then send your artwork files, supporting documents, and marketing collateral to your Fluz contact — or email us directly and we will open the submission for you. > **[Complete the due diligence questionnaire](https://form.jotform.com/262446130628051)** > > Compliance questionnaire for the co-branding entity, signed by an authorized officer or principal. > **[Send your artwork](mailto:humans@fluz.app?subject=Custom%20card%20art%20submission)** > > Email [humans@fluz.app](mailto:humans@fluz.app) with your program name, the card types you are issuing, and your artwork files attached. Questions on a design before you commit to it? Reach your Fluz contact or write to [humans@fluz.app](mailto:humans@fluz.app) — a five-minute look at a draft is cheaper than a rejection six weeks in. # Deploying to production Source: https://docs.fluz.app/deploying-to-production Move your integration from staging to live — the endpoint map, the credentials you have to reissue, the app configuration you have to redo, and what to verify before real money moves. Going live is a configuration cutover, not a rewrite. Your queries, mutations, and flows are identical in both environments. What changes is **which hosts you call** and **which credentials you call them with** — and those change completely. > [!WARNING] > > Nothing carries over from staging. Applications, API keys, client secrets, redirect URIs, and webhook subscriptions all exist separately in each environment. A staging credential will never work against a production host, and vice versa. That's deliberate — it's what makes it impossible to accidentally move real money from a test harness. ## Endpoint map | Purpose | Staging | Production | | :- | :- | :- | | Developer dashboard | `https://uni.staging.fluzapp.com/apps-and-integrations` | `https://fluz.app/for-developers` | | OAuth authorize | `https://uni.staging.fluzapp.com/authorize` | `https://fluz.app/authorize` | | Token exchange | `https://uni.staging.fluzapp.com/token/exchange` | `https://fluz.app/token/exchange` | | Token refresh | `https://uni.staging.fluzapp.com/token/refresh` | `https://fluz.app/token/refresh` | | GraphQL API | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | `https://transactional-graph.fluzapp.com/api/v1/graphql` | > [!NOTE] > > The GraphQL host and the OAuth host are different services with different domains. Swapping one and not the other is the single most common cutover mistake — and it fails in a confusing way, because authorization succeeds and then every API call is rejected. ### Reaching each portal The live portal is at `https://fluz.app/for-developers`. From there, the developer tab has an **'Open Staging'** link into the staging portal — your login credentials are the same in both. Each portal shows only that environment's applications and API keys. *** ## Make the environment a variable If any host, key, or redirect URI is a string literal in your codebase, fix that before cutting over. Everything environment-specific belongs in configuration. ```javascript Node.js theme={null} const ENVIRONMENTS = { staging: { authorizeUrl: 'https://uni.staging.fluzapp.com/authorize', tokenExchangeUrl: 'https://uni.staging.fluzapp.com/token/exchange', tokenRefreshUrl: 'https://uni.staging.fluzapp.com/token/refresh', graphqlUrl: 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql', redirectUri: 'https://staging.example.com/oauth/finalize', }, production: { authorizeUrl: 'https://fluz.app/authorize', tokenExchangeUrl: 'https://fluz.app/token/exchange', tokenRefreshUrl: 'https://fluz.app/token/refresh', graphqlUrl: 'https://transactional-graph.fluzapp.com/api/v1/graphql', redirectUri: 'https://app.example.com/oauth/finalize', }, }; export const fluz = ENVIRONMENTS[process.env.FLUZ_ENV ?? 'staging']; // Credentials come from your secret store, keyed by the same environment name. export const credentials = { clientId: process.env.FLUZ_CLIENT_ID, clientSecret: process.env.FLUZ_CLIENT_SECRET, apiKey: process.env.FLUZ_API_KEY, apiSecret: process.env.FLUZ_API_SECRET, }; ``` ```python Python theme={null} import os ENVIRONMENTS = { "staging": { "authorize_url": "https://uni.staging.fluzapp.com/authorize", "token_exchange_url": "https://uni.staging.fluzapp.com/token/exchange", "token_refresh_url": "https://uni.staging.fluzapp.com/token/refresh", "graphql_url": "https://transactional-graph.staging.fluzapp.com/api/v1/graphql", "redirect_uri": "https://staging.example.com/oauth/finalize", }, "production": { "authorize_url": "https://fluz.app/authorize", "token_exchange_url": "https://fluz.app/token/exchange", "token_refresh_url": "https://fluz.app/token/refresh", "graphql_url": "https://transactional-graph.fluzapp.com/api/v1/graphql", "redirect_uri": "https://app.example.com/oauth/finalize", }, } FLUZ = ENVIRONMENTS[os.environ.get("FLUZ_ENV", "staging")] # Credentials come from your secret store, keyed by the same environment name. CREDENTIALS = { "client_id": os.environ["FLUZ_CLIENT_ID"], "client_secret": os.environ["FLUZ_CLIENT_SECRET"], "api_key": os.environ["FLUZ_API_KEY"], "api_secret": os.environ["FLUZ_API_SECRET"], } ``` > [!NOTE] > > Default to staging, never to production. If a deployment loses its environment variable, you want it hitting the test environment, not moving real money. *** ## Rebuild your app in the live portal Work through [Create an OAuth App](/create-an-o-auth-app) again against `https://fluz.app/for-developers`. Then check every one of these — each is something people forget: **1. Reissue every credential** Production `client_id`, `client_secret`, `apiKey`, and `apiSecret` are all new values. Put them in your production secret store. Confirm no staging value survives in a production config. **2. Re-register redirect URIs against production hosts** Your production callback URL, in its exact canonical form. Then **remove any `localhost` or staging URIs** — a production app should not accept a redirect to a developer's laptop. **3. Re-point webhook URLs** At production endpoints that are publicly reachable, monitored, and alerting. Re-do the per-event subscriptions; they don't copy across. If you used a catch-all URL in staging, decide whether you actually want that in production. **4. Set Origin to your production domain** For embedded widgets, this must match the domain actually serving the page, or the widget won't load. **5. Re-select your scopes** Scope selections don't transfer. Walk your integration's API calls and confirm each one's scope is checked on the production app's Permissions tab. A missing scope is silently dropped, not rejected. **6. Finish the Overview tab** Name, subtitle, description, avatar, and logomark are what real users now see on a real consent screen deciding whether to give you access to their money. Placeholder text ships to production if you let it. **7. Confirm your app's status** Applications carry a status in the dashboard — an app sitting in review is not yet an app your customers can use. Confirm your production app is active before you announce anything. *** ## What behaves differently in production Staging mirrors production's capabilities and transaction flows without real money. That's most of the surface, but not all of it. | | Staging | Production | | :- | :- | :- | | Funds | Simulated | Real. Transfers are irreversible on your side | | KYC | Test paths with predictable outcomes | Real verification against real identity data, with real declines | | Card authorizations | Simulated | Live network authorizations and real declines | | Settlement timing | Immediate or simulated | Real banking timelines | | Funding sources | Test bank cards and accounts | Real cards and Plaid-linked accounts | | Failure modes | The ones you chose to test | Insufficient funds, expired cards, network declines, verification failures | Three consequences worth planning for: * **Idempotency stops being optional.** Every money-moving call needs a unique `idempotencyKey`, and widget tokens need a unique `jti`. In staging a duplicate is a nuisance; in production it's a double payment. See [Idempotency](/docs/idempotency-requests). * **Your error handling gets exercised.** Test users don't get declined for insufficient funds or fail KYC in ways you didn't script. Every failure path needs a defined user-facing outcome before launch, not after. * **Reconciliation matters.** Verify balances on both sides of a transfer rather than assuming success from a 200 response. *** ## Data hygiene, both directions **Never put production data in staging.** No real customer details, no real financial information, no PII. Staging is for data created explicitly for testing. The reverse also holds: don't carry test users, test funding sources, or test webhook payloads into production. Test artifacts in a live ledger are hard to unpick later, and some of them can't be deleted. *** ## Widget cutover If you're shipping an [embedded widget](/developers/widgets), the same rules apply plus these: * **Regenerate the embed code from the production app's Installation tab.** Your `appId` is environment-specific, so sign the production App ID into production tokens. * **Sign `patToken`s with your production `apiSecret`**, server-side. Confirm the secret is loaded from your production secret store and that the token generator is not still pointing at a staging value. * **Confirm `Origin`** matches your production domain exactly. * **Re-check the transaction type.** Pay-In and Payout move money in opposite directions; verify the direction against a real transfer before you open it to users. *** ## Pre-launch checklist > **Configuration** > > * Production app created, configured, and active > * All four credentials reissued and stored in the production secret store > * No staging or localhost redirect URIs remain on the production app > * Webhook URLs point at production endpoints, with event subscriptions re-selected > * Scopes re-selected and matched against your actual API calls > * Overview tab complete — name, subtitle, description, avatar, logomark > **Code** > > * No hard-coded hosts, keys, or redirect URIs anywhere in the codebase > * Environment resolution defaults to staging > * Both the OAuth host and the GraphQL host swapped > * Idempotency keys generated per operation, not per session > * Callback route is idempotent and validates `state` > * Token refresh runs before expiry rather than reacting to a failure > **Operations** > > * Webhook endpoint monitored, with alerting on failure to deliver or process > * Logging captures request identifiers and idempotency keys, and never captures secrets, PANs, or PII > * Someone owns the "user can't connect" and "transfer stuck" runbooks > * You've run one real end-to-end transaction at the smallest possible amount, in both directions, and reconciled both ledgers > **Rollout** > > * Internal users first, then a small cohort, then general availability > * You can disable the integration without a code deploy — a feature flag, not a rollback > * Re-authorization path built and tested, for when refresh tokens expire or users revoke *** ## Common cutover failures | Symptom | Cause | | :- | :- | | OAuth succeeds, then every API call is rejected | GraphQL host still pointed at staging (or vice versa) | | `/authorize` errors on the production URL | Production app's redirect URI not registered, or `client_id` is the staging value | | Token exchange fails immediately after a clean consent | Basic auth header built from staging `client_id`/`client_secret` | | Widget won't render on the live site | `Origin` still set to the staging or localhost domain | | Widget opens but the transaction is rejected | `patToken` signed with the staging `apiSecret` | | A permission that worked in staging now fails | Scopes weren't re-selected on the production app | | No webhooks in production | URLs weren't re-pointed, or event subscriptions weren't re-selected | | Consent screen shows a placeholder app name | Production Overview tab never filled in | | Duplicate transfers | Idempotency key reused or regenerated per retry | *** ## Next steps > **[Create an OAuth app](/create-an-o-auth-app)** > > Re-run every tab against your production app. > **[Grant flow](/client-facing-o-auth-grant-flow)** > > Verify the consent screen on production hosts. > **[Refresh an access token](/refresh-o-auth-access-token)** > > Keep production connections alive without re-prompting. > **[API features](/features)** > > Everything now running against real money. # Tools that shorten the path to production Source: https://docs.fluz.app/developers Embeddable widgets for low-code launches, drop-in connectors for APIs you already integrate with, and real-time event notifications. Fluz gives you three ways to move money in and out of your product without building the money-movement UI or plumbing yourself. Pick the entry point that matches how you build. > **[Embedded widgets](/developers/widgets)** > > Drop a JavaScript widget into your page for card and account flows. > **[API connectors](/API-connectors-overview)** > > Point existing Tango, Stripe, Worldpay, Venmo, or PayPal integrations at Fluz. > **[Event notifications](fluz-dashboard/webhooks)** > > Webhooks for real-time updates on transactions, cards, and accounts. ## Good things to know > **API foundations** > > * [GraphQL](/concepts/graphql) > * [Authentication](/concepts/authentication) > * [Staging vs. live](/concepts/environments) > **Building safely** > > * [Idempotency](/concepts/idempotency) > * [Rate limits](/concepts/rate-limits) > * [Error codes](/get-started/general-api-error-codes) > **Data requirements** > > * [Address formatting](/concepts/address-formatting-requirements) > * [Sandbox test data](/test-bank-cards) ## Code examples Copy-paste snippets for individual API operations — tokens, cards, gift cards, KYC, and money movement. > **[Browse code examples](/recipes/generate-user-access-token)** > > Single-operation GraphQL scripts you can drop straight into your integration. > [!NOTE] > > **AI-native docs:** point your assistant at `llms.txt` for a machine-readable index, or connect via MCP at `docs.fluz.app/mcp`. # Add a new app widget Source: https://docs.fluz.app/developers/add-a-widget Follow these steps to create and manage your new Fluz Widget. * Access your developer dashboard by selecting 'For developers' from your profile menu. * Select '[Browse templates](https://fluz.app/for-developers/templates) '. You will see all available widget templates listed here. * Click on the widget you would like to configure. For example, click 'Withdraw to a virtual Master'. This will open a wizard allowing you to customize the 'Withdraw to a virtual Master' app widget. * Fill out the App name * Fill out a subtitle of your app * Fill out the app Description * Click "Create new app" * Your new app widget is now created and will be listed under 'Your apps' * You should be redirected to the "Edit app" flow to finish configuring your widget # Add the JS widget to your page Source: https://docs.fluz.app/developers/adding-the-js-widget-to-your-page Load the Fluz embedded widget, initialize it with a patToken, open it, and handle the outcome. > [!NOTE] > > **Before you begin:** your server must generate a `patToken` for each session and make it available to your page. See [Set up your server](/developers/setting-up-your-server). ## 1. Load the widget script Add this loader to your page. It injects the widget script and resolves once `FluzEmbedded` is ready, so the rest of your code never runs before the widget exists. ```html theme={null} ``` ## 2. Initialize the widget Once the script has loaded, call `init()` with the transaction details and the `patToken` from your server. ```javascript theme={null} loadFluzWidget().then((FluzEmbedded) => { FluzEmbedded.init({ patToken: "", // The patToken generated on your server transaction_type: "", // e.g. DEPOSIT or WITHDRAW (see Init options) amount: 0.0, // Amount in dollars and cents onOpen: () => console.log("Widget opened"), onClose: () => console.log("Widget closed"), onSuccess: (result) => console.log("Flow completed", result.externalReferenceId), onError: (error) => console.error(error.code, error.message), }); }); ``` > [!NOTE] > > `init()` takes `transaction_type` in snake case. The claim inside the signed `patToken` is `transactionType`. Both must describe the same transaction. > [!WARNING] > > Do not pass `apiKey` to `init()`. Your API key is your application's credential, and anything in your page is visible to the user. The script identifies your application from the signed `patToken`, which carries your `appId`. Versions before 1.3.0 required `apiKey`; 1.3.0 ignores it if it is still passed. ## 3. Open the widget Choose how the user launches the widget. Both options run after `init()`. **Use your own button** Bind `open()` to a button that already exists on your page. ```javascript theme={null} loadFluzWidget().then((FluzEmbedded) => { FluzEmbedded.init({ /* options from step 2 */ }); document .getElementById("existing-button") // replace with your selector ?.addEventListener("click", () => FluzEmbedded.open()); }); ``` **Use the Fluz button** Append a pre-labeled Fluz button to an element on your page. Clicking it calls `open()`. ```javascript theme={null} loadFluzWidget().then((FluzEmbedded) => { FluzEmbedded.init({ /* options from step 2 */ }); const el = document.getElementById("btn-container"); // replace with your selector if (el) FluzEmbedded.renderButton(el); }); ``` ## Init options | Option | Required | What it does | | :- | :- | :- | | `patToken` | Yes | The signed pre-approved transaction token. [Generate it on your server](/developers/setting-up-your-server), never in browser JavaScript. | | `transaction_type` | Yes | `DEPOSIT`, `WITHDRAW`, `CASH_DEPOSIT`, `PAY_IN`, `PAYOUT`, `KYB`, or `ENROLLMENT`. | | `amount` | Most types | A number. Omit it for types that carry no amount, such as `KYB` and `ENROLLMENT`. | | `container` | No | **Staging only.** An element or CSS selector. When set, the widget renders inline inside that element instead of in a fullscreen modal. See [Render inline](#render-the-widget-inline-staging-only). | | `onOpen` | No | Called once the widget has been added to the page. | | `onClose` | No | Called when the widget is removed, whether by the user or by your call to `close()`. | | `onSuccess` | No | Called once when the flow completes, then never again. Receives the [session details](#what-the-callbacks-receive). | | `onError` | No | Called each time an attempt fails: identity verification, a transaction, or the widget itself. Receives `{ code, message }` plus whichever session details are known. | > [!NOTE] > > `init()` checks only what the script itself needs: `patToken`, `transaction_type`, and a resolvable `container`. Everything else is validated against your signed token, so a bad `transaction_type` or a missing `amount` reaches you through `onError`, not a console message your code can't see. ## Methods | Method | What it does | | :- | :- | | `FluzEmbedded.init(options)` | Configures the widget. Rejects invalid options and logs why. | | `FluzEmbedded.open()` | Renders the widget. `openModal()` is an alias kept for older integrations. | | `FluzEmbedded.close()` | Removes the widget and fires `onClose`. | | `FluzEmbedded.renderButton(element)` | Appends a pre-labeled button that calls `open()` when clicked. | | `FluzEmbedded.isOpen()` | Returns whether the widget is currently rendered. | ## Handle the outcome `onOpen` and `onClose` only tell you the widget appeared and went away, not whether the user finished. To know how the flow ended, pass `onSuccess` and `onError`. Both are optional; if you pass neither, the widget still works, you just won't receive outcomes. ```javascript theme={null} FluzEmbedded.init({ transaction_type: "PAY_IN", amount: 50.0, patToken: "your-token", onSuccess: ({ userId, accountId, externalReferenceId, email }) => { // UI only: show the user they're done and move them along. showConfirmation(externalReferenceId); }, onError: ({ code, message, externalReferenceId }) => { if (code === "WIDGET-004") { // The token expired. Mint a fresh one and reopen. return refreshTokenAndReopen(); } if (code === "US-0005" || code === "G-0004") { // The user is still in the widget and may retry. // Record the attempt; don't tear down your own flow. return recordFailedAttempt(code, externalReferenceId); } reportToSupport(code, message, externalReferenceId); }, }); ``` > [!WARNING] > > **Do not settle from `onSuccess`.** It runs in the visitor's browser, carries no verifiable record of the transaction, and anyone can call it from the console. Use it for what the user sees: a confirmation, a redirect, clearing a spinner. Credit accounts, release fulfillment, or write to your ledger only from your server, driven by a [webhook](/fluz-dashboard/webhooks) or a transaction you fetched and verified yourself. ### How the callbacks behave * **`onSuccess` is final.** It fires at most once, and no outcome is delivered after it, so a completed flow never also reports an error. * **`onError` reports an attempt, not a verdict.** A user whose identity check fails or whose payment is declined is still in the widget and can try again. `onError` can fire more than once in a session, and a later `onSuccess` can still arrive. * **Neither callback closes the widget.** The user stays on screen until they dismiss it, which fires `onClose`. Treat the session as over only when `onSuccess` fires or the user closes the widget. ### What the callbacks receive Both callbacks include, when known, the ids that tie the session to your records and the contact details on the Fluz account that ran it. Fluz fills these in server-side from your signed token and the user's session, so they're the values Fluz recorded, not anything the page assembled. | Field | Where it comes from | | :- | :- | | `userId` | The Fluz user id (UUID). The same id [webhook events](/fluz-dashboard/webhooks) carry, so an outcome and a later webhook line up. | | `accountId` | The Fluz account id (UUID) the flow ran against: the user's consumer account, or their business account for a business flow. | | `externalReferenceId` | The `externalId` you signed into the `patToken`, handed back to you. | | `email` | The email on the Fluz account that ran the flow. | | `phoneNumber` | The phone number on that account, when one is recorded. | **Read each field on its own; don't assume the set is complete.** * `externalReferenceId` comes from your token, so it survives failures that happen before a user signs in. * The other four come from the Fluz account and only appear once the session got that far. * A failure before your token validates (an expired token, a missing app permission) carries nothing. `onError` receives only `code` and `message`. `email` and `phoneNumber` describe the Fluz account the user signed into, which may differ from the values you passed in the `patToken`. These fields identify the session; they aren't a record of what happened. Use `externalReferenceId` to find the right row in your system, then confirm the outcome with a webhook or API call before acting on it. > [!WARNING] > > **`email` and `phoneNumber` require a registered Origin.** Fluz addresses outcome messages to the **Origin** on your app's OAuth tab and includes contact details only when that origin is set and usable. With no origin on file, you still receive the three ids. > > Keep the Origin pointed at the page that embeds the widget. If it names a host you no longer embed from, the browser refuses to deliver the messages, so `onSuccess` and `onError` stop firing. `onOpen`, `onClose`, and the close control are unaffected. The only trace is a `postMessage` target-origin mismatch in the browser console. See [Configure App Widget](/developers/configure-app-widget). ### Error codes `onError` receives a stable `code` to branch on and a human-readable `message` to log. `US-0005` and `G-0004` are failed attempts the user can retry; every other code means the session can't continue. | Code | What happened | What to do | | :- | :- | :- | | `WIDGET-001` | The widget link is missing a required parameter. | Check the values you passed to `init()`. Don't retry. | | `WIDGET-002` | The widget link contains an invalid parameter. | Check `transaction_type` and `amount`. Don't retry. | | `WIDGET-003` | Your application is missing permissions the widget needs. | Contact your account manager. Don't retry. | | `WIDGET-004` | The session token has expired. | Mint a new `patToken` and reopen the widget. | | `WIDGET-005` | The session was terminated before it completed. | Start a new session. | | `AUTH-0033` | The account is already linked to another user of your application. | Route the user to the account they already connected. | | `US-0005` | The user didn't complete identity verification: the check failed with no further step, or the account is in review. | The user stays in the widget and may retry. Don't grant access that depends on a verified user. | | `G-0004` | The transaction couldn't be completed. | The user stays on the confirmation screen and may retry. Don't settle or release funds. | | `G-0001` | Something went wrong on our side. | Retry once, then alert. | Treat any unrecognized code like `G-0001`. New codes ship over time without a breaking change. ## Render the widget inline (staging only) > [!WARNING] > > The `container` option is available on **staging only**. Production integrations must use the default fullscreen modal until it ships there. By default, the widget mounts a fixed, full-viewport overlay on your page. Pass a `container` and it renders inside an element you position and size yourself, with no overlay, scroll lock, or page takeover. ```javascript theme={null} loadFluzWidget().then((FluzEmbedded) => { FluzEmbedded.init({ patToken: "", // The patToken generated on your server transaction_type: "WITHDRAW", amount: 50.0, container: "#fluz-widget", // a CSS selector or the element itself onClose: () => { // The container is now empty. Render what should replace the widget. }, }); FluzEmbedded.open(); }); ``` ### Sizing The widget fills your container at 100% width and height and **never resizes it**. Content taller than the container scrolls inside the widget, not your page. * **Set an explicit width and height** on the container in your CSS. Without a height, the widget falls back to the browser's default iframe height of 150px, the most common mistake in this integration. The script logs a console warning if the container is too small when the widget opens. * **Minimum supported size is 320 × 480 px.** Every screen, including document capture, 2FA, and PIN entry, is supported at that size. Smaller layouts aren't supported. * **There's no maximum.** Wide containers get the desktop layout, including the transaction summary pane; narrow ones get the mobile layout. ### Differences from the modal | Behavior | Modal | Inline | | :- | :- | :- | | Overlay on top of your page | Yes | No | | Escape closes the widget | Yes | No, your page keeps the key | | Browser back button is intercepted | Yes | No, your page keeps its history | | Widget's own close control | Dismisses the modal | Removes the widget and empties your container | Plan for that last row: when the user closes an inline widget, `onClose` fires and your container is emptied, so render its replacement from `onClose`. ### Container errors If the container can't be used, nothing renders, `onOpen` doesn't fire, and the script logs an error: * **At `init()`:** `container` is neither an element nor a non-empty string. The widget stays unconfigured, and a later `open()` refuses. * **At `open()`:** the selector matches nothing, isn't valid CSS, or the element isn't attached to the document. An unresolvable container **never falls back to the fullscreen modal**. > [!NOTE] > > A `container` selector is resolved when `open()` is called, not at `init()`, so it's fine to call `init()` before the element exists. # Configure app widget Source: https://docs.fluz.app/developers/configure-app-widget After you've created a new app widget, you will need to configure the OAuth settings in order for the widget to obtain the necessary permissions from the user. Follow these steps below. * **Navigate to 'Your apps' from the developer dashboard** * **Select the app to set up** * **Review the Overview tab for correctness** * Add an avatar and logomark. * **Review Permissions** * This is the list of individual scopes that will be requested for the user to approve. * Rather than list each scope individually for the user to approve, any scopes that are selected on this page will be grouped under the top level header, and the individual header will be presented to the user. If a scope is left unchecked, it will also be omitted from what the user ultimately approves. * Some scopes that are required for the particular widget type you are configuring to be able to run will be gathered at the bottom of this tab. The user will be unable to unselect these specific scopes. * **Provide the following settings under OAuth:** * **Origin** - The domain name that will be hosting the widget, for example google.com or mysite.com. * **Redirect URIs** - A public URL that our authorization server can redirect to for the OAuth authorization code flow. Do not include any query parameters in this URL. You can include any number of these URIs. If you use the OAuth flow to generate an authorization code, you must use the same callback URI for the exchange of the code. * **Webhook URLs** - A public REST endpoint to receive widget related events from Fluz. * You can add as many webhook URLs as you would like, and you can subscribe the individual URLs to specific webhook events. If you provide a URL without any webhook events selected, then it will be treated as a "catch-all" URL and all webhook events will be sent to that URL. * **Review the 'Installation' section.** This will include the generated Javascript code for your widget. You can copy and embed this Javascript code into your web application to host the widget. # Disable or delete your app Source: https://docs.fluz.app/developers/disable-or-delete-your-app If you would like to disable or delete your widget. Follow the steps below. * Navigate to 'Your apps' in the developer dashboard * Select the app * There are two buttons at the bottom of every tab: 'Disable app' and 'Delete app' * To disable, select 'Disable app'. This will set the app status to 'DISABLED', and users should be unable to interact with your app. You can reenable your app later. * To delete, select 'Delete app'. # Enrollment widget Source: https://docs.fluz.app/developers/enrollment-widget The ENROLLMENT widget type: send a user through account setup right after OAuth, choosing which steps they see — identity verification, funding sources, and PIN. The `ENROLLMENT` widget type walks a user through account setup immediately after they authorize your application. It has the same shape as the other widget types — the user is redirected straight into a task after authorizing — with one difference: **you choose which setup steps the user sees.** Unlike the payout and pay-in types, this widget moves no money. There is no amount to confirm and no transaction to approve. > [!NOTE] > > **Prerequisites** > > * A widget application with OAuth settings in place. See [Configure App Widget](/developers/configure-app-widget). Any application type can run this widget, including a permissions-only `OAuth Integration` app. > * The user must have granted your application's scopes. Scopes are checked for the application as a whole, not per step. If any are missing, the widget sends the user through the OAuth consent screen before the first step, the same as the other widget types. No step is skipped or failed because of a scope. > * An OAuth user and an access token, same as the other widget types. ## Choosing the steps Pass an ordered `steps` array in the pre-approved transaction token alongside `transactionType: "ENROLLMENT"`: ```javascript theme={null} import jwt from 'jsonwebtoken'; const generatedToken = jwt.sign( { appId: '', // Your App ID (Overview tab) transactionType: 'ENROLLMENT', externalId: '', // Your unique identifier for the user steps: ['KYC', 'FUNDING_SOURCE', 'PIN'], jti: uuidv4(), }, secret, // Your apiSecret { expiresIn: '1 day' } ); ``` `amount` is not required for this widget type. Everything else about the token is unchanged — see [Set Up Your Server](/developers/setting-up-your-server). To link the user and run no setup steps, pass an empty array: `steps: []`. The widget ends at consent, so you get the grant and nothing else. This is the flow behind [`registerUser` consent mode](/api-reference/mutations/register-user#consent-mode) when you list no steps. ### Supported steps | Step | What the user does | | :- | :- | | `KYC` | Completes personal identity verification, escalating to document capture if needed. See [Verify by Widget](/verify-customers-by-widget). | | `FUNDING_SOURCE` | Links one or more funding sources — a bank account via Plaid, a bank card, or PayPal. | | `PIN` | Sets their Fluz transaction PIN. | > [!NOTE] > > This widget runs **personal** identity verification only. Business verification (KYB) is not one of its steps — use the dedicated KYB widget type for that. See [Business Registration](/docs/business-registration). ### Order Steps run in the order you list them. Fluz does not reorder them, and there are no invalid combinations — none of the three steps is a prerequisite for another, so `["PIN", "FUNDING_SOURCE", "KYC"]` is as valid as `["KYC", "FUNDING_SOURCE", "PIN"]`. List the steps in the order that makes sense for your product. ## Skip behavior A step the user has already satisfied is skipped silently — they never see its screen. | Step | Skipped when | | :- | :- | | `KYC` | The user has already passed identity verification | | `PIN` | The user already has a PIN set | | `FUNDING_SOURCE` | **Never** | The funding-source step is never skipped, because a user who already has one source may want to add another. It always renders, and the user decides when to move on. If every step you requested is already satisfied, or you passed `steps: []`, the widget completes immediately after consent without rendering a step screen. > [!NOTE] > > Because `FUNDING_SOURCE` never auto-skips, a `steps` list that includes it always shows at least one screen. If you want the "nothing to do, complete immediately" outcome to be reachable, request only `KYC` and `PIN`. ## The funding source step This step is deliberately open-ended. The user can add as many funding sources as they like in one visit — the screen lists what they have added so far and carries an explicit **Continue** control, so advancing is their decision rather than something that happens automatically after the first successful link. A user who does not want to add anything can move on without adding a source. Funding-source changes do not emit a webhook yet. A dedicated event is in progress. Until it ships, read the user's funding sources with the `getWallet` query after the flow completes. See [Funding sources](/features/funding-sources). ### Opening a specific funding source > [!NOTE] > > **Staging only** > > `targetFundingSource` is currently supported only in the staging environment. Production widgets do not honor it yet; this page will be updated when it is rolled out. To send the user straight into one kind of funding source, add `targetFundingSource` to the token alongside `steps`: ```javascript theme={null} const generatedToken = jwt.sign( { appId: '', // Your App ID (Overview tab) transactionType: 'ENROLLMENT', externalId: '', // Your unique identifier for the user steps: ['KYC', 'FUNDING_SOURCE', 'PIN'], targetFundingSource: 'BANK_ACCOUNT', jti: uuidv4(), }, secret, // Your apiSecret { expiresIn: '1 day' } ); ``` | Value | Where the funding source step opens | | :- | :- | | `BANK_ACCOUNT` | Plaid, ready to link a bank account. | | `BANK_CARD` | The bank card form. | | `PAYPAL` | The funding source type picker, where the user taps **PayPal**. PayPal opens in a pop-up window, and browsers only allow that after a tap, so the widget cannot launch it for the user. | This happens once, the first time the user reaches the step. After they link the source, they land on the usual funding source screen, which lists what they have added, lets them add another source of any type, and carries the **Continue** control. If they back out instead, they land on the type picker, where they can choose a different type or go back to the usual screen. `targetFundingSource` changes only where the step starts. It does not restrict the user to that type, and it does not make adding a source required. The claim is optional. Leave it out and the step opens on the usual screen. It is **staging only** for now — not yet supported in production. ## Completion When the last step resolves, the widget shows a completion screen. If you supplied a `callbackUrl`, it carries a button back to your application; otherwise it offers a **Done** control that closes the widget. > [!WARNING] > > Do not infer success from the widget closing — a user can dismiss it at any point. Rely on the `onSuccess` and `onError` callbacks, and on webhooks for the individual steps: identity verification events fire independently of the widget. See [Webhooks](/fluz-dashboard/webhooks). An enrollment that includes `KYC` only completes once identity verification passes. When a check fails, `onError` fires with `US-0005` and `onSuccess` does not fire for that attempt. What happens next depends on where it failed: a check that ends with no further step to offer leaves the user on the form, where a retry that passes still completes the enrollment and fires `onSuccess` once; an account that goes into review is shown the review screen instead, and that session cannot complete. Either way, treat `US-0005` as "not verified yet" rather than a final answer, and confirm the outcome from the `KYC_STATUS_UPDATE` webhook — which reaches you only if your application and the user's grant both hold the `VERIFY_KYC` scope. See [Outcome callbacks](/developers/adding-the-js-widget-to-your-page#know-how-the-flow-ended). ## Rejected tokens The `steps` and `targetFundingSource` claims are validated when the session opens. These all reject the session rather than degrading to a partial flow: | Problem | Result | | :- | :- | | `steps` missing entirely | Missing-parameter error | | `steps` is not an array | Invalid-parameter error | | `steps` contains a value that is not `KYC`, `FUNDING_SOURCE`, or `PIN` | Invalid-parameter error naming the unsupported value and listing the supported ones | | `steps` lists the same step twice | Invalid-parameter error naming the duplicate | | `targetFundingSource` is not `BANK_ACCOUNT`, `BANK_CARD`, or `PAYPAL` | Invalid-parameter error naming the unsupported value and listing the supported ones | | `targetFundingSource` is set but `steps` does not include `FUNDING_SOURCE` | Invalid-parameter error | | `targetFundingSource` is a type that is not enabled for your application | Invalid-parameter error naming the value and listing the types your application allows | The user sees a generic "invalid widget link" screen — the specific reason is not shown to them, since a malformed token is an integration problem rather than something they can act on. Check your token generator, and see the error detail delivered to your `onError` callback. ## Next steps > **[Set up your server](/developers/setting-up-your-server)** > > Generate the pre-approved transaction token in your language. > **[Embed the widget](/developers/adding-the-js-widget-to-your-page)** > > Script tag, init call, callbacks. > **[Verify by widget](/verify-customers-by-widget)** > > What the user experiences during the KYC step. > **[Funding sources](/features/funding-sources)** > > What you can do with a source once it is linked. # Authentication Source: https://docs.fluz.app/developers/good-to-know/authentication How API keys, user access tokens, and scopes work — for your own account and for connected customer accounts. Every Fluz API request carries a **user access token** in the `Authorization` header. How you get that token depends on whose account you're operating on. ## Two paths, one API > **Your own account** > > `API key` → `Your token` > > Your server uses its application API key (`Authorization: Basic `) to mint a token scoped to your Fluz account. > **A customer's account** > > `OAuth grant` → `Their token` > > A customer authorizes your app, and Fluz returns a token scoped to their account. Once you hold a token, the rest of the API is identical. `createVirtualCard` on your token creates a card on your wallet; `createVirtualCard` on a customer token creates it on theirs. ## Path 1 — your own account 1. In the dashboard, create an application and copy its **API Key**, **User ID**, and **Account ID**. 2. From your backend, call `generateUserAccessToken` on `https://transactional-graph.fluzapp.com/api/v1/graphql` (staging: `https://transactional-graph.staging.fluzapp.com/api/v1/graphql`) with the header `Authorization: Basic `, passing your `userId`, `accountId`, and the scopes you need as arguments. 3. Attach the returned `token` as `Authorization: Bearer ` on every request to the transactional graph. See [API credentials](/get-started/api-credentials) for a full example. ## Path 2 — a customer's account Use this when you're building a platform — e.g. issuing cards on behalf of your users. 1. Redirect the customer to Fluz's OAuth authorize URL with your `clientId`, requested scopes, and `redirect_uri`. 2. The customer signs in and grants your scopes. 3. Fluz redirects back with a short-lived authorization `code`. 4. Your server exchanges the code for a customer-scoped access token at the OAuth token exchange endpoint — see [the OAuth grant flow](/client-facing-o-auth-grant-flow). 5. Refresh customer-scoped tokens via the [OAuth token refresh endpoint](/refresh-o-auth-access-token) without re-prompting the customer. Full walkthrough in [Build a platform](/build-a-platform). ## Scopes Tokens carry an explicit set of scopes. Common ones by capability: | Area | Scopes | | - | - | | Gift cards | `LIST_OFFERS`, `PURCHASE_GIFTCARD`, `REVEAL_GIFTCARD`, `LIST_PURCHASES` | | Virtual cards | `CREATE_VIRTUALCARD`, `REVEAL_VIRTUALCARD`, `EDIT_VIRTUALCARD` | | Deposits | `MAKE_DEPOSIT` | | Funding sources | `LIST_PAYMENT`, `MANAGE_PAYMENT` | | Card data (PCI) | `PCI_COMPLIANCE` — application-level, Fluz-administered; cannot be requested when generating a token. Granted on request for your own account; requires a PCI DSS certification to take raw card data onto your servers on someone else's account. Not needed at all if you use [Secure Elements](/build-a-platform/secure-elements-overview) | Request the minimum you need. Mint a new token when you need broader access. ## Token lifetime * **Access tokens:** short-lived JWTs (minutes, not hours). Mint a new one when it expires — see [Replace an expired access token](/get-started/refresh-expired-access-token). * **OAuth customer tokens:** refreshable via the [OAuth token refresh endpoint](/refresh-o-auth-access-token). * **Application API key:** valid until rotated in the dashboard. > [!WARNING] > > Never ship your API key to a browser or mobile client. It mints tokens for your account; leaking one is equivalent to leaking your credentials. ## Next steps > **[Get your API credentials](/get-started/api-credentials)** > > Path 1 in practice — mint a token for your own account and make a call. > **[Build a platform](/build-a-platform)** > > Path 2 in practice — connect customer accounts with the OAuth grant flow. # Security & compliance Source: https://docs.fluz.app/developers/security How Fluz protects funds, customer data, and card credentials — and what that means for your integration. Fluz is built to keep you out of scope for the parts of financial infrastructure that are hardest to get right. ## Certifications * **SOC 2 Type II** — audited annually. * **PCI DSS Level 1** — the highest tier of card-data compliance. * **FDIC-insured banking partners** — balances are held at insured US institutions. ## Data protection * TLS 1.2+ required for every request; older versions rejected at the load balancer. * Data encrypted at rest with AES-256. * PANs, CVVs, and PINs never appear in your logs — they render only inside Fluz-hosted surfaces: the [embedded widget](/developers/widgets) or [Secure Elements](/build-a-platform/secure-elements-overview). Receiving raw card data on your own servers is possible but requires a PCI DSS certification on file — see [Due diligence](/build-a-platform#due-diligence). * Secrets rotated automatically; API keys and OAuth secrets can be rotated on demand from the dashboard. ## Fraud monitoring * 24/7 fraud operations team monitoring transactions in real time. * ML-based scoring on every authorization. * Automatic velocity checks; you can add your own rules via the dashboard. * Optional manual review holds for high-risk flows. ## Your responsibilities 1. **Never ship your API Key or OAuth `clientSecret` values to a client.** Mint access tokens on your server. 2. **Verify webhook signatures** before trusting a payload. See [Configure App Widget](/developers/configure-app-widget) for webhook URL setup. 3. **Scope tokens narrowly.** Only request the scopes you actually use — mint a new token when you need broader access. 4. **Persist your own operation IDs** so retries land safely under de-duplication (see [Idempotency](/concepts/idempotency)). 5. **Rotate application secrets** on employee turnover and after any suspected exposure. ## Reporting a vulnerability Email `humans@fluz.app` with reproduction steps. We acknowledge within one business day and coordinate disclosure timelines with reporters. # Set up your server for the Fluz JS widget Source: https://docs.fluz.app/developers/setting-up-your-server Generate the signed pre-approved transaction token (patToken) your page passes to the Fluz JS widget. To use the Fluz JS widget, you do two things: 1. **On your page**, load the widget JavaScript and bind a UI element (for example, a button) to open the widget modal. 2. **On your server**, generate a signed JWT, the pre-approved transaction token (`patToken`), and pass it to the widget. This page covers step 2. ## What the patToken does Because the user is moving money into or out of your operator spend account through Fluz, every widget session must carry a signed JWT that pre-authorizes that specific transaction. For example, if user `abc123` has \$50 in your operator spend account and wants to withdraw it to their Fluz account, your server generates a `patToken` with `transactionType: "WITHDRAW"` and `amount: 50.00`. Fluz validates the token before allowing the transaction. Sign the token with **HS256** using your `apiSecret`, which you can find in your app details under **For Developers** in Fluz. > [!WARNING] > > Always generate the token on your server. Never expose your `apiSecret` in client-side code. ## Token claims ### Required | Claim | Description | | - | - | | `appId` | Your app's App ID, shown on the **Overview** tab of your app in the Developer Console. | | `transactionType` | One of `DEPOSIT`, `WITHDRAW`, or `ENROLLMENT`. `DEPOSIT`: the user adds money to Fluz, which then moves to your operator spend account. `WITHDRAW`: a payout from your operator spend account to the user's Fluz account. `ENROLLMENT`: runs account setup instead of a transaction. See [Enrollment Widget](/developers/enrollment-widget). | | `amount` | The amount, in dollars and cents, the user is approved to transact. Required for `DEPOSIT` and `WITHDRAW`; omit it for `ENROLLMENT`. | | `externalId` | Your unique identifier for the user, such as a user ID, account ID, phone number, or email address. Fluz includes it in webhook events so you can map the user back to your system. | | `jti` | A UUID v4 generated each time you sign a token. Fluz uses it for idempotency. See the [JWT spec](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7). | | `exp` | Token expiry as a Unix timestamp. The examples below use 24 hours. | | `steps` | Required only when `transactionType` is `ENROLLMENT`; ignored otherwise. An ordered array of setup steps to show: `KYC`, `FUNDING_SOURCE`, `PIN`. Pass `[]` to end the flow at consent. Unknown or duplicate values reject the session. | > [!WARNING] > > Never put your API key in the token, and never pass it to the browser. Your API key is your application's credential: anyone who has it can call the Fluz API as your application. The token is visible in the user's browser, so identify your application with `appId` only. The token's signature, made with your API secret on your server, proves it came from you. ### Optional (prefill user details) | Claim | Description | | - | - | | `phoneNumber` | The user's phone number. If provided, the user skips the **Enter the phone number** step and goes straight to **Enter the two-factor authentication code**. | | `firstName` | The user's first name. | | `lastName` | The user's last name. | | `email` | The user's email address. | | `username` | The user's username. | ### Optional (enrollment) | Claim | Description | | - | - | | `targetFundingSource` | **Staging only** for now — not yet supported in production. Used only when `transactionType` is `ENROLLMENT`; ignored otherwise. One of `BANK_ACCOUNT`, `BANK_CARD`, `PAYPAL`. Opens that funding source's flow the first time the user reaches the `FUNDING_SOURCE` step. Requires `FUNDING_SOURCE` in `steps`. An unknown value rejects the session. See [Opening a specific funding source](/developers/enrollment-widget#opening-a-specific-funding-source). | ## Generate the token Pick your language from the dropdown. Each example includes the install command at the top and signs the required claims; add any optional claims to the same payload. ```javascript JavaScript (Node.js) theme={null} // npm install jsonwebtoken import jwt from 'jsonwebtoken'; import { randomUUID } from 'node:crypto'; const amount = 0.0; // Amount in dollars and cents const transactionType = ''; // DEPOSIT or WITHDRAW const externalId = ''; // Your unique identifier for the user const appId = ''; // Your App ID (Overview tab) const secret = ''; // Your apiSecret const patToken = jwt.sign( { amount, appId, transactionType, externalId, jti: randomUUID(), }, secret, { algorithm: 'HS256', expiresIn: '1d' } ); console.log(patToken); ``` ```python Python theme={null} # pip install PyJWT import uuid from datetime import datetime, timedelta, timezone import jwt amount = 0.0 # Amount in dollars and cents transaction_type = "" # DEPOSIT or WITHDRAW external_id = "" # Your unique identifier for the user app_id = "" # Your App ID (Overview tab) secret = "" # Your apiSecret payload = { "amount": amount, "appId": app_id, "transactionType": transaction_type, "externalId": external_id, "jti": str(uuid.uuid4()), "exp": datetime.now(timezone.utc) + timedelta(days=1), } pat_token = jwt.encode(payload, secret, algorithm="HS256") print(pat_token) ``` ```ruby Ruby theme={null} # gem install jwt require 'jwt' require 'securerandom' amount = 0.0 # Amount in dollars and cents transaction_type = "" # DEPOSIT or WITHDRAW external_id = "" # Your unique identifier for the user app_id = "" # Your App ID (Overview tab) secret = "" # Your apiSecret payload = { amount: amount, appId: app_id, transactionType: transaction_type, externalId: external_id, jti: SecureRandom.uuid, exp: Time.now.to_i + (24 * 60 * 60) # 1 day } pat_token = JWT.encode(payload, secret, 'HS256') puts pat_token ``` ```go Go theme={null} // go get github.com/golang-jwt/jwt/v5 github.com/google/uuid package main import ( "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) func main() { amount := 0.0 // Amount in dollars and cents transactionType := "" // DEPOSIT or WITHDRAW externalId := "" // Your unique identifier for the user appId := "" // Your App ID (Overview tab) secret := "" // Your apiSecret claims := jwt.MapClaims{ "amount": amount, "appId": appId, "transactionType": transactionType, "externalId": externalId, "jti": uuid.NewString(), "exp": time.Now().Add(24 * time.Hour).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) patToken, err := token.SignedString([]byte(secret)) if err != nil { fmt.Println("Error generating token:", err) return } fmt.Println(patToken) } ``` ```java Java theme={null} // Maven: io.jsonwebtoken:jjwt-api, jjwt-impl, jjwt-jackson (0.12.x) import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.UUID; public class PatTokenGenerator { public static void main(String[] args) { double amount = 0.0; // Amount in dollars and cents String transactionType = ""; // DEPOSIT or WITHDRAW String externalId = ""; // Your unique identifier for the user String appId = ""; // Your App ID (Overview tab) String secret = ""; // Your apiSecret SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); long oneDayMs = 24L * 60 * 60 * 1000; String patToken = Jwts.builder() .claim("amount", amount) .claim("appId", appId) .claim("transactionType", transactionType) .claim("externalId", externalId) .id(UUID.randomUUID().toString()) .expiration(new Date(System.currentTimeMillis() + oneDayMs)) .signWith(key, Jwts.SIG.HS256) .compact(); System.out.println(patToken); } } ``` ```php PHP theme={null} $amount, "appId" => $appId, "transactionType" => $transactionType, "externalId" => $externalId, "jti" => Uuid::uuid4()->toString(), "exp" => time() + (24 * 60 * 60), // 1 day ]; $patToken = JWT::encode($payload, $secret, 'HS256'); echo $patToken; ``` ```csharp C# (.NET) theme={null} // dotnet add package System.IdentityModel.Tokens.Jwt using System; using System.IdentityModel.Tokens.Jwt; using System.Text; using Microsoft.IdentityModel.Tokens; class PatTokenGenerator { static void Main() { decimal amount = 0.0m; // Amount in dollars and cents string transactionType = ""; // DEPOSIT or WITHDRAW string externalId = ""; // Your unique identifier for the user string appId = ""; // Your App ID (Overview tab) string secret = ""; // Your apiSecret var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var payload = new JwtPayload { { "amount", amount }, { "appId", appId }, { "transactionType", transactionType }, { "externalId", externalId }, { "jti", Guid.NewGuid().ToString() }, { "exp", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds() } }; var token = new JwtSecurityToken(new JwtHeader(creds), payload); var patToken = new JwtSecurityTokenHandler().WriteToken(token); Console.WriteLine(patToken); } } ``` # Terminate a widget session Source: https://docs.fluz.app/developers/terminate-widget-session Cancel a payment you have already handed off to the widget, so the user can no longer complete it. You generate a `patToken`, hand it to the widget, and the user walks away — or your own system cancels the order a minute later. The token stays valid until whatever `exp` you signed it with, so without a way to revoke it the user can come back later and complete a payment you no longer want. `terminateWidgetSession` ends the session server-side. The user is refused the next time they move inside the widget, and any payout attempt against that session is rejected. > [!NOTE] > > **Prerequisites:** your application's API Key for Basic auth, and either the operator token you issued or its `jti`. ## Authentication This mutation is authenticated with your **application's** credentials, not a user access token — the same `Basic` header you already use for [`generateUserAccessToken`](/get-started/api-credentials): ```text theme={null} Authorization: Basic ``` Send the API Key from the Developer Console verbatim; it is already base64-encoded and decodes to the `app_id:apiSecret` pair the server checks. Use the key for the same application whose `apiSecret` signed the operator token. > [!WARNING] > > Staging and live are separate applications with separate credentials. A key from the wrong environment returns `401` with the same message as a malformed one — see [If the token request returns 401](/get-started/api-credentials) for the full checklist. You can only terminate sessions belonging to the application you authenticate as. A `jti` issued by a different application is not affected by your call — you will get a successful response with `wasActive: false`, and that application's session keeps running. Basic auth is not available to applications with `PERSONAL` status. ## Identifying the session Supply **either** the operator token or its `jti`. At least one is required. **By token (preferred)** Pass the same `patToken` you handed to the widget. It is verified against your application's secret, so a token that is not yours is rejected outright. An already-expired token is still accepted — terminating an expired session is harmless, and it means you do not have to track expiry yourself before calling. The token's own `exp` also bounds how long the termination is remembered, which is why this is the better option when you still hold it. **By jti** Use this when you no longer have the token. The `jti` is the UUID v4 you generated when you signed the `patToken` — see [Set Up Your Server](/developers/setting-up-your-server). It is also handed back to your callback URL as the `fluz_jti` query parameter, so you can terminate a session from a callback without having stored the token. Because there is no token to read an `exp` from, a `jti`-only termination is remembered for a fixed period rather than for exactly as long as the session could have been used. Supplying both is allowed as long as they describe the same session. The token wins, and the `jti` is treated as an assertion about it — a `jti` that disagrees with the token's own is rejected rather than silently ignored, so a mix-up cannot terminate the wrong session. ## Arguments * **`input`** (`TerminateWidgetSessionInput!`): identifies the session to terminate. ### TerminateWidgetSessionInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `token` | `String` | The operator token (`patToken`) you handed to the user to open the widget. | At least one of `token` or `jti` | | `jti` | `String` | The `jti` claim of that token. Same value delivered to your callback URL as `fluz_jti`. | At least one of `token` or `jti` | ## Sample mutation ```graphql theme={null} mutation { terminateWidgetSession(input: { jti: "11111111-1111-4111-8111-111111111111" }) { jti wasActive terminatedAt } } ``` ## cURL example ```curl theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Basic YOUR_API_KEY' \ -d '{ "query": "mutation { terminateWidgetSession(input: { jti: \"11111111-1111-4111-8111-111111111111\" }) { jti wasActive terminatedAt } }" }' ``` ## Sample response ```json theme={null} { "data": { "terminateWidgetSession": { "jti": "11111111-1111-4111-8111-111111111111", "wasActive": true, "terminatedAt": "2026-09-01T14:32:07.412Z" } } } ``` ## Response fields | Field | Type | Description | | :- | :- | :- | | `jti` | `String!` | The session identifier that was terminated. | | `wasActive` | `Boolean!` | `true` when the session had a live reservation that this call revoked — the user was in the widget, or could have been. `false` when there was nothing live to revoke. Informational only; the session is terminated either way. | | `terminatedAt` | `DateTime!` | When the termination was recorded. | > [!NOTE] > > **`wasActive: false` is a success, not a failure.** It is the normal answer when the user never opened the widget — which is also the safest moment to cancel. Terminating a session that was never opened is fully supported and is the recommended way to call off a payment you have already handed over. ## When termination is refused Termination is idempotent — terminating an already-terminated session succeeds. It is refused in exactly two cases, each meaning the money question is already settled: | Error | Code | Status | What it means | | :- | :- | :- | :- | | `WidgetSessionInProgress` | `WIDGET-006` | `409` | The session's payout is currently executing. Wait for the outcome — do not retry blindly. You will receive a completion or failure event either way. | | `WidgetSessionAlreadyCompleted` | `WIDGET-007` | `409` | The payment already went through. There is nothing left to terminate. | Other errors you may see: | Error | Code | Status | Cause | | :- | :- | :- | :- | | `MissingParameter` | `WIDGET-001` | `400` | Neither `token` nor `jti` supplied, or the token has no `jti` claim. | | `InvalidParameter` | `WIDGET-002` | `400` | The token does not verify against your application's secret, was not issued for this application, or the supplied `jti` disagrees with the token's own. | | `MisconfiguredApplication` | `WIDGET-003` | `400` | The application is not configured to open widget sessions. | ## What the user sees Termination takes effect on the user's **next navigation or refresh** inside the widget. It does not close a screen that is already rendered. When they next move, they are shown a "Session ended" message naming your application and telling them to close the window and start again from your product. If they get as far as confirming a payment, that confirmation is refused with the same message, and the payout is rejected server-side with `WidgetSessionTerminated` (`WIDGET-005`, `410`). > [!WARNING] > > Terminating does **not** by itself stop a payout that has already started executing — that case returns `409` instead, and you should wait for the completion or failure event rather than assuming the money is stopped. ## How long a termination is remembered A terminated session is refused for as long as it could otherwise have been used: * **Terminated by token** — until the token's own `exp`, and never less than one hour. * **Terminated by `jti` alone** — for 30 days. Nothing bounds how long you may sign an operator token for, so without an `exp` to read the termination is kept well past any plausible session lifetime. After that window the record is discarded. In practice the operator token has long since expired, so the session cannot be used regardless. ## Next steps > **[Set up your server](/developers/setting-up-your-server)** > > Generate the `patToken` and the `jti` this mutation takes. > **[Embed the widget](/developers/adding-the-js-widget-to-your-page)** > > Script tag, init call, button binding. > **[Embedded Widgets overview](/developers/widgets)** > > How widget sessions, OAuth grants, and pre-approved transaction tokens fit together. > **[Idempotency](/docs/idempotency-requests)** > > Why every money-moving call needs a unique `jti`. # Embedded widgets overview Source: https://docs.fluz.app/developers/widgets Drop a hosted Fluz flow into your own product to collect user permissions, capture sensitive data inside our PCI scope, and confirm money movement — then run everything else server-side over the API. ## What a widget actually is A Fluz Widget is a hosted, Fluz-rendered flow that you embed in your own site with a few lines of JavaScript. It runs on your domain, under your branding, either in a modal on top of your page or, on staging only for now, [inline in a container you size yourself](/developers/adding-the-js-widget-to-your-page#render-the-widget-inline-instead-of-in-a-modal). It exists to do three jobs that you should not have to build yourself: > **Collect the user's permission** > > The widget is how an end user creates or logs into their Fluz account and **grants your application the scopes it needs to act on that account**. No grant, no API access. > **Capture sensitive data** > > Card numbers, SSNs, identity documents, and PINs are collected by Fluz, inside Fluz's PCI DSS environment, and encrypted on our side. They never touch your servers. > **Confirm money movement** > > The user sees and approves the amount and direction of a transfer in a surface they can trust, which is what turns a pre-authorized token into a completed transaction. [Demo a widget: payouts](https://demos.fluz.app/payout-prepaid/?launch-card=1\&subtitle=Send%20a%20payout%20and%20watch%20it%20become%20a%20spendable%20card.\&image=https%3A%2F%2Fdemos.fluz.app%2Fassets%2Fprepaid-demo-shot.png\&image-dark=https%3A%2F%2Fdemos.fluz.app%2Fassets%2Fprepaid-demo-shot-dark.png) Everything else — issuing the card, pulling the funds, checking the balance, reading transactions — is yours to do over the API, on your own schedule, with no user present. > [!NOTE] > > **The mental model:** the widget is a *consent and sensitive-data surface*, not a product. It is the narrow, high-compliance part of the flow. The API is where the work happens. *** ## The division of labor | Job | Widget | API | | :- | :- | :- | | Create the user's Fluz account | ✅ | ✅ [User Registration](/docs/user-registration) | | Verify identity (KYC) | ✅ | ✅ [User KYC Verification](/docs/user-kyc-verification) | | Verify a business (KYB) | — | ✅ [Business Registration](/docs/business-registration) | | Obtain scope grants from the user | ✅ | ✅ [OAuth grant flow](/docs/grant-widget-user-permissions-todo) | | Collect a card PAN / CVV | ✅ | Tokenized only | | Collect an SSN or ID document | ✅ | Pass-through if you already hold it | | Set a transaction PIN | ✅ | — | | Confirm a specific transfer amount | ✅ | — | | Link a bank account via Plaid | ✅ | ✅ [Funding Sources](/features/funding-sources) | | Issue, edit, lock, or reveal a virtual card | — | ✅ [Virtual Cards](/features/virtual-cards) | | Deposit, withdraw, transfer, send money | — | ✅ [Wallets & Transfers](/features/move-funds-with-external-accounts) | | Buy gift cards, read the catalog | — | ✅ [Merchant Catalog](/merchant-catalog) | | Read transactions, annotate, run approvals | — | ✅ [Transactions](/features/get-all-transactions) | | Bulk-issue up to 10,000 cards | — | ✅ [Bulk Operations](/features/virtual-cards) | *** ## You can run everything behind the scenes This is the most commonly missed point about the widget: **it is not the only way to use Fluz, and it is not the way most work gets done.** Once a user has granted your application scopes — whether through the widget or through the standalone [OAuth grant flow](/docs/grant-widget-user-permissions-todo) — your server holds a user access token. From that point, every capability listed on the [API Features](/features) page is available to you programmatically, with no widget open and no user watching: > **[Funding sources](/features/funding-sources)** > > Link bank cards and Plaid bank accounts, then pull funds on demand. > **[Wallets & transfers](/features/move-funds-with-external-accounts)** > > Open spend accounts, deposit, withdraw, and move funds internally or across accounts. > **[Virtual cards](/features/virtual-cards)** > > Spend controls, lock/unlock, PINs, wallet provisioning, bulk issuance. > **[Open loop cards](/features/open-loop-cards/send-open-loop-cards)** > > Generate hosted card links recipients claim, with full link lifecycle control. > **[Send money](/features/account-to-account-transfers)** > > Look up recipients by phone or email and transfer to other Fluz wallets. > **[Approvals & authorized users](/features/approvals-and-requests)** > > Add team members, issue them cards, and route approval requests. The widget's job is to get you to the token. What you do after that is entirely server-side. *** ## Pick how much of the flow you hand to us You do not have to choose "all widget" or "all API." Most integrations land somewhere in the middle, and the deciding factor is usually **what sensitive data you already hold and want to keep holding.** **Full widget** **You hand us the whole user journey.** The widget handles account creation, phone + 2FA login, KYC, PIN setup, the permissions grant, and the transaction confirmation. You render a button and generate a signed token. * Fastest path to production — measured in hours, not sprints. * Zero PCI scope, zero CIP data handling on your side. * Least control over look and feel between the click and the callback. **Good fit:** payout and withdrawal flows, marketplaces, gig platforms, rewards programs — anywhere you want money to leave your system without you becoming a financial institution. **Hybrid (most common)** **You own the parts you already own; we own the parts you'd rather not.** Register the user yourself with [`registerUser`](/docs/user-registration) using the profile data you already collected at signup. Run KYC yourself with [`verifyUserInformation`](/docs/user-kyc-verification) if you already hold the SSN and address. Then open the widget only for the steps that genuinely need it: * the permissions grant, * document upload when KYC comes back `DECLINED` or needs review, * card PAN capture, * PIN setup, * the transaction confirmation screen. Your onboarding stays yours. The user never re-types information you already have. The widget appears for a narrow, obviously-financial moment and then gets out of the way. **Good fit:** platforms with an existing KYC'd user base, fintechs, anyone who has already done identity verification and does not want to make the user do it twice. **Headless / API only** **No widget at all.** Register users, verify them, link funding sources, issue cards, and move money entirely over the API. Obtain scope grants through the standalone [OAuth authorization flow](/docs/grant-widget-user-permissions-todo) — a redirect, not an embed — or operate on your own platform account. * Full control over every pixel. * **You** are responsible for PCI DSS scope if you collect card data, and for the security of any CIP data you handle. * Some flows still require a hosted surface: collecting identity documents is the usual holdout. Showing a user their own card details no longer is — [Secure Elements](/build-a-platform/secure-elements-overview) mounts a Fluz-hosted frame inline in your own layout, so you can render card details in your own UI without opening the widget modal or holding a PCI certification. **Good fit:** bulk issuance, back-office operations, disbursement runs, ERP and accounting integrations, and any flow with no end user in the loop. > [!NOTE] > > **On registering users via API:** if you register and KYC a user yourself and *then* open the widget, pass `externalId` in the pre-approved transaction token so we can match the session to the account you already created rather than starting a new one. You can also pass `phoneNumber`, `firstName`, `lastName`, `email`, and `username` to skip the corresponding steps in the widget. See [Set Up Your Server](/developers/setting-up-your-server). *** ## How widgets relate to OAuth applications A widget **is** an OAuth application. It is not a separate object with a separate permission model — it is an OAuth app that ships with an embeddable front end. **1. You define the ceiling (app scopes)** On the **Permissions** tab of your app, you select the scopes your application is allowed to request. This is the maximum your app can ever ask for, regardless of what any individual user agrees to. Scopes that a given widget type cannot function without are grouped at the bottom of the tab and cannot be unselected. See [Application Scopes](/docs/application-scopes) for the full list — `MAKE_DEPOSIT`, `MAKE_WITHDRAW`, `LIST_PAYMENT`, `CREATE_VIRTUALCARD`, `REVEAL_VIRTUALCARD`, `PURCHASE_GIFTCARD`, and the rest. **2. You configure where the grant can go (OAuth tab)** **Origin** — the domain hosting the widget. **Redirect URIs** — where our authorization server may send the user back, no query parameters, and it must match exactly at exchange time. **Webhook URLs** — one or many REST endpoints, each optionally subscribed to specific events; a URL with no events selected becomes a catch-all. See [Configure App Widget](/developers/configure-app-widget). **3. The user sets the floor (user scopes)** When the widget opens, the user is shown the scopes you requested — grouped under readable top-level headers rather than listed as raw enum values — and approves them. Anything they decline is simply not granted. **4. Both grants must be live** An application's effective permissions are the **intersection** of the app-level grant and the user-level grant, and both must be unexpired. This is enforced at `generateUserAccessToken`, not at call time — so a revoked or lapsed grant surfaces as a token failure, not a mysterious mid-flow error. **5. Code becomes tokens** The grant produces an authorization `code` at your redirect URI. Exchange it at `/token/exchange` with a Basic auth header of `client_id:client_secret` for an `accessToken`, a `refreshToken`, and the confirmed `scope` array. See [Exchanging an authorization code](/docs/exchanging-an-oauth-authorization-code) and [Refreshing an access token](/docs/refreshing-an-oauth-access-token). > [!WARNING] > > The pre-approved transaction token (`patToken`) and the OAuth access token are **different things** and do different jobs. The `patToken` is a short-lived, single-transaction JWT signed with your `apiSecret` that authorizes *one* movement of *one* amount. The OAuth `accessToken` is what lets your server act on a user's account over time. A widget session typically involves both. *** ## PCI compliance and sensitive data When the widget is open, the sensitive fields inside it are Fluz's, not yours. The user is typing into our iframe, posting to our servers, under our compliance program. That means Fluz takes responsibility for: * **Card data.** PANs, expiration dates, and CVVs are captured and stored in accordance with PCI DSS requirements and encrypted at rest on our side. Your page never sees them, your logs never contain them, and your infrastructure stays out of PCI scope for these flows. * **Full card reveal.** Showing an end user their own virtual card number happens on a Fluz-hosted surface — either inside the widget, or inline in your own page via [Secure Elements Card Reveal](/build-a-platform/card-reveal). Either way the data renders in Fluz's frame, not yours, so you stay out of PCI scope. Taking the raw PAN and CVV onto your own servers is the only path that requires a PCI DSS certification — see [Due diligence](/build-a-platform#due-diligence). * **CIP and identity data.** SSNs, dates of birth, addresses, and uploaded identity documents are collected and retained inside our verification environment. * **PINs.** Set and stored by us, never transmitted to you. * **Bank credentials.** Plaid link flows run inside the widget; you never handle the user's banking login. What stays your responsibility: your `apiSecret` and `client_secret`. The Installation tab renders working snippets that contain your real credentials, which is convenient and also a hazard — **generate the `patToken` on your server, never in browser JavaScript.** Anything in your page source is public. > [!NOTE] > > Fluz maintains SOC 2 Type II controls and handles card data in accordance with PCI DSS requirements. If your compliance team needs documentation for a vendor review, contact your Fluz account manager. *** ## Getting your embed code You do not hand-write the integration. The **Installation** tab of your app generates it for you, pre-filled with your app's `appId`, and gives you two selectors: **Transaction Type** — choose the direction of money movement: | Installation tab label | `transactionType` in the JWT | What happens | | :- | :- | :- | | **Pay-In** | `DEPOSIT` | The user moves funds into Fluz and on into your operator spend account. Money flows *toward* your platform. | | **Payout** | `WITHDRAW` | Funds move from your operator spend account to the user's Fluz account. Money flows *toward* your user. | Not every widget type moves money. [`ENROLLMENT`](/developers/enrollment-widget) runs a user through account setup — identity verification, funding sources, PIN — in an order you choose, with no amount and no transaction confirmation. **Server Language** — the snippet that generates the signed pre-approved transaction token, in the language your backend actually uses: > **JavaScript** > **Ruby** > **Python** > **Go** > **Java** > **PHP** > **C# / .NET** > **More** Switch the selector and the code block rewrites itself — correct JWT library, correct claim names, correct HS256 signing, correct one-day expiry. Copy it, drop in your `apiSecret` from your secret store, and you have a working token generator. Every variant is also documented in full at [Set Up Your Server](/developers/setting-up-your-server). The client-side half is a single script tag plus a `FluzEmbedded.init(...)` call. You can let us render the button, bind the modal to a button you already have, or, on staging only for now, skip the modal entirely and render the widget inline in a container you size yourself. See [Adding the JS Widget to Your Page](/developers/adding-the-js-widget-to-your-page). Your app's configuration lives at: ```text theme={null} https://fluz.app/for-developers/overview/{appId} ``` for example `https://fluz.app/for-developers/overview/19be9561-a6a1-4e02-8243-10ede908ef33`. The tabs across the top — **Overview**, **Permissions**, **OAuth**, **Installation** — map exactly to the steps above. *** ## Start from a template You do not start from a blank app. From the developer dashboard, choose **Browse templates** and pick the one closest to what you are building. A template pre-configures the app type, the required scopes, the transaction direction, and the sequence of screens the user will see — so a new app is functional the moment you finish naming it. Templates available today include: | Template | What it sets up | | :- | :- | | **Withdraw to a virtual Mastercard** | A `Payout` flow ending in an instantly usable Fluz virtual card. Pre-selects the withdrawal and card-creation scopes. | | **OAuth Integration** | A permissions-only app with no embedded UI — for headless and redirect-based integrations. | > [!NOTE] > > **Treat the template as a starting point, not a specification.** After it is created, go to the **Permissions** tab and shape the app around what you are actually trying to do — add the scopes your use case needs, remove the ones it does not. A payout widget that will later issue cards on the user's behalf needs `CREATE_VIRTUALCARD`; one that only moves cash does not. Requesting fewer scopes means a shorter consent screen and a higher completion rate, so ask for what you need and nothing more. Creating an app: [Add a New App Widget](/developers/add-a-widget) · Configuring it: [Configure App Widget](/developers/configure-app-widget) · Turning it off: [Disable or Delete Your App](/developers/disable-or-delete-your-app) *** ## What the end user sees Once a user reaches a page hosting your widget and takes the action that opens the modal: **1. Log in or register** The user authenticates to their Fluz account with a 2FA code sent to their phone. If they do not have an account, they create one here. Passing `phoneNumber` in the `patToken` skips straight to the code entry step. **2. KYC** If you already hold the user's SSN, pass it to us and we validate it. If not, the widget runs the full KYC flow. Responses are `APPROVED`, `DECLINED`, `DUPLICATE`, or `ERROR` — see [User KYC Verification](/docs/user-kyc-verification) for what each means and how many attempts a user gets. **3. Grant permissions** The user reviews and approves the scopes your app requested. **4. Set a PIN** A Fluz-wide security measure, prompted again later for actions requiring elevated confirmation. **5. Confirm the transaction** The user sees the amount and direction and either approves or dismisses. Either way, you get an event. ### Pay-In: funds into your platform > [!NOTE] > > Check the user's Fluz balance first to confirm they can cover the transaction. 1. The user enters a deposit amount and clicks your button. 2. The widget presents a confirmation screen. * **Confirmed** → we initiate the transfer from the user's spend account to yours. * **Denied or dismissed** → we send an event. 3. You receive a completion or failure event. 4. Verify your own Fluz balance to confirm settlement. ### Payout: funds out to your user > [!NOTE] > > Check your account's Fluz balance first. If you cannot cover the transfer, initiate a deposit from your funding source. Quarantine or hold the user's funds on your side to prevent double-spend while the transfer is in flight. 1. The user enters a withdrawal amount and clicks your button. 2. The widget presents a confirmation screen. * **Confirmed** → we initiate the transfer from your operator spend account to the user's. * **Denied or dismissed** → we send an event. 3. You receive a completion or failure event. 4. The widget shows the user their withdrawal is complete and gives them direct access to their Fluz virtual card. > [!NOTE] > > Every money-moving call needs a unique `jti` in the token for idempotency, and a unique `idempotencyKey` on the API side. See [Idempotency](/docs/idempotency-requests). *** ## Next steps > **[Add a widget](/developers/add-a-widget)** > > Create your first app from a template. > **[Configure OAuth & permissions](/developers/configure-app-widget)** > > Scopes, origins, redirect URIs, webhooks. > **[Set up your server](/developers/setting-up-your-server)** > > Generate the pre-approved transaction token in your language. > **[Embed the widget](/developers/adding-the-js-widget-to-your-page)** > > Script tag, init call, button binding. > **[Enrollment widget](/developers/enrollment-widget)** > > Run KYC, funding sources, and PIN in an order you choose. > **[Secure Elements](/build-a-platform/secure-elements-overview)** > > Show card details inline in your own page, without the widget modal. > **[Terminate a session](/developers/terminate-widget-session)** > > Cancel a payment you have already handed off, before the user completes it. > **[Everything the API can do](/features)** > > The full capability surface, all of it available server-side. > **[Build a platform](/build-a-platform)** > > Run every capability on connected accounts with customer-scoped tokens. # Discover connected users Source: https://docs.fluz.app/discover-connected-users Return the users connected to your application through active OAuth grants, with their external reference id and granted scopes. Returns the users connected to your application through active OAuth grants, with the `externalReferenceId` and granted scopes for each. Use it to see who you can target and what each user has authorized before calling other bulk operations. Returns identifiers and granted scopes only — **never user tokens**. ## Requirements * `Authorization: Basic ` (your application API key) * The **bulk API capability** on your application ## Query ```graphql theme={null} query GetBulkConnectedOAuthUsers($paginate: BulkPaginationInput) { getBulkConnectedOAuthUsers(paginate: $paginate) { totalCount hasNextPage users { externalReferenceId accountId userId scopes connectedAt } } } ``` ### Variables ```json theme={null} { "paginate": { "limit": 100, "offset": 0 } } ``` ## Response ```json theme={null} { "data": { "getBulkConnectedOAuthUsers": { "totalCount": 128, "hasNextPage": true, "users": [ { "externalReferenceId": "your-user-001", "accountId": "8f3c…", "userId": "b21a…", "scopes": ["LIST_PAYMENT", "LIST_PURCHASES"], "connectedAt": "2026-05-01T12:00:00.000Z" } ] } } } ``` ## Arguments | Argument | Type | Description | | - | - | - | | `paginate.limit` | `Int` | Users per page. Maximum and default are **100**. | | `paginate.offset` | `Int` | Number of users to skip. Defaults to `0`. | ## Response fields | Field | Description | | - | - | | `users[].externalReferenceId` | Your reference id for the user. `null` if none was set at connect time — such users are only reachable via `ALL_CONNECTED`. | | `users[].accountId` | The connected user's account id. | | `users[].userId` | The connected user's user id. | | `users[].scopes` | Scopes the user granted to your application. | | `users[].connectedAt` | When the user connected. | | `totalCount` | Total connected users across all pages. | | `hasNextPage` | Whether more users exist beyond this page. | ## Notes * The bulk API allows larger pages than the rest of the API — up to 100 per page. * Page with `offset` until `hasNextPage` is `false`. # Exchange an OAuth code Source: https://docs.fluz.app/exchange-an-o-auth-authorization-code ## Exchanging an auth code for an Access Token You can take the code received as the response to a permissions request and make a request to exchange for an Access Token and Refresh Token. To excxhange your authorization `code` for an access token, make a request to `https://uni.staging.fluzapp.com/token/exchange` with the following query params: | query param | description | | - | - | | code | The auth token above | | redirect\_uri | the redirect\_uri you used in the previous auth step. This uri must match exactly. | Additionally, set an Authorization header that is a base64 encoded string that is a combination of your client\_id:app\_secret. This is a Basic auth header, and so should follow the following format: `Authorization: Basic ` For example, if your `client_id` is `abc123` and your `client_secret` from the OAuth configuration is `def456`, the base64 encoded value would be `YWJjMTIzOmRlZjQ1Ng==`. The `client_id` and `client_secret` can be found in the `Overview` tab of the [Configure OAuth App](/configure-o-auth-app). Here is an example cURL command: ```text theme={null} curl -X GET "https://uni.staging.fluzapp.com/token/exchange?code=&redirect_uri=" -H "Authorization: Basic YWJjMTIzOmRlZjQ1Ng" ``` The response will include: | query param | description | | :- | :- | | accessToken | Short-lived token for subsequent requests into Fluz‘s backend services | | refreshToken | refresh token to store and use when the accessToken becomes expired | | scopes | the values the user permitted through the previous flow | Here is an example of a full response: ```json theme={null} {"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTc1MjA5NDgwNH0.0dKCtVpN0mTHHMuGNNx4VLJisTnovFNPQKhSw6zWosc","authorizationCode":"f082972eb110b80f73b0d0f95d1de9266069a1e4","accessTokenExpiresAt":"2025-07-08T21:05:30.673Z","refreshToken":"8ec16c25951616150b0332a4a6d66547","refreshTokenExpiresAt":"2025-08-08T20:55:30.738Z","scope":\["MAKE\_WIDTHDRAW","MAKE\_DEPOSIT","LIST\_PAYMENT"],"client":\{"id":"dab5c80e-0321-4c3a-988a-ffedfd64d8db","app\_id":"0a92d46e-edf4-422e-8c62-946051e5067b","app\_name":"First OAuth integration","grants":\["authorization\_code","client\_credentials","password","refresh\_token"],"redirectUris":\["http\://localhost:3035/oauth/finalize"],"accessTokenLifetime":600},"user":\{"id":"5070d5a1-d71a-4190-91b0-f116eec51771"}} ``` # Activity history overview Source: https://docs.fluz.app/features/activity-history Read the audit trail of a virtual card, gift card, spend account, or funding source: who created it, who changed what, who it was shared with, and which approval authorized each change. Activity history is the audit trail for the things on a Fluz account. Fluz tracks four kinds of objects. Each one keeps its own list of every change ever made to it. You can read that list for any single object with one query: `objectChangeHistory`. It works in three parts: 1. **Objects.** There are four: virtual cards, gift cards, spend accounts, and funding sources. 2. **Events.** Every change to an object is saved as an event. The object was created, a field changed, it was shared, an approval was requested or decided. Events are never edited or deleted. 3. **One query.** Pass the object's type and ID to `objectChangeHistory` and you get its events back, newest first. ```mermaid theme={null} flowchart LR subgraph OBJ["1. Objects"] VC["Virtual card
VIRTUAL_CARDS"] GC["Gift card
GIFT_CARDS"] SA["Spend account
SPEND_ACCOUNTS"] FS["Funding source
FUNDING_SOURCES"] end subgraph EV["2. Events recorded on each object"] VCE["Created, settings and limits, lock and unlock,
PIN and wallet, memos and receipts, sharing, approvals"] GCE["Purchased, archived, balance edited,
memos and receipts, sharing, approvals"] SAE["Created, renamed, default changed,
closed, ownership, access"] FSE["Added, verified, edited, removed,
set as primary or backup"] end VC --> VCE GC --> GCE SA --> SAE FS --> FSE VCE --> Q GCE --> Q SAE --> Q FSE --> Q Q["3. objectChangeHistory(objectType, objectId)"] --> R["Every event on that object,
newest first"] ``` One query works for all four objects, and every event has the same shape. Only the events differ. See the [Event catalog](/features/activity-history-events) for every event on every object. *** ## The four objects | Object | What it is | `objectType` | Pass as `objectId` | Events | | - | - | - | - | - | | Virtual card | A card number Fluz issues on the account. | `VIRTUAL_CARDS` | `virtualCardId` | [Virtual card activity history](/features/virtual-card-activity-history) | | Gift card | A merchant gift card bought through Fluz. | `GIFT_CARDS` | `giftCardId` | [Gift card activity history](/features/gift-card-activity-history) | | Spend account | A balance on the account that holds money and funds cards. | `SPEND_ACCOUNTS` | `userCashBalanceId` | [Spend account activity history](/features/spend-account-activity-history) | | Funding source | An outside bank card, bank account, or PayPal account linked to the account. | `FUNDING_SOURCES` | `bankCardId`, `bankAccountId`, or `paypalVaultId` | [Funding source activity history](/features/funding-source-activity-history) | Nothing else has activity history. Authorized users, approvals, share links, and developer applications aren't objects here. See [Approval and authorized user history](#approval-and-authorized-user-history) for how to trace them. *** ## What's in an event Every event answers the same five questions, whatever the object: ```mermaid theme={null} flowchart LR E["One event"] --> W1["What happened?
changeType + change"] E --> W2["Who did it?
actor: a user or a Fluz system,
plus the app it came through"] E --> W3["Who was it for?
subjectUserId"] E --> W4["Who approved it?
approvalId + approval"] E --> W5["When?
createdAt"] ``` For example, the first event in the [sample response](#sample-response) below reads: *On October 8, a cardholder unlocked the card (`status`: `LOCKED` → `UNLOCKED`), after a manager approved the request through the Acme Expenses app (`approvalId`).* > [!NOTE] > > **Activity history records actions on the object, not reads or spend.** > > Revealing card details, viewing balances, and listing objects aren't recorded. Neither are card transactions, authorizations, or refunds. To read spend, use [Transaction activity](/features/transactions-details-overview). *** ## Query activity history `objectChangeHistory` Returns the newest events first, up to 50 per object. **Required scope:** `VIEW_OBJECT_CHANGE_HISTORY` > [!NOTE] > > **Prerequisites:** the `VIEW_OBJECT_CHANGE_HISTORY` scope enabled on your application. The query accepts both Bearer (a user access token that carries the scope) and Basic (``) authentication. The object must belong to the account the credentials act on. ### Arguments | Argument | Type | Description | Required | | :- | :- | :- | :- | | `objectType` | `ObjectOwnerObjectType!` | `VIRTUAL_CARDS`, `GIFT_CARDS`, `SPEND_ACCOUNTS`, or `FUNDING_SOURCES`. | Yes | | `objectId` | `UUID!` | The ID of the object, as returned by the queries that list it. | Yes | ### Sample query ```graphql theme={null} query { objectChangeHistory( objectType: VIRTUAL_CARDS objectId: "07df5653-43a8-4532-9881-3ab5857bbe11" ) { id changeType change createdAt subjectUserId approvalId actor { userId system applicationId applicationName } } } ``` ### cURL example ```bash theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "query { objectChangeHistory(objectType: VIRTUAL_CARDS, objectId: \"07df5653-43a8-4532-9881-3ab5857bbe11\") { id changeType change createdAt subjectUserId approvalId actor { userId system applicationId applicationName } } }" }' ``` ### Sample response A card that an authorized user asked to unlock, and a manager approved through the API. The newest event comes first. ```json theme={null} { "data": { "objectChangeHistory": [ { "id": "4b1f7c2e-9d3a-4f61-8e2b-7a5c0d9e1f23", "changeType": "FIELD_CHANGED", "change": { "field": "status", "old": "LOCKED", "new": "UNLOCKED" }, "createdAt": "2026-10-08T14:02:11.482Z", "subjectUserId": null, "approvalId": "07df5653-43a8-4532-9881-3ab5857bbe12", "actor": { "userId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "system": null, "applicationId": null, "applicationName": null } }, { "id": "2d6e8a1b-3c5f-4e7a-9b0d-1f2e3a4b5c6d", "changeType": "APPROVAL_APPROVED", "change": { "approval_type": "VIRTUAL_CARD", "approval_code": "600005", "decision_channel": "API" }, "createdAt": "2026-10-08T14:02:10.917Z", "subjectUserId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "approvalId": "07df5653-43a8-4532-9881-3ab5857bbe12", "actor": { "userId": "00a2ec0a-255f-4fe7-85ad-2958dc8d3c72", "system": null, "applicationId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "applicationName": "Acme Expenses" } }, { "id": "8c9d0e1f-2a3b-4c5d-9e6f-0a1b2c3d4e5f", "changeType": "APPROVAL_REQUESTED", "change": { "approval_type": "VIRTUAL_CARD", "approval_code": "600005" }, "createdAt": "2026-10-08T13:47:55.120Z", "subjectUserId": null, "approvalId": "07df5653-43a8-4532-9881-3ab5857bbe12", "actor": { "userId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "system": null, "applicationId": null, "applicationName": null } } ] } } ``` *** ## Response fields `objectChangeHistory` returns a list of `ObjectChangeHistoryEvent`. | Field | Type | Description | | - | - | - | | `id` | `UUID!` | The event ID. | | `objectType` | `ObjectOwnerObjectType!` | The object type you queried. | | `objectId` | `UUID!` | The object you queried. | | `changeType` | `String!` | What happened. See [Change types](#change-types). | | `change` | `JSON` | The details of the change. The shape depends on `changeType`. See [The change payload](#the-change-payload). | | `actor` | `ObjectChangeHistoryActor!` | Who made the change. See [Who made the change](#who-made-the-change). | | `subjectUserId` | `UUID` | The user the change was made for, when that's a different person from the actor. For example, the cardholder of a card an admin created. | | `approvalId` | `UUID` | The approval linked to this event, if any. | | `approval` | `ObjectChangeHistoryApproval` | The linked approval and its approvers. See [Approvals](#approvals). | | `createdAt` | `DateTime!` | When the change happened. | *** ## Change types | `changeType` | Meaning | | - | - | | `OBJECT_CREATED` | The object was created. | | `FIELD_CHANGED` | One field of the object changed. A change to several fields at once produces one event per field. | | `OWNER_ASSIGNED` | The object got its first owner. | | `OWNER_TRANSFERRED` | Ownership moved to another user. | | `ACCESS_GRANTED` | A user got access to the object. | | `ACCESS_LEVEL_CHANGED` | A user's access level on the object changed. | | `ACCESS_REVOKED` | A user lost access to the object. | | `PENDING_ACCESS_GRANTED` | Access was granted to someone who was invited but hasn't registered yet. | | `PENDING_ACCESS_LEVEL_CHANGED` | The pending access level of an invited person changed. | | `PENDING_ACCESS_REVOKED` | The pending access of an invited person was revoked, for example because the invite was cancelled. | | `ACCESS_APPLIED_ON_REGISTRATION` | An invited person registered, and their pending access took effect. | | `APPROVAL_REQUESTED` | Someone asked for approval to act on the object. | | `APPROVAL_APPROVED` | An approver approved the request. | | `APPROVAL_DECLINED` | An approver declined the request. | | `APPROVAL_EXPIRED` | The request expired before anyone decided it. | Treat `changeType` as an open list: new values can appear as Fluz records more actions. *** ## The change payload `change` is a JSON object. Its keys use `snake_case`. | `changeType` | `change` | | - | - | | `OBJECT_CREATED` | Usually `{}`. A virtual card created by a schedule or a CSV import says so. See [Virtual card activity history](/features/virtual-card-activity-history#creation). | | `FIELD_CHANGED` | `{ "field", "old", "new" }`, plus optional context keys such as `reason` or `transaction_log_id`. Each object page lists its fields and values. | | `OWNER_ASSIGNED`, `OWNER_TRANSFERRED` | `{ "field": "user_id", "old", "new" }`. `old` and `new` are user IDs. `old` is `null` for a first owner. | | `ACCESS_*`, `PENDING_ACCESS_*` | `{ "field": "role", "old", "new" }`. `old` and `new` are [access levels](#ownership-and-access); `null` means no access. | | `APPROVAL_*` | `{ "approval_type", "approval_code", "decision_channel" }`. `decision_channel` is only on approve and decline events. | Fluz never puts sensitive values in `change`. PINs, card numbers, wallet tokens, address text, and memo or receipt content are replaced by a marker such as `"updated"`, `"UPDATED"`, or `"ADDED"`, or by an ID. *** ## Who made the change `actor` names the one party that performed the action. | Field | Description | | - | - | | `userId` | The user who made the change. When someone acts for another person, this is the person acting, not the person it was done for, except where an object page notes otherwise. | | `system` | Set when no user made the change: the Fluz service or job that did it, such as `virtual-card-service` or `approvals-service`. `cms` means Fluz support. The support agent's identity isn't returned. | | `applicationId` | The connected application the change came through. `null` for changes made in the Fluz web or mobile app, by Fluz support, or by a Fluz system. | | `applicationName` | The application's name at the time of the change. Renaming the application later doesn't change past events. | Exactly one of `userId` and `system` is set. Each object page lists the `system` values you can see for that object. Treat `system` values as labels for display and auditing: new values can appear, and existing ones can change. With API key (Basic) authentication, `userId` is the user who owns your developer account and `applicationId` is your application. `subjectUserId` names the person the change was made for, when that's someone other than the actor. For example, an admin creates a card for an authorized user: `actor.userId` is the admin, and `subjectUserId` is the cardholder. *** ## Approvals When a change goes through [Approvals & requests](/features/approvals-and-requests), the object's history shows the whole trail. Every event in it carries the same `approvalId`. 1. **`APPROVAL_REQUESTED`.** The actor is the requester, with the application the request came through. 2. **`APPROVAL_APPROVED` or `APPROVAL_DECLINED`.** One event per approver who decided. The actor is the user who submitted the decision, and `subjectUserId` is the requester. A decision made through an approval link that names no approver is attributed to `system: "approvals-service"`. 3. **The resulting action.** For example, a `FIELD_CHANGED` event that unlocks the card. A change to the object itself is usually attributed to the requester, because they asked for it. An access grant is attributed to the approver who granted it. Each object page notes the exceptions. If nobody decides before the request expires, an `APPROVAL_EXPIRED` event is recorded instead, attributed to `system: "approvals-service"`. For a request that creates a new object, such as a virtual card or gift card purchase, the new object gets the request and approval events once it exists. A declined or expired creation request doesn't create an object, so the new object has no history. A request to replace a virtual card records its decline or expiry on the original card. The `approval` field returns the approval as it is now: | Field | Description | | - | - | | `approvalId` | The approval ID. | | `approvalType` | The approval type, such as `VIRTUAL_CARD`, `GIFT_CARD`, or `ACCOUNT_ACCESS`. | | `status` | The approval's current status. | | `requesterUserId` | The user who asked for approval. | | `approvers` | Everyone asked to decide, in the order they were added. | | `approvers.approverUserId` | The user who was asked to decide. | | `approvers.decidedByUserId` | The user who actually submitted the decision. `null` when no user was named, for example a decision through an account-level link. | | `approvers.status` | That approver's status. | | `approvers.decidedAt` | When they decided. | | `approvers.decisionChannel` | How the decision was submitted: `LINK` (an email or SMS approval link), `REST` (the approvals inbox in the Fluz app), or `API` (`approveApprovalRequest` or `declineApprovalRequest`). | Fluz never asks a requester to approve their own request, and rejects the decision if they try. *** ## Ownership and access Ownership and access events work the same way on every object type that supports them. * **Ownership.** [`assignObjectOwner`](/api-reference/mutations/assign-object-owner) records `OWNER_ASSIGNED`. [`transferObjectOwner`](/api-reference/mutations/transfer-object-owner) records `OWNER_TRANSFERRED`. New virtual cards and gift cards also record `OWNER_ASSIGNED`. Spend accounts created in the Fluz app record it for the owner picked, or for the creator. * **The owner's access.** An owner always gets at least `SPENDER` access. When that access is granted, raised, or removed, the access event carries `"reason": "owner-floor"`. For example, a transfer revokes the previous owner's `SPENDER` access. * **Sharing.** Sharing an object with a member of the account in the Fluz app records `ACCESS_GRANTED`, `ACCESS_LEVEL_CHANGED`, or `ACCESS_REVOKED`. `subjectUserId` is the member. * **Invites.** Sharing with someone who hasn't registered yet records `PENDING_ACCESS_*` events. Those events have no `subjectUserId`; `change.subject_non_registered_user_id` identifies the invite. When the person registers, `ACCESS_APPLIED_ON_REGISTRATION` is recorded and attributed to the person who invited them. * **Access requests.** A member who asks for access to an object goes through an approval. The object records `APPROVAL_REQUESTED`, then `APPROVAL_APPROVED` or `APPROVAL_DECLINED`. If the request is approved, an `ACCESS_GRANTED` event follows, or `ACCESS_LEVEL_CHANGED` if the requester already had lower access. It's attributed to the approver, with the requester as `subjectUserId`. If the requester already had that level or higher, no access event is recorded. Access levels in `change.old` and `change.new`: | Level | Value | | - | - | | `GUEST` | `0` | | `VIEWER` | `1` | | `SPENDER` | `10` | | `MANAGER` | `100` | | `ADMIN` | `1000` | Access a user has through a group isn't recorded on the object. *** ## Approval and authorized user history Approvals and authorized users aren't objects in activity history. There's no `objectType` for them and no separate history query. What they did is recorded on the objects they touched. ### Approvals * **The trail behind one change.** Every event in an approval's trail carries the same `approvalId`. Ask for the `approval` field to get the approval's type, status, requester, and each approver's decision, channel, and time. * **Approvals still waiting.** [`approvalRequests`](/api-reference/queries/approval-requests) lists the account's open requests. Once a request is approved, declined, or expires, it drops off that list. Its record lives on in the object's history. * **Not available.** A single list of every past approval on the account, and the history of an approval request that never touched one of the four objects, such as a transfer or reimbursement request. ```graphql theme={null} query { objectChangeHistory( objectType: VIRTUAL_CARDS objectId: "07df5653-43a8-4532-9881-3ab5857bbe11" ) { changeType createdAt approvalId approval { approvalType status requesterUserId approvers { approverUserId decidedByUserId status decidedAt decisionChannel } } } } ``` ### Authorized users * **What an authorized user did.** Events where `actor.userId` is their user ID. * **What was done for them.** Events where `subjectUserId` is their user ID. For example, a card an admin issued to them, or access they were given. * **Across objects.** History is read one object at a time. To see everything a user did, query each object they can reach and filter on `actor.userId`. * **Not recorded.** Adding or removing an authorized user, changing their roles, supervisor assignments, and access they get through a group. *** ## Good to know * **History starts when recording started.** Changes made before Fluz began recording an action don't appear, and nothing is backfilled, with one exception. Virtual cards that existed when ownership launched have an `OWNER_ASSIGNED` event dated when ownership launched, not when the card was created. * **Only real changes are recorded.** An update that sets a field to its current value records nothing. * **Recording doesn't block most actions.** If an event can't be written, the action still succeeds. Most ownership and access events are the exception: they're written together with the change. Use the object's current state, not its history, as the source of truth for its current values. * **The newest 50 events.** The query returns at most 50 events, newest first, with no pagination. * **Changes from every channel.** The history includes changes made through the API, in the Fluz web and mobile apps, by Fluz support, and by Fluz systems. *** ## Access rules The object must belong to the account the credentials act on: | Authentication | Account | | - | - | | Bearer (user access token) | The account the token was issued for. | | Basic (``) | The application's configured operator account. | For an OAuth grant restricted to specific spend accounts, `SPEND_ACCOUNTS` history is only available for those spend accounts. Asking for an object on another account returns the same error as asking for one that doesn't exist, so the API never reveals whether the object exists. ## Errors | Code | When it happens | | - | - | | `AUTH-0031` | The Bearer token doesn't carry `VIEW_OBJECT_CHANGE_HISTORY`. | | `AUTH-0043` | The object doesn't exist, or it isn't on the account the credentials act on. | See [General API error codes](/get-started/general-api-error-codes) for authentication errors shared by every operation. ## Next steps > **[Event catalog](/features/activity-history-events)** > > Every event on every object, in one place. > **[Virtual card activity history](/features/virtual-card-activity-history)** > > Creation, settings, locks, PINs, wallet tokens, and expense details. > **[Gift card activity history](/features/gift-card-activity-history)** > > Purchase, archive, used status, balance, and expense details. > **[Spend account activity history](/features/spend-account-activity-history)** > > Creation, renames, default changes, images, and closure. > **[Funding source activity history](/features/funding-source-activity-history)** > > Bank cards, bank accounts, PayPal, and primary or backup changes. # Activity history event catalog Source: https://docs.fluz.app/features/activity-history-events Every event Fluz records in activity history, by object: the changeType, the field, the values, and what records it. This page lists every event `objectChangeHistory` can return, grouped by object. Each row is one event you can get back. For the query itself, actors, and access rules, see [Activity history overview](/features/activity-history). How to read the tables: * **`changeType`** is the kind of event. * **`field`** is set on `FIELD_CHANGED` events and names what changed. * **`old` → `new`** are the values in `change.old` and `change.new`. Treat every list here as open. Fluz adds events as it records more actions. *** ## Events on every object These work the same way on each object that supports them. See [Ownership and access](/features/activity-history#ownership-and-access) and [Approvals](/features/activity-history#approvals). | Event | `changeType` | `change` | Objects | | - | - | - | - | | Created | `OBJECT_CREATED` | Usually `{}` | All four | | Owner assigned | `OWNER_ASSIGNED` | `field: "user_id"`, `null` → user ID | Virtual card, gift card, spend account, bank account | | Owner transferred | `OWNER_TRANSFERRED` | `field: "user_id"`, old user ID → new user ID | Virtual card, gift card, spend account, bank account | | Access granted | `ACCESS_GRANTED` | `field: "role"`, `null` → access level | Virtual card, gift card, spend account | | Access level changed | `ACCESS_LEVEL_CHANGED` | `field: "role"`, old level → new level | Virtual card, gift card, spend account | | Access revoked | `ACCESS_REVOKED` | `field: "role"`, access level → `null` | Virtual card, gift card, spend account | | Invite access granted, changed, or revoked | `PENDING_ACCESS_GRANTED`, `PENDING_ACCESS_LEVEL_CHANGED`, `PENDING_ACCESS_REVOKED` | `field: "role"`, plus `subject_non_registered_user_id` | Virtual card, gift card, spend account | | Invited person registered | `ACCESS_APPLIED_ON_REGISTRATION` | `field: "role"` | Virtual card, gift card, spend account | | Approval requested | `APPROVAL_REQUESTED` | `approval_type`, `approval_code` | Virtual card, gift card, spend account | | Approval approved | `APPROVAL_APPROVED` | `approval_type`, `approval_code`, `decision_channel` | Virtual card, gift card, spend account | | Approval declined | `APPROVAL_DECLINED` | `approval_type`, `approval_code`, `decision_channel` | Virtual card, gift card, spend account | | Approval expired | `APPROVAL_EXPIRED` | `approval_type`, `approval_code` | Virtual card, gift card, spend account | Access levels: `GUEST` `0`, `VIEWER` `1`, `SPENDER` `10`, `MANAGER` `100`, `ADMIN` `1000`. *** ## Virtual card events `objectType: VIRTUAL_CARDS`. Details: [Virtual card activity history](/features/virtual-card-activity-history). ### Creation | Event | `changeType` | `change` | Recorded when | | - | - | - | - | | Card created | `OBJECT_CREATED` | `{}` | `createVirtualCard`, the Fluz app, or an approved card request | | Card created by a schedule or bulk order | `OBJECT_CREATED` | `{ "source": "SCHEDULE", "scheduled_task_id", "scheduled_event_id" }` | `createVirtualCardBulkOrder` or a scheduled creation | | Card created by CSV import | `OBJECT_CREATED` | `{ "source": "CSV_IMPORT", "import_job_id" }` | A CSV import in the Fluz app | ### Settings | Event | `field` | `old` → `new` | | - | - | - | | Nickname changed | `nickname` | Nickname, or `null` | | Spend limit changed | `spend_limit` | Dollars, for example `500`, or `null` | | Limit period changed | `spend_limit_duration` | `DAILY`, `WEEKLY`, `MONTHLY`, `ANNUAL`, `LIFETIME`, or `null` | | Lock date changed | `lock_date` | ISO 8601 UTC timestamp, or `null` | | Lock after next use turned on or off | `lock_next_use` | `true` / `false` | | Funding bank account changed | `funding_source` | `bankAccountId`, or `null` | | Funding spend account changed | `spend_account` | `userCashBalanceId`, or `null` | | Single-use card converted to multi-use | `card_type` | `SINGLE_USE` → `MULTI_USE` | ### Status | Event | `field` | `old` → `new` | Extra keys | | - | - | - | - | | Locked by a user | `status` | `UNLOCKED` → `LOCKED` | — | | Unlocked | `status` | `LOCKED` → `UNLOCKED` | — | | Locked by Fluz | `status` | `UNLOCKED` → `LOCKED` | `reason` | | Locked by Fluz support | `status` | `UNLOCKED` → `LOCKED` | — | | Single-use card spent | `status` | → `USED` | — | | Cancelled | `status` | → `CANCELLED` | `reason` | | Expired | `status` | `UNLOCKED` or `LOCKED` → `EXPIRED` | — | ### Security | Event | `field` | `old` → `new` | | - | - | - | | PIN set | `pin` | `null` → `"updated"` | | Added to a digital wallet | `wallet_token` | → `"added"` | | Wallet token activated | `wallet_token` | `"added"` → `"activated"` | | Wallet token removed | `wallet_token` | → `"removed"` | | Billing address updated by Fluz support | `billing_address` | `null` → `"UPDATED"`, with `"via": "BUSINESS_ADDRESS_UPDATE"` | ### Expense details Recorded on the card a transaction was made with. Each event also carries `transaction_log_id` and `transaction_record_id`. | Event | `field` | `old` → `new` | | - | - | - | | Memo added, changed, or removed | `memo` | `null` → `ADDED`, `CHANGED`, or `REMOVED` | | Receipt added, changed, or removed | `receipt` | `null` → `ADDED`, `CHANGED`, or `REMOVED` | | Expense category changed | `expense_category` | Category names, plus `old_id` / `new_id` | | QuickBooks category changed | `erp_category` | Category names, plus `old_id` / `new_id` | ### Sharing | Event | `changeType` | Extra keys | | - | - | - | | Shared card claimed by the recipient | `ACCESS_GRANTED` or `ACCESS_LEVEL_CHANGED` | `"reason": "shared-card-claim"` | Plus the ownership, access, invite, and approval events in [Events on every object](#events-on-every-object). *** ## Gift card events `objectType: GIFT_CARDS`. Details: [Gift card activity history](/features/gift-card-activity-history). | Event | `changeType` / `field` | `old` → `new` | | - | - | - | | Gift card purchased | `OBJECT_CREATED` | `{}` | | Archived, or marked spent in the web app | `status` | `ACTIVE` → `ARCHIVED` | | Reactivated | `status` | `ARCHIVED` → `ACTIVE` | | Balance edited | `balance` | Numbers, for example `25` → `12.5` | | Memo added, changed, or removed | `memo` | `null` → `ADDED`, `CHANGED`, or `REMOVED` | | Receipt added, changed, or removed | `receipt` | `null` → `ADDED`, `CHANGED`, or `REMOVED` | | Expense category changed | `expense_category` | Category names | | QuickBooks category changed | `erp_category` | Category names | Plus the ownership, access, invite, and approval events in [Events on every object](#events-on-every-object). *** ## Spend account events `objectType: SPEND_ACCOUNTS`. Details: [Spend account activity history](/features/spend-account-activity-history). | Event | `changeType` / `field` | `old` → `new` | | - | - | - | | Spend account created | `OBJECT_CREATED` | `{}` | | Renamed | `nickname` | Previous and new nickname | | Image changed | `image` | Previous and new image URL, or `null` | | Became the default | `is_default` | `false` → `true` | | Stopped being the default | `is_default` | `true` → `false` | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` ↔ `true` | | Closed | `status` | For example `ACTIVE` → `CLOSED` | Plus the ownership, access, invite, and approval events in [Events on every object](#events-on-every-object). *** ## Funding source events `objectType: FUNDING_SOURCES`. Details: [Funding source activity history](/features/funding-source-activity-history). ### Bank cards | Event | `changeType` / `field` | `old` → `new` | | - | - | - | | Added | `OBJECT_CREATED` | `{}` | | Added again after removal | `status` | `REMOVED` or `PENDING_REMOVAL` → `ACTIVE` | | Removed | `status` | `ACTIVE` or `PENDING_REMOVAL` → `REMOVED` | | Removal requested | `status` | → `PENDING_REMOVAL` | | Verified | `verified` | `false` → `true` | | Card type corrected | `card_type` | → `DEBIT`, `CREDIT`, or `PREPAID` | | Nickname changed | `nickname` | Nickname, or `null` | | Billing address changed | `billing_address` | Address ID, or `null` | | Preferred category changed | `preferred_mcc` | Merchant category ID, or `null` | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` ↔ `true` | ### Bank accounts | Event | `changeType` / `field` | `old` → `new` | | - | - | - | | Linked | `OBJECT_CREATED` | `{}` | | Relinked | `status` | `DISABLED` → `ENABLED` | | Removed or disabled | `status` | → `DISABLED` | | Enabled or disabled | `status` | `ENABLED` ↔ `DISABLED` | | Nickname changed | `nickname` | Nickname, or `null` | | Billing address changed | `billing_address` | Address ID, or `null` | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` ↔ `true` | | Owner assigned or transferred | `OWNER_ASSIGNED`, `OWNER_TRANSFERRED` | User IDs | ### PayPal | Event | `changeType` / `field` | `old` → `new` | | - | - | - | | Added | `OBJECT_CREATED` | `{}` | | Added again | `status` | → `ACTIVE` | | Removed | `status` | → `REMOVED` | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` ↔ `true` | *** ## Approval codes | Request | `approval_type` | `approval_code` | Recorded on | | - | - | - | - | | Create a virtual card | `VIRTUAL_CARD_CREATION` | `600003` | The new card | | Replace a card funded by a restricted bank account | `VIRTUAL_CARD_CREATION` | `600004` | The original card, then the replacement | | Change a card's spend limit | `VIRTUAL_CARD` | `600002` | The card | | Unlock a card | `VIRTUAL_CARD` | `600005` | The card | | Change a card's lock date | `VIRTUAL_CARD` | `600006` | The card | | Unlock a card declined because it was locked | `VIRTUAL_CARD` | `600102` | The card | | Raise a limit after a declined purchase | `VIRTUAL_CARD` | `600103` | The card | | Purchase a gift card | `GIFT_CARD` | `400007` | The new gift card | | Access to an object | `ACCOUNT_ACCESS` | `100004`, `100005` | The virtual card, gift card, or spend account | *** ## Reason and source values | Key | Value | Meaning | | - | - | - | | `reason` (lock) | `LOCK_DATE_REACHED` | The card's lock date passed. | | | `LOCK_NEXT_USE` | The card was set to lock after next use and was used. | | | `SINGLE_LOAD_LIMIT` | The card reached its single-load limit. | | | `FRAUD_DECISION` | A fraud review locked the card. | | | `TOKENIZATION_RISK` | Adding the card to a digital wallet failed a risk check. | | | `LOCKED_BY_SYSTEM` | Fluz locked the card for another reason. | | | `PURCHASE_CLEANUP` | Locked while cleaning up a purchase that didn't complete. | | | `FUNDING_SOURCE_REMOVED` | The funding source behind the card was removed. | | `reason` (cancel) | `USER_REQUESTED_CANCEL` | A user cancelled the card. | | | `PURCHASE_VIRTUAL_CARD_ERROR` | The purchase that created the card failed. | | | `FLUZPAY_WITHDRAW_OR_USAGE` | Fluz cancelled a reserved card to release its balance. | | `reason` (access) | `owner-floor` | The owner's minimum `SPENDER` access was granted, raised, or removed. | | | `shared-card-claim` | A recipient claimed a shared card. | | `source` | `SCHEDULE`, `CSV_IMPORT` | How a virtual card was created. | | `via` | `BUSINESS_ADDRESS_UPDATE` | Fluz support updated a business address on all its cards. | | | `CMS_IMPORT` | Fluz support disabled a bank account through a bulk import. | *** ## Systems you can see | `actor.system` | Objects | What it does | | - | - | - | | `virtual-card-service` | Virtual card | Locks, unlocks, marks cards used, applies approved limit increases, records wallet token updates. | | `virtual-card-expirer` | Virtual card | Expires cards. | | `purchase-service` | Virtual card, funding source | Cancels a card whose purchase failed. Corrects a bank card's type. | | `scheduled-task-handler` | Virtual card, gift card, spend account | Creates scheduled cards, gift cards, and spend accounts. | | `approvals-service` | Virtual card, gift card, spend account | Records expired requests and decisions made through a link with no named approver. | | `erp-service` | Virtual card, gift card | Applies a QuickBooks category automatically. | | `transactional-graph-service` | Virtual card, gift card, spend account | API changes where the caller can't be resolved to a user. | | `business-service` | Spend account | Creates spend accounts during business onboarding and conversion. | | `fluz-web-unified` | Spend account | Creates a spend account during embedded widget setup. | | `identity-service` | Funding source | Links, relinks, and disables bank accounts from background jobs. | | `fluz-mobile-backend` | Funding source | Picks a backup funding source automatically. | | `cms` | All | Fluz support. | *** ## What isn't recorded * **Reads.** Revealing card details or a gift card code, viewing balances, and listing objects. * **Money movement.** Transactions, authorizations, refunds, deposits, withdrawals, and transfers. Use [Transaction activity](/features/transactions-details-overview). * **Other things on the account.** Authorized users, roles, approvals that don't touch one of the four objects, share links, and developer applications. * **Group access.** Access a user has through a group. * **A few specific actions.** A card closed only at the issuer, failed bank card verifications, the last step of a bulk card removal, a primary funding source set automatically after linking, and gift card status changes made in Fluz Enterprise. ## Next steps > **[Activity history overview](/features/activity-history)** > > The query, response fields, actors, approvals, and access levels. > **[Approvals & requests](/features/approvals-and-requests)** > > The requests that create approval events in activity history. # Add expense details Source: https://docs.fluz.app/features/add-expense-details You can enrich any transaction with a **memo**, a **category**, and/or an **attachment** (e.g. receipts, invoices, purchase orders). Annotations can be added at the time of the original transaction or updated afterward using the `updateTransactionMetadata` mutation. For accounts with ERP integrations enabled and an active QuickBooks Online connection, you can also manage ERP transaction metadata: accounting category, vendor, customer, billable status, and an ERP-specific memo. ERP metadata is separate from annotations. Annotations are supported on: * Deposits (`depositCashBalance`) * Gift card purchases (`purchaseGiftCard`) * Wallet transfers (`createTransfer`, `transferInternalBalance`) * Virtual card creation (`createVirtualCard`) * `attachment` is excluded ERP metadata is supported on: * Existing transactions (`updateTransactionMetadata.input.erpTransactionMetadata`) * Bulk transaction updates (`bulkUpdateErpTransactionMetadata`) * Transaction reads (`Transaction.erpMetadata`, `erpTransactionMetadata`, `erpTransactionMetadataList`) *** ## How it works 1. **Optional:** If you want to attach a file, upload it first via the REST upload endpoint. You'll get back an `attachmentId`. 2. Pass `memo`, `transactionCategory`, and/or `attachmentId` into your mutation input — either at transaction time or later via `updateTransactionMetadata`. 3. For ERP metadata, first query imported QuickBooks Online reference items, or pass names that should be resolved or created in QuickBooks Online. 4. Update ERP metadata on one transaction with `updateTransactionMetadata.input.erpTransactionMetadata`, or update up to 100 transactions with `bulkUpdateErpTransactionMetadata`. 5. Read annotations back on `getTransactions`, `getUserPurchases`, or the mutation response. Read ERP details on `Transaction.erpMetadata`, `erpTransactionMetadata`, or `erpTransactionMetadataList`. The `attachmentUrl` field returns a short-lived signed URL for file access. *** ## Step 1: Upload an attachment (optional) > This is a REST endpoint, not a GraphQL mutation ### Endpoint `POST /api/v1/file-upload/transaction-memo-attachment` ### Authentication `Authorization: Bearer ` ### Request Send the file as `multipart/form-data` with the field name `file`. **Accepted types:** `application/pdf`, `image/png` > ⚠️ JPEG is not accepted for transaction attachments. ### Sample Request ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/file-upload/transaction-memo-attachment \ -H "Authorization: Bearer " \ -F "file=@receipt.pdf" ``` ### Response ```json theme={null} { "attachmentId": "3f2a1b4c-e5d6-7890-abcd-ef1234567890" } ``` Copy the `attachmentId` — you'll pass it into your mutation input in the next step. > The `attachmentId` is scoped to your account. The system verifies the file exists in your account's storage when you submit the mutation. An ID from a different account will be rejected. ### Upload Errors | Cause | Status | Details | | - | - | - | | No file included in request | 400 | Missing file | | File type not allowed (e.g. JPEG) | 400 | `INVALID_ARGUMENTS` | *** ## Step 2: Annotate the transaction You can provide annotations at the time of the original transaction **or** update them afterward. ### Option A — At transaction time The following mutations accept `memo`, `transactionCategory`, and `attachmentId` as optional fields in their input: * `depositCashBalance` → `DepositCashBalanceInput` * `purchaseGiftCard` → `PurchaseGiftCardInput` * `createTransfer` → `CreateTransferInput` * `transferInternalBalance` → `TransferInternalBalanceInput` #### Annotation Fields | Field | Type | Description | | - | - | - | | `memo` | String | Free-text note. Max 255 characters. | | `transactionCategory` | String | Category label. Free-form — categories are created automatically on first use and reused if the same name is passed again. | | `attachmentId` | String | The ID returned by the upload endpoint. The file must be uploaded before submitting the mutation. | #### Sample — Purchase Gift Card with Annotation ```graphql theme={null} mutation purchaseGiftCard($input: PurchaseGiftCardInput!) { purchaseGiftCard(input: $input) { purchaseDisplayId purchaseAmount memo transactionCategory attachmentUrl giftCard { giftCardId status } } } ``` ```json theme={null} { "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "offerId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19", "amount": 50.0, "balanceAmount": 50.0, "memo": "Team lunch — Q2", "transactionCategory": "Meals & Entertainment", "attachmentId": "3f2a1b4c-e5d6-7890-abcd-ef1234567890" } ``` *** ### Option B — After the transaction (`updateTransactionMetadata`) Use this mutation to add or update annotations on any existing transaction. > **Partial update semantics:** Only the fields you include are updated. Omitted fields are left unchanged. Pass `null` to clear a field. #### Mutation ```graphql theme={null} mutation updateTransactionMetadata($input: UpdateTransactionMetadataInput!) { updateTransactionMetadata(input: $input) { recordId memo transactionCategory attachmentUrl } } ``` #### UpdateTransactionMetadataInput | Field | Type | Required | Description | | - | - | - | - | | `recordId` | UUID! | Yes | The `recordId` of the transaction to update. | | `memo` | String | No | Free-text note. Max 255 characters. Omit to leave unchanged; pass `null` to clear. | | `transactionCategory` | String | No | Category label. Omit to leave unchanged; pass `null` to clear. | | `attachmentId` | String | No | ID from the upload endpoint. Omit to leave unchanged; pass `null` to clear. | | `erpTransactionMetadata` | `UpdateErpTransactionMetadataInput` | No | Optional ERP-aware categorization. Omit, or pass `null`, to leave ERP metadata unchanged. | #### Required scopes `LIST_PAYMENT` and `LIST_PURCHASES`. If you include `erpTransactionMetadata`, the request also requires `MANAGE_ERP_TRANSACTION_METADATA`. #### Sample — Add a memo and category ```json theme={null} { "recordId": "550e8400-e29b-41d4-a716-446655440000", "memo": "Q1 vendor payment", "transactionCategory": "Operating Expenses" } ``` #### Sample — Attach a file to an existing transaction ```json theme={null} { "recordId": "550e8400-e29b-41d4-a716-446655440000", "attachmentId": "3f2a1b4c-e5d6-7890-abcd-ef1234567890" } ``` #### Sample — Clear a memo ```json theme={null} { "recordId": "550e8400-e29b-41d4-a716-446655440000", "memo": null } ``` #### Sample Response ```json theme={null} { "data": { "updateTransactionMetadata": { "recordId": "550e8400-e29b-41d4-a716-446655440000", "memo": "Q1 vendor payment", "transactionCategory": "Operating Expenses", "attachmentUrl": "https://storage.googleapis.com/..." } } } ``` #### Errors | Cause | Error | | - | - | | `recordId` not found or belongs to a different account | `INVALID_ARGUMENTS` — transaction not found | | Transaction type does not support metadata | `INVALID_ARGUMENTS` — transaction does not support metadata | | `attachmentId` is not a valid UUID | `INVALID_ARGUMENTS` — Invalid attachment ID | | File not found in storage (not uploaded, or wrong account) | `INVALID_ARGUMENTS` — Attachment file not found. Please upload the file first. | *** ## Step 3: Manage ERP metadata ERP metadata is used to categorize transactions before exporting them to the connected accounting provider. The currently available provider is QuickBooks Online. ERP metadata fields are separate from annotations: * Annotation `memo` is limited to 255 characters. * ERP `erpTransactionMetadata.memo` is limited to 4000 characters. * Annotation `transactionCategory` is a Fluz category label. * ERP `categoryReferenceItemId` points to a QuickBooks Online chart-of-accounts item. ### UpdateErpTransactionMetadataInput | Field | Type | Description | | - | - | - | | `categoryReferenceItemId` | `UUID` | QuickBooks Online chart-of-accounts reference item ID. Omit to leave unchanged; pass `null` to clear. Must refer to an active chart-of-accounts item. | | `categoryName` | `String` | Find or create a QuickBooks Online Expense account by name, then use it as the category. Ignored when `categoryReferenceItemId` is supplied. Max 100 characters. | | `vendorReferenceItemId` | `UUID` | QuickBooks Online vendor reference item ID. Omit to leave unchanged; pass `null` to clear. Must refer to an active vendor. | | `vendorName` | `String` | Find or create a QuickBooks Online vendor by name. Ignored when `vendorReferenceItemId` is supplied. Max 100 characters. | | `customerReferenceItemId` | `UUID` | QuickBooks Online customer reference item ID. Omit to leave unchanged; pass `null` to clear. Must refer to an active customer. | | `customerName` | `String` | Find or create a QuickBooks Online customer by name. Ignored when `customerReferenceItemId` is supplied. Max 100 characters. | | `isBillable` | `Boolean` | Sets billable status. Omit to leave unchanged; pass `null` to clear. If `true`, a customer is required before the transaction can become `READY`. | | `memo` | `String` | ERP memo. Omit to leave unchanged; pass `null` to clear. Max 4000 characters. | Leave `erpTransactionMetadata` out of the request when you do not want to update ERP metadata. ### Sample: Update annotations and ERP metadata together ```graphql theme={null} mutation updateTransactionMetadata($input: UpdateTransactionMetadataInput!) { updateTransactionMetadata(input: $input) { recordId memo transactionCategory erpMetadata { category { referenceItemId name } vendor { referenceItemId name } customer { referenceItemId name } isBillable memo syncStatus } } } ``` ```json theme={null} { "input": { "recordId": "550e8400-e29b-41d4-a716-446655440000", "memo": "Receipt added in Fluz", "transactionCategory": "Office Supplies", "erpTransactionMetadata": { "categoryReferenceItemId": "11111111-1111-4111-8111-111111111111", "vendorName": "Example Vendor", "customerReferenceItemId": "22222222-2222-4222-8222-222222222222", "isBillable": true, "memo": "QuickBooks Online memo for client office supplies" } } } ``` *** ## Step 4: Find ERP reference items Use these queries to find imported QuickBooks Online reference items before setting `categoryReferenceItemId`, `vendorReferenceItemId`, or `customerReferenceItemId`. | Query | Returns | | - | - | | `erpChartOfAccounts` | Imported QuickBooks Online chart-of-accounts items. | | `erpVendors` | Imported QuickBooks Online vendors. | | `erpCustomers` | Imported QuickBooks Online customers. | ### Reference item fields | Field | Description | | - | - | | `referenceItemId` | Fluz ID for this imported ERP reference item. Use this in ERP metadata update inputs. | | `externalId` | QuickBooks Online ID. | | `name` | Display name. | | `fullyQualifiedName` | Fully qualified provider name, when available. | | `accountType` | Chart-of-accounts account type. Only populated for chart-of-accounts items. | | `accountSubType` | Chart-of-accounts subtype. Only populated for chart-of-accounts items. | | `active` | Whether the imported provider item is active. | | `lastChangedAt` | Last QuickBooks Online update timestamp, when available. | ### Reference item filters | Field | Type | Description | | - | - | - | | `q` | `String` | Case-insensitive substring search on name. | | `active` | `Boolean` | Defaults to `true`. Set `false` to list inactive items. | | `accountType` | `[String!]` | Chart-of-accounts only. Filters by QuickBooks Online account type, such as `Expense`. | | `accountSubType` | `[String!]` | Chart-of-accounts only. Filters by QuickBooks Online account subtype. | | `sortKey` | `ErpReferenceItemSortKey` | `NAME`, `FULLY_QUALIFIED_NAME`, or `LAST_CHANGED_AT`. Defaults to `NAME`. | | `sortOrder` | `SortOrder` | `ASC` or `DESC`. Defaults to `ASC`. | ### Sample: Search chart of accounts ```graphql theme={null} query erpChartOfAccounts { erpChartOfAccounts( filter: { q: "office" accountType: ["Expense"] sortKey: NAME sortOrder: ASC } paginate: { limit: 10, offset: 0 } ) { totalCount hasNextPage items { referenceItemId externalId name fullyQualifiedName accountType accountSubType active lastChangedAt } } } ``` ### Sample: Search vendors and customers ```graphql theme={null} query erpCounterparties { erpVendors( filter: { q: "example vendor" } paginate: { limit: 10, offset: 0 } ) { items { referenceItemId name active } } erpCustomers( filter: { q: "example company" } paginate: { limit: 10, offset: 0 } ) { items { referenceItemId name active } } } ``` *** ## Step 5: Read ERP metadata ERP metadata can be read from the transaction object or through dedicated ERP metadata queries. ### Read ERP metadata in getTransactions ```graphql theme={null} query getTransactionsWithErpMetadata { getTransactions(paginate: { limit: 10, offset: 0 }) { totalCount hasNextPage transactions { recordId amount memo transactionCategory erpMetadata { transactionMetadataId transactionRecordId category { referenceItemId name } vendor { referenceItemId name } customer { referenceItemId name } isBillable memo syncStatus isSyncing updatedAt } } } } ``` `erpMetadata` is `null` when the account has no active ERP connection or the transaction has no ERP metadata. ### Read one transaction's ERP metadata ```graphql theme={null} query erpTransactionMetadata($transactionRecordId: UUID!) { erpTransactionMetadata(transactionRecordId: $transactionRecordId) { transactionMetadataId transactionRecordId category { referenceItemId name } vendor { referenceItemId name } customer { referenceItemId name } isBillable memo syncStatus isSyncing updatedAt } } ``` ```json theme={null} { "transactionRecordId": "550e8400-e29b-41d4-a716-446655440000" } ``` This query returns `null`, not an error, when there is no ERP metadata for the transaction or the account has no active ERP connection. ### List ERP metadata records ```graphql theme={null} query erpTransactionMetadataList { erpTransactionMetadataList( filter: { syncStatus: DRAFT, updatedAfter: "2026-07-01T00:00:00.000Z" } paginate: { limit: 20, offset: 0 } ) { totalCount hasNextPage items { transactionRecordId category { referenceItemId name } isBillable memo syncStatus updatedAt } } } ``` ### ERP metadata list filters | Field | Type | Description | | - | - | - | | `syncStatus` | `ErpTransactionSyncStatus` | Filter by sync status. | | `updatedAfter` | `DateTime` | Return metadata updated after this timestamp. | | `transactionRecordIds` | `[UUID!]` | Limit results to specific transaction record IDs. | | `sortKey` | `ErpTransactionMetadataSortKey` | Currently `UPDATED_AT`. Defaults to `UPDATED_AT`. | | `sortOrder` | `SortOrder` | `ASC` or `DESC`. Defaults to `DESC`. | *** ## ERP sync status | Status | Meaning | | - | - | | `DRAFT` | Missing required ERP fields and cannot be exported yet. Most transaction types need a category. If `isBillable` is `true`, a customer is also required. | | `READY` | Required ERP fields are present and the transaction is ready to export. | | `SYNC_STARTED` | Export to the accounting provider is in progress. The transaction cannot be edited while this status is active. | | `SYNCED` | Export succeeded. The transaction cannot be modified through ERP metadata APIs after this point. | | `SYNC_FAILED` | The last export attempt failed. Edit the metadata and retry export. | *** ## Bulk update ERP metadata Use `bulkUpdateErpTransactionMetadata` to update ERP metadata for up to 100 transactions in one request. Each item uses the same `UpdateErpTransactionMetadataInput` fields. Omitted fields are left unchanged; nullable reference fields, `memo`, and `isBillable` can be passed as `null` to clear them. ### Mutation ```graphql theme={null} mutation bulkUpdateErpTransactionMetadata( $items: [BulkUpdateErpTransactionMetadataItem!]! ) { bulkUpdateErpTransactionMetadata(items: $items) { succeeded failed { transactionRecordId error { code message } } } } ``` ### Variables ```json theme={null} { "items": [ { "transactionRecordId": "550e8400-e29b-41d4-a716-446655440000", "erpTransactionMetadata": { "categoryName": "Office Supplies", "vendorName": "Example Vendor", "memo": "Bulk-updated QuickBooks Online memo" } }, { "transactionRecordId": "660e8400-e29b-41d4-a716-446655440000", "erpTransactionMetadata": { "categoryReferenceItemId": "11111111-1111-4111-8111-111111111111", "customerName": "Example Company LLC", "isBillable": true } } ] } ``` ### Sample response ```json theme={null} { "data": { "bulkUpdateErpTransactionMetadata": { "succeeded": ["550e8400-e29b-41d4-a716-446655440000"], "failed": [ { "transactionRecordId": "660e8400-e29b-41d4-a716-446655440000", "error": { "code": "ERP-0019", "message": "The reference item type does not match the expected type for this field." } } ] } } } ``` Per-item ERP validation failures are returned in `failed`, other valid items can still succeed. *** ## Reading annotations Annotations are returned on the following: | Query / Mutation | Type | Fields | | - | - | - | | `getTransactions` | `Transaction` | `memo`, `transactionCategory`, `attachmentUrl`, optional `erpMetadata` | | `getUserPurchases` | `UserPurchase` | `memo`, `transactionCategory`, `attachmentUrl` | | `updateTransactionMetadata` | `Transaction` | `memo`, `transactionCategory`, `attachmentUrl`, optional `erpMetadata` | | `depositCashBalance` | `CashBalanceDeposit` | `attachmentUrl` | | `erpTransactionMetadata` | `ErpTransactionMetadata` | ERP category, vendor, customer, billable status, ERP memo, sync status | | `erpTransactionMetadataList` | `ErpTransactionMetadataConnection` | Paginated ERP metadata records | > ⚠️ **`attachmentUrl` is a signed URL.** It expires shortly after being generated. Do not store it — re-fetch the transaction when you need to display or access the file. *** ## Common errors | Cause | Error | | - | - | | Missing annotation scopes | `AUTH-0031` / invalid scope | | Missing `VIEW_ERP_TRANSACTION_METADATA` for ERP reads | `AUTH-0031` / invalid scope | | Missing `MANAGE_ERP_TRANSACTION_METADATA` for ERP writes | `AUTH-0031` / invalid scope | | ERP integrations are not enabled for the account | `ERP-0018` / forbidden | | No active QuickBooks Online connection | `ERP-0001` / no active connection | | QuickBooks Online connection requires attention | `ERP-0016` / connection action required | | Transaction not found for this account | `ERP-0026` / transaction not found | | Transaction already synced | `ERP-0027` / transaction already synced | | Transaction export is in progress | `ERP-0052` / transaction sync in progress | | Reference item is the wrong type or not in this connection | `ERP-0019` / invalid reference type | | Reference item is inactive | `ERP-0028` / stale reference item | | ERP memo exceeds 4000 characters | `ERP-0029` / memo too long | *** ## Notes and limits * `recordId` is the transaction record ID returned by transaction queries. * Annotation updates are partial: omitted fields stay unchanged, and `null` clears. * ERP metadata updates are also partial: omitted fields stay unchanged, and supported nullable fields can be cleared with `null`. * In `updateTransactionMetadata`, `erpTransactionMetadata: null` is a no-op, not a clear. * In `updateTransactionMetadata`, `erpTransactionMetadata: {}` is invalid. Omit the field instead. * If both an ERP reference ID and a name are provided for the same field, the reference ID wins. * `categoryName` creates or selects a QuickBooks Online account with account type `Expense`. * `vendorName` creates or selects a QuickBooks Online vendor. * `customerName` creates or selects a QuickBooks Online customer. * GraphQL pagination uses `OffsetInput`, `limit` defaults to 20 and is capped by the API pagination cap. # Approvals & requests overview Source: https://docs.fluz.app/features/approvals-and-requests Use the Fluz API to submit approval requests for sensitive account actions, list pending requests, and approve or decline them on behalf of managers. Approval requests are available through the Transactional Graph Service GraphQL API. When a request is submitted, Fluz creates a durable approval record and notifies configured approvers. If approved, the underlying action runs automatically (for example, purchasing a gift card or transferring funds). ## How It Works ```text theme={null} Requester Manager / Approver | | | 1. request* mutation | |------------------------------->| | | | 2. APPROVAL_CREATE webhook | |<-------------------------------| | | | 3. approvalRequests query (optional) | 4. approveApprovalRequest or declineApprovalRequest | | | 5. APPROVAL_APPROVE or | | APPROVAL_DECLINE webhook | |<-------------------------------| ``` 1. **Request** — A user with the appropriate `REQUEST_*` scope calls a `request*` mutation. The API validates the input and queues the request. The mutation returns a `messageId`, not an `approvalId`. 2. **Create** — Fluz creates a pending approval and assigns account managers as approvers. Your application receives an `APPROVAL_CREATE` webhook with the `approvalId`. 3. **List (optional)** — Approvers with `LIST_APPROVALS` can call `approvalRequests` to fetch open requests for the account. 4. **Approve or decline** — Approvers with `MANAGE_APPROVALS` call `approveApprovalRequest` or `declineApprovalRequest`. On approval, Fluz executes the requested action. 5. **Webhook** — Your application receives `APPROVAL_APPROVE` or `APPROVAL_DECLINE`. If execution fails after approval, you may receive `APPROVAL_HANDLER_ERROR`. > Requests are processed asynchronously. Poll `approvalRequests` or listen for webhooks to obtain the `approvalId` after submitting a request. ## Shared Operations These operations apply to all approval request types. ### List Pending Approvals `approvalRequests` Returns open (`PENDING`) approval requests for the authenticated account. **Required scope:** `LIST_APPROVALS` ```graphql theme={null} query { approvalRequests { approvalId accountId requesterUserId approvalType approvalCode approvalStatus approversLogic expireDate createdAt approvers { approvalApproverId approverUserId role status } } } ``` ### Approve a Request `approveApprovalRequest` Approves a pending request. On success, Fluz runs the action associated with the request (for example, creating a virtual card). **Required scope:** `MANAGE_APPROVALS` ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action error { code message } } } ``` ### Decline a Request `declineApprovalRequest` Declines a pending request. No downstream action is executed. **Required scope:** `MANAGE_APPROVALS` ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action error { code message } } } ``` ## Application Scopes Approval-related scopes must be granted at both the application level (by Fluz) and the user level (via OAuth consent or `generateUserAccessToken`). | Scope | Usage | | - | - | | `LIST_APPROVALS` | List pending approval requests; receive `APPROVAL_CREATE` and `APPROVAL_HANDLER_ERROR` webhooks | | `MANAGE_APPROVALS` | Approve or decline requests; receive `APPROVAL_APPROVE` and `APPROVAL_DECLINE` webhooks | | `REQUEST_VIRTUAL_CARD` | Submit virtual card creation requests | | `REQUEST_VIRTUAL_CARD_LIMIT_CHANGE` | Submit virtual card limit change requests | | `REQUEST_GIFT_CARD` | Submit gift card purchase requests | | `REQUEST_INTERNAL_TRANSFER` | Submit internal spend-account transfer requests | | `REQUEST_ACCOUNT_TRANSFER` | Submit account-to-account transfer requests | | `REQUEST_REIMBURSEMENT` | Submit reimbursement requests | See [Authentication](/concepts/authentication) for how scopes are granted and validated. ## Request Types | Request | Mutation | Create scope | | - | - | - | | Virtual card creation | `requestVirtualCard` | `REQUEST_VIRTUAL_CARD` | | Virtual card limit change | `requestVirtualCardLimitChange` | `REQUEST_VIRTUAL_CARD_LIMIT_CHANGE` | | Gift card purchase | `requestGiftCardPurchase` | `REQUEST_GIFT_CARD` | | Internal transfer | `requestInternalTransfer` | `REQUEST_INTERNAL_TRANSFER` | | Account transfer | `requestAccountTransfer` | `REQUEST_ACCOUNT_TRANSFER` | | Reimbursement | `requestReimbursement` | `REQUEST_REIMBURSEMENT` | Each request type has a dedicated guide with input fields, examples, and webhook identifiers. ## Webhooks Configure webhooks in the Fluz developer portal. Approval events use the same delivery mechanism as other Fluz webhooks. See the [Developers overview](/developers) for setup and verification. | Event | When it fires | Webhook subscription scope | | - | - | - | | `APPROVAL_CREATE` | A new approval request is created | `LIST_APPROVALS` | | `APPROVAL_APPROVE` | A request is approved | `MANAGE_APPROVALS` | | `APPROVAL_DECLINE` | A request is declined | `MANAGE_APPROVALS` | | `APPROVAL_HANDLER_ERROR` | Approval succeeded but action execution failed | `LIST_APPROVALS` | ### APPROVAL\_CREATE payload ```json theme={null} { "approvalId": "07df5653-43a8-4532-9881-3ab5857bbe12", "accountId": "b1155504-ad30-4b2f-873d-b8795277b128", "userId": "c3d4e5f6-a7b8-9012-cdef-345678901234", "approvalType": "GIFT_CARD", "approvalCode": "400007", "approvalStatus": "PENDING", "createdAt": "2025-07-09T10:00:00Z" } ``` Use `approvalType` and `approvalCode` together to identify the request type. Each per-type guide lists the values for that request. ### APPROVAL\_APPROVE / APPROVAL\_DECLINE payload ```json theme={null} { "approvalId": "07df5653-43a8-4532-9881-3ab5857bbe12", "accountId": "b1155504-ad30-4b2f-873d-b8795277b128", "userId": "c3d4e5f6-a7b8-9012-cdef-345678901234", "approvalType": "GIFT_CARD", "approvalCode": "400007", "approvalStatus": "APPROVED", "approvalAction": "APPROVE", "approverAccountId": "d4e5f6a7-b8c9-0123-def4-567890123456", "approverUserId": "e5f6a7b8-c9d0-1234-ef56-789012345678", "linkId": "f6a7b8c9-d0e1-2345-f678-901234567890", "attemptedAt": "2025-07-09T10:05:00Z" } ``` For declines, `approvalAction` is `DECLINE` and `approvalStatus` is `DECLINED`. ### APPROVAL\_HANDLER\_ERROR payload Sent when a request is approved but the downstream action fails (for example, insufficient balance at execution time). ```json theme={null} { "approvalId": "07df5653-43a8-4532-9881-3ab5857bbe12", "accountId": "b1155504-ad30-4b2f-873d-b8795277b128", "userId": "c3d4e5f6-a7b8-9012-cdef-345678901234", "approvalType": "GIFT_CARD", "approvalCode": "400007", "approvalStatus": "APPROVED", "approvalAction": "APPROVE", "failureStage": "ACTION_EXECUTION", "error": "Insufficient balance", "failedAt": "2025-07-09T10:05:01Z" } ``` ## Authentication Approval mutations support Bearer token (user OAuth) and Basic Auth (application API key), depending on your integration pattern. The authenticated principal becomes the requester. Use a User Access Token with the required scopes. See the [Authentication & Authorization Guide](/concepts/authentication). ## Error Handling Request mutations return a `RequestApprovalResponse`: ```json theme={null} { "data": { "requestGiftCardPurchase": { "success": true, "messageId": "1234567890" } } } ``` On failure: ```json theme={null} { "data": { "requestGiftCardPurchase": { "success": false, "messageId": null, "error": { "code": "APPROVAL_REQUEST_FAILED", "message": "Input validation error: ..." } } } } ``` Approve and decline mutations return `success: false` with an `error` object when the action cannot be completed (for example, approval not found or already resolved). # Virtual card activity history Source: https://docs.fluz.app/features/card-activity-history Every event Fluz records on a virtual card — creation, settings, locks, PINs, wallet tokens, expense details, sharing, and approvals — and how each one appears in objectChangeHistory. A virtual card's activity history shows who created the card, every change to its settings and status, who it was shared with, and which approvals authorized those changes. Read it with `objectChangeHistory` and `objectType: VIRTUAL_CARDS`. > [!NOTE] > > **Prerequisites:** the `VIEW_OBJECT_CHANGE_HISTORY` scope on your application, with either a user access token or your API key, and the `virtualCardId` of a card on the account. See [Activity history overview](/features/activity-history) for the full query, response fields, and access rules. ```graphql theme={null} query { objectChangeHistory( objectType: VIRTUAL_CARDS objectId: "07df5653-43a8-4532-9881-3ab5857bbe11" ) { changeType change createdAt subjectUserId approvalId actor { userId system applicationId applicationName } } } ``` ## At a glance ```mermaid theme={null} flowchart LR VC["Virtual card
VIRTUAL_CARDS"] VC --> A["Created
API, schedule, CSV import, approval, Fluz app"] VC --> B["Settings
nickname, spend limit, lock date, funding"] VC --> C["Status
locked, unlocked, used, cancelled, expired"] VC --> D["Security
PIN, wallet token, billing address"] VC --> E["Expense details
memo, receipt, category"] VC --> F["Ownership and sharing
owner, access, invites, claims"] VC --> G["Approvals
requested, approved, declined, expired"] ``` Each box is a group of events below. For one table of every virtual card event, see the [Event catalog](/features/activity-history-events#virtual-card-events). *** ## Creation The card records `OBJECT_CREATED` when it's issued. | How the card was created | `actor` | `subjectUserId` | `change` | | - | - | - | - | | `createVirtualCard` | The calling user and the connected application | The cardholder, when you pass `authUserId` | `{}` | | `createVirtualCardBulkOrder` or another scheduled creation | `system: "scheduled-task-handler"` | The cardholder | `{ "source": "SCHEDULE", "scheduled_task_id", "scheduled_event_id" }` | | A CSV import | The user who ran the import | The cardholder, when it's someone else | `{ "source": "CSV_IMPORT", "import_job_id" }` | | An approved [virtual card request](/features/request-virtual-card-approval) | The requester | The cardholder, when it's someone else | `{}`, with `approvalId` set | | The Fluz web or mobile app | The signed-in user | The cardholder, when it's someone else | `{}` | When a card is created through an approval, it also gets the request's `APPROVAL_REQUESTED` and `APPROVAL_APPROVED` events, dated when they happened. See [Approvals](#approvals). New cards also record `OWNER_ASSIGNED` for their first owner, and an `ACCESS_GRANTED` event with `"reason": "owner-floor"` for the owner's `SPENDER` access. See [Ownership and access](/features/activity-history#ownership-and-access). *** ## Settings changes A settings change records one `FIELD_CHANGED` event per field that changed, with `change: { "field", "old", "new" }`. | `field` | `old` / `new` | Recorded when | | - | - | - | | `nickname` | The nickname, or `null` | `editVirtualCard` with `cardNickname`, or the Fluz app. | | `spend_limit` | The limit in dollars as a number, for example `500`, or `null` when cleared | `editVirtualCard` with both `spendLimit` and `spendLimitDuration`, an approved [limit change](/features/request-virtual-card-limit-change), raising a lifetime limit in the Fluz app, or editing the card balance in the Fluz app. A balance edit records the card's resulting limit, which includes Fluz's buffer, not the balance you entered. | | `spend_limit_duration` | `DAILY`, `WEEKLY`, `MONTHLY`, `ANNUAL`, `LIFETIME`, or `null` | `editVirtualCard` with both `spendLimit` and `spendLimitDuration`, an approved limit change, or the Fluz app. | | `lock_date` | An ISO 8601 UTC timestamp, usually the end of the chosen day, for example `"2026-12-31T23:59:59.999Z"`, or `null` when cleared | `editVirtualCard` with `lockDate`, an approved lock date change, the Fluz app, or an unlock that moves or clears the lock date. | | `lock_next_use` | `true` or `false` | `editVirtualCard` with `lockCardNextUse`, or the Fluz app. | | `funding_source` | The `bankAccountId` funding the card, or `null` when the card isn't funded by a bank account | `editVirtualCard` with `bankAccountId` or `primaryFundingSource`, or the Fluz app. | | `spend_account` | The `userCashBalanceId` funding the card, or `null` when the card isn't funded by a spend account | `editVirtualCard` with `userCashBalanceId`, closing the spend account that funded the card with `closeUserCashBalance`, or the Fluz app. | | `card_type` | `SINGLE_USE` → `MULTI_USE` | A single-use card is converted to a multi-use card in the Fluz app. The conversion also records any status, lock date, or limit fields that changed with it. | A limit change records both `spend_limit` and `spend_limit_duration` when both change, and only the one that changed otherwise. A funding switch can record both `funding_source` and `spend_account`: one gets the new ID and the other changes to `null`. Switching to a bank card records the previous funding field changing to `null`. The bank card's own ID isn't recorded. *** ## Status changes Locking, unlocking, and closing a card record `FIELD_CHANGED` with `field: "status"`. | `status` value | Meaning | | - | - | | `UNLOCKED` | The card is open and can spend. | | `LOCKED` | The card is open but locked. Authorizations are declined. | | `USED` | A single-use card was spent. | | `CANCELLED` | The card was cancelled. | | `EXPIRED` | The card expired. | The history records lock state as a status. `UNLOCKED` and `LOCKED` both describe an open card, which the card queries report as `status: ACTIVE` with the lock in `authorizationSetting.lockedByUser` and `lockedBySystem`. See [Card status and limits](/virtual-cards/card-status-and-limits). | Event | `old` → `new` | `actor` | Extra keys | | - | - | - | - | | Locked by a user | `UNLOCKED` → `LOCKED` | The user, through `lockVirtualCard`, `closeUserCashBalance` with `lockAllVirtualCards`, or the Fluz app | — | | Unlocked | `LOCKED` → `UNLOCKED` | The user, through `unlockVirtualCard` or the Fluz app. The requester, for an approved unlock request. | — | | Locked by Fluz | `UNLOCKED` → `LOCKED` | See [Lock reasons](#lock-reasons) | `reason` | | Locked by Fluz support | `UNLOCKED` → `LOCKED` | `system: "cms"` | — | | Single-use card spent | → `USED` | `system: "virtual-card-service"` | — | | Cancelled | → `CANCELLED` | The user whose request cancelled the card. `system: "purchase-service"` when no user is tied to the request. | `reason` | | Expired | `UNLOCKED` or `LOCKED` → `EXPIRED` | `system: "virtual-card-expirer"` | — | An unlock can also change the lock date, recorded as a separate `lock_date` event. Unlocking a single-use card sets a lock date one day out. Unlocking a multi-use card whose lock date has passed clears it to `null`. A cancelled card's `reason` is one of: | `reason` | Meaning | | - | - | | `USER_REQUESTED_CANCEL` | A user cancelled the card. | | `PURCHASE_VIRTUAL_CARD_ERROR` | The purchase that created the card failed, so Fluz cancelled it. | | `FLUZPAY_WITHDRAW_OR_USAGE` | Fluz cancelled a reserved card to release the balance it held for another purchase. | ### Lock reasons When a card is locked without a person asking for it, the lock event carries `change.reason`: | `reason` | Why the card was locked | `actor` | | - | - | - | | `LOCK_DATE_REACHED` | The card's lock date passed. | `system: "virtual-card-service"` | | `LOCK_NEXT_USE` | The card had `lockCardNextUse` set and was used. | `system: "virtual-card-service"` | | `SINGLE_LOAD_LIMIT` | The card reached its single-load limit. | `system: "virtual-card-service"` | | `FRAUD_DECISION` | A fraud review decided to lock the card. | `system: "virtual-card-service"` | | `TOKENIZATION_RISK` | Adding the card to a digital wallet failed a risk check. | `system: "virtual-card-service"` | | `LOCKED_BY_SYSTEM` | Fluz locked the card for another reason, for example because its last digital wallet token was removed. | `system: "virtual-card-service"` | | `PURCHASE_CLEANUP` | Fluz locked the card while cleaning up a purchase that didn't complete. | The user whose request led to the lock | | `FUNDING_SOURCE_REMOVED` | The funding source behind the card was removed in the Fluz mobile app. | The user who removed it | A single-load card that drops back under its limit is unlocked again, attributed to `system: "virtual-card-service"`. *** ## Security changes Fluz records that these changes happened, and who made them, but never the sensitive value. | `field` | `old` → `new` | Recorded when | | - | - | - | | `pin` | `null` → `"updated"` | A PIN is set with [`setVirtualCardPIN`](/set-virtual-card-pin), when a card is created with a PIN, or in the Fluz app. Only successful changes are recorded. | | `wallet_token` | → `"added"` | The card is added to a digital wallet. The actor is the user. Some wallet provisioning paths record only the `"activated"` event. | | `wallet_token` | `"added"` → `"activated"` | The card network activates the wallet token. The actor is `system: "virtual-card-service"`. | | `wallet_token` | → `"removed"` | The wallet token is removed. The actor is `system: "virtual-card-service"`. | | `billing_address` | `null` → `"UPDATED"`, with `"via": "BUSINESS_ADDRESS_UPDATE"` | Fluz support updates a business's address on all its cards. One event per card whose address changed. The actor is `system: "cms"`. | *** ## Expense details Memos, receipts, and categories belong to a transaction, not to the card. When one changes, Fluz records a `FIELD_CHANGED` event on the card the transaction was made with. | `field` | `old` / `new` | Set with | | - | - | - | | `memo` | `old` is `null`. `new` is `ADDED`, `CHANGED`, or `REMOVED`. The memo text is never recorded. | `memo` | | `receipt` | `old` is `null`. `new` is `ADDED`, `CHANGED`, or `REMOVED`. The file is never recorded. | `attachmentId` | | `expense_category` | The previous and new category names, or `null` | `transactionCategory` | | `erp_category` | The previous and new QuickBooks category names, or `null` | `erpTransactionMetadata.categoryReferenceItemId` or `categoryName` | Each event also identifies the transaction: | Key | Description | | - | - | | `transaction_log_id` | The Fluz transaction the change belongs to. | | `transaction_record_id` | The transaction's `recordId`, when it's known. | | `old_id`, `new_id` | The previous and new category IDs, on category events. Omitted on the side that has no category. | These events are recorded for: * [`updateTransactionMetadata`](/features/add-expense-details). * The `UPDATE_TRANSACTION_METADATA` [bulk operation](/submit-a-bulk-operation), for `memo` and `transactionCategory`. * `bulkUpdateErpTransactionMetadata`, for `erp_category`. * Edits in the Fluz web app. In the Fluz mobile app, receipt uploads and removals, and QuickBooks category changes. They're attributed to the user who made the change and the connected application, if any. A category counts as changed only when its ID changes. Not recorded: * A memo or category set when the transaction is made. * A card's default memo or category copied onto each new transaction. * QuickBooks vendor, customer, class, billable, and memo fields, including memo edits in the Fluz mobile app. * A QuickBooks category or memo that Fluz copies into the Fluz fields. *** ## Sharing A card's ownership, access, and invite events follow the shared rules in [Ownership and access](/features/activity-history#ownership-and-access). When a recipient claims a card that was shared with them, the card records `ACCESS_GRANTED` or `ACCESS_LEVEL_CHANGED` with `"reason": "shared-card-claim"`. The claimer is both the actor and the subject. *** ## Approvals These requests record approval events on the card. The trail is described in [Approvals](/features/activity-history#approvals). | Request | `approval_type` | `approval_code` | Resulting event | | - | - | - | - | | [Create a virtual card](/features/request-virtual-card-approval) | `VIRTUAL_CARD_CREATION` | `600003` | `OBJECT_CREATED` on the new card | | [Change the spend limit](/features/request-virtual-card-limit-change) | `VIRTUAL_CARD` | `600002` | `spend_limit` / `spend_limit_duration` | | Unlock the card | `VIRTUAL_CARD` | `600005` | `status` → `UNLOCKED` | | Unlock a card declined because it was locked | `VIRTUAL_CARD` | `600102` | `status` → `UNLOCKED` | | Raise a limit after a purchase was declined | `VIRTUAL_CARD` | `600103` | `spend_limit`, attributed to `system: "virtual-card-service"` | | Change the lock date | `VIRTUAL_CARD` | `600006` | `lock_date` | | Replace a card funded by a restricted bank account | `VIRTUAL_CARD_CREATION` | `600004` | Approval events, including a decline or expiry, on the original card. Once approved, the replacement card records its own `OBJECT_CREATED`, attributed to the original cardholder, and the approval trail | | Access to the card | `ACCOUNT_ACCESS` | `100004`, `100005` | `ACCESS_GRANTED`, or `ACCESS_LEVEL_CHANGED` if the requester already had lower access | A request about an existing card that nobody decides in time records `APPROVAL_EXPIRED` on the card. A declined or expired request to create a card (`600003`) leaves no history, because no card was created. *** ## Systems you can see | `actor.system` | What it does | | - | - | | `virtual-card-service` | Locks, unlocks, and marks cards used, applies approved limit increases, and records wallet token updates from the card network. | | `virtual-card-expirer` | Expires cards. | | `scheduled-task-handler` | Creates scheduled cards and the cards of bulk orders. | | `purchase-service` | Cancels a card whose purchase failed when no user is tied to the request. | | `approvals-service` | Records expired requests and decisions made through an approval link that names no approver. | | `erp-service` | Applies a QuickBooks category automatically. | | `transactional-graph-service` | Records an expense details change made through the API when the caller can't be resolved to a user. | | `cms` | Fluz support. | *** ## What isn't recorded * Revealing card details. * Transactions, authorizations, refunds, and spend amounts. A transaction only shows up through what it caused, such as a `USED`, `LOCK_NEXT_USE`, or `SINGLE_LOAD_LIMIT` event. * The bank card's ID when the card switches to bank card funding. You see `funding_source` or `spend_account` change to `null` instead. * Turning Fluz balance funding on or off. * Card balance loads and closures at the card issuer that don't change the card's status. * Share links. Generating, claiming, or deactivating a share link isn't part of the card's history. The claim's access grant is. ## Next steps > **[Activity history overview](/features/activity-history)** > > The query, response fields, actors, approvals, and access levels. > **[Event catalog](/features/activity-history-events)** > > Every event on every object, in one place. > **[Edit a virtual card](/features/edit-virtual-card)** > > Change the settings that appear in the card's history. # Create bulk order Source: https://docs.fluz.app/features/create-bulk-order The `createVirtualCardBulkOrder` mutation allows you to create multiple virtual cards in a single request. > [!NOTE] > > **Prerequisites:** a user access token with the `CREATE_VIRTUALCARD` scope, and a `CreateVirtualCardBulkOrderInput` object. To find an `offerId`, see [Get Virtual Card Offers](/features/get-card-offers). > [!NOTE] > > Bulk orders are processed asynchronously. This mutation returns an `orderId` and an initial `orderStatus` of `PENDING` — poll [Get Bulk Order Status](/features/get-bulk-order-status) to track completion. ## Sandbox test offers | Offer ID | Program Name | Reward Value | | :- | :- | :- | | `ed669305-5e43-40a0-9a25-7a15ed174628` | Virtual Card | 1.5% | | `b23630f6-8d91-43df-84aa-a541e7691197` | Virtual Card - Mastercard Prepaid | 1.5% | | `592c394e-26cc-44ac-a145-a5f81301fe77` | Brand Locked Virtual Card - Mastercard Prepaid | 1.5% | ## Important considerations * **Offer ID:** The `offerId` is applied to all cards created within the bulk order. * **Funding Source:** You can fund your virtual cards using either your Fluz balance or a linked bank account. * If you select a bank account as the `primaryFundingSource`, you must provide the `bankAccountId`. * **Spend Limits:** The `spendLimit` you set must adhere to the program's defined spend limits for the chosen `spendLimitDuration`. An error will occur if the limit is exceeded. * **Quantity:** Each item in the `orderItems` array includes a `quantity`, allowing you to create multiple cards with the same configuration efficiently. ## Arguments * `input` (`CreateVirtualCardBulkOrderInput!`): The input object containing details for the new virtual card bulk order. ## CreateVirtualCardBulkOrderInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `offerId` | `UUID!` | The Offer ID to be used for all cards in this bulk order. Use getVirtualCardOffers to fetch a list of active offers. | Yes | | `orderItems` | `[CreateVirtualCardBulkOrderItemInput!]!` | An array of objects, each defining a set of virtual cards to be created. | Yes | ### CreateVirtualCardBulkOrderItemInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `quantity` | `Int!` | The number of virtual cards to create with this specific configuration. | Yes | | `spendLimit` | `Float!` | The maximum amount you can charge to each card. You are only charged for the amount actually used. | Yes | | `spendLimitDuration` | `VirtualCardSpendLimitDuration` | Card limit duration type. Default is `LIFETIME`. | No | | `lockDate` | `String` | The date when the card will be locked. The default is 47 months from creation. Format: yyyy-mm-dd | No | | `lockCardNextUse` | `Boolean` | Setting to lock the card after its next use. The default is `false`. | No | | `cardNickname` | `String` | The card's nickname. | No | | `primaryFundingSource` | `VirtualCardFundingSource` | Primary Funding Source for Virtual Card. Default is `FLUZ_BALANCE`. | No | | `bankAccountId` | `UUID` | The unique identifier for the bank account. Required if `primaryFundingSource` is `BANK_ACCOUNT`. | No | ## cURL example ```bash theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "mutation CreateVirtualCardBulkOrder { createVirtualCardBulkOrder( input: { offerId: \"592c394e-26cc-44ac-a145-a5f81301fe77\", orderItems: [ { quantity: 3, spendLimit: 100, spendLimitDuration: DAILY, lockCardNextUse: true, cardNickname: \"Test nickname\", primaryFundingSource: FLUZ_BALANCE }, { quantity: 1, spendLimit: 200, spendLimitDuration: WEEKLY, lockCardNextUse: false, lockDate: \"2030-10-10\", cardNickname: \"Test nickname 2\", primaryFundingSource: BANK_ACCOUNT, bankAccountId: \"7f60cb5d-683f-4181-9194-408ea92d6248\" } ] } ) { orderId orderStatus } }" }' ``` ## Sample mutation ```graphql theme={null} mutation CreateVirtualCardBulkOrder { createVirtualCardBulkOrder( input: { offerId: "592c394e-26cc-44ac-a145-a5f81301fe77" orderItems: [ { quantity: 3 spendLimit: 100 spendLimitDuration: DAILY lockCardNextUse: true cardNickname: "Test nickname" primaryFundingSource: FLUZ_BALANCE } { quantity: 1 spendLimit: 200 spendLimitDuration: WEEKLY lockCardNextUse: false lockDate: "2030-10-10" cardNickname: "Test nickname 2" primaryFundingSource: BANK_ACCOUNT bankAccountId: "7f60cb5d-683f-4181-9194-408ea92d6248" } ] } ) { orderId orderStatus } } ``` ## Sample response ```json theme={null} { "data": { "createVirtualCardBulkOrder": { "orderId": "MTFkYjIxNTYtNTEwOS00ODNmLTkwMTYtYjA5N2E2NTEyMjU5fDIzMjIxODRhLTEzMjgtNDkwYy05Mzc1LTZlOWM4MWIwMjkzZA==", "orderStatus": "PENDING", } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `orderId` | `UUID` | The unique identifier for the bulk creation order. Pass this to `getVirtualCardBulkOrderStatus` to track progress. | | `orderStatus` | `VirtualCardBulkOrderStatus!` | The status of the bulk order (e.g., `COMPLETED`, `PENDING`, `FAILED`). | ## Next steps > **[Get bulk order status](/features/get-bulk-order-status)** > > Poll the `orderId` to track processing and retrieve the cards as they're issued. # Decline codes Source: https://docs.fluz.app/features/decline-codes The list of decline codes is consistent across our API | Code | Decline Category | Short Display Decline Reason | Display Decline Reason | Resolution | | - | - | - | - | - | | Validation | Access denied | You don't have permission to access this feature. Please contact support if you believe this is an error. | Contact support | | | Validation | Too many attempts | You've exceeded the number of allowed attempts. Please wait 15 minutes before trying again. | | | | Validation | Invalid information provided | Some of the information entered is incorrect. Please review your details and try again. | | | | Validation | Missing information | Some required information is missing. Please complete all required fields and try again. | | | | Validation | Address verification error | The address on your account couldn't be verified. Please verify that it is correct or contact support for assistance. | Contact support | | | Validation | Address not found | We couldn't locate the address you selected. Please verify the address is correct and try again. | Contact support | | | Validation | Name verification error | The first name on your account couldn't be verified. Please verify that it is correct or contact support for assistance. | Contact support | | | Validation | Name verification error | The last name on your account couldn't be verified. Please verify that it is correct or contact support for assistance. | Contact support | | | Validation | Birthdate verification error | Your date of birth couldn't be verified. Please verify that it is correct or contact support for assistance. | Contact support | | | Validation | Email verification error | Your email couldn't be verified. Please verify that it is correct or contact support for assistance. | Contact support | | | Validation | Phone number verification error | Your phone number couldn't be verified. Please verify that it is correct or contact support for assistance. | Contact support | | | Validation | Billing address missing | Please add a billing address to your payment method and try again. | | | | Account | Verification error | We couldn't verify your information to create your card. Please provide your SSN or contact support for assistance. | Complete verification | | | Internal | Internal error | A server error prevented us from completing this transaction. Please try again shortly. | | | | Internal | Transaction failed | We couldn't complete this transaction. Please try again later. Any pending charge will automatically be removed within 2-7 business days. | | | | Account | Balance unavailable | We couldn't load your balance information. Please try again or contact support if the problem continues. | Contact support | | | Account | Settings unavailable | We couldn't load your cash balance settings. Please try again or contact support if the issue persists. | Contact support | | | Acquiring | Bank relinking required | We couldn't verify your bank account details. Please relink your bank account. | Relink bank | | | Acquiring | Invalid billing address | The billing address doesn't match your bank records. Please update the billing address. | Update billing address | | | Acquiring | Bank relink required | Your linked bank account is no longer active. Please relink your bank account. | Relink bank | | | Acquiring | Unavailable funding source | The selected funding source isn't available for this transaction. Please select a different funding source. | Change funding source | | | Acquiring | Unavailable funding source | The selected funding source does not have enough spend power for this transaction. Please select a different funding source. | Change funding source | | | Acquiring | Backup funding source required | Change funding source | | | | Acquiring | Backup funding source not available | Your selected backup funding source isn't available for this transaction. Please select a different funding source or switch your backup. | Change funding source | | | Acquiring | Backup funding source expired | Your selected backup funding source has expired. Please select a different funding source or update your backup. | Change funding source | | | Acquiring | Unavailable funding source | The selected funding source isn't available for this transaction. Please select a different funding source. | Change funding source | | | Acquiring | Payment processing error | This transaction couldn’t be completed due to a temporary processing error. Please try again later. | | | | Acquiring | Duplicate Transaction | This appears to be a duplicate transaction and was blocked. No action is needed if the original transaction succeeded. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Incorrect CVV | The CVV entered doesn’t match the card. Please check the CVV on your card and try again. | | | | Acquiring | Incorrect billing address | The billing address doesn't match your bank records. Please update the billing address. | Update billing address | | | Acquiring | Insufficient funds | There aren't enough funds available on this card. Please add funds to continue. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Invalid card number | The card number entered is incorrect. Please check the card number and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Expired card | This card has expired and can't be used. Please select a different funding source. | Change funding source | | | Acquiring | Issuer bank system error | This transaction couldn't be completed due to a temporary system issue on your bank's side. Please select a different funding source or try again later. | Change funding source | | | Acquiring | Unsupported transaction type | This transaction type isn't supported for this card. Please select a different funding source. | Change funding source | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | System unavailable | The bank network is temporarily unavailable. Please try again later. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Card details issue | There's an issue with the card details or status. Please update your card details | Update card details | | | Acquiring | Incorrect address | The address doesn't match the merchant records. Please update your address. | Update address | | | Acquiring | Invalid card number | The card number entered is incorrect. Please check the card number and try again. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Incorrect billing address | The billing address doesn't match your bank records. Please update the billing address. | Update billing address | | | Acquiring | Transaction details invalid | Some details about your card or transaction are missing or incorrect. Please review and try again. | | | | Acquiring | Invalid card number | The card number entered is incorrect. Check the card number and try again. | | | | Acquiring | Temporary system issue | This transaction couldn’t be completed due to a temporary issue on our side. Please try again later. | | | | Acquiring | Unavailable category | This card can’t be used for this category right now. Please choose a different category or card. | | | | Acquiring | Unavailable category | This card can’t be used for this category right now. Please choose a different category or card. | | | | Acquiring | Card temporarily not supported | This card type isn't supported at the moment. Please select a different funding source. | Change funding source | | | Acquiring | Card not authorized | This card isn't authorized for use on your account. Please contact support to enable this card. | Contact support | | | Acquiring | Insufficient funds | There aren't enough funds available on this card. Please add funds or change funding source to continue. | Add money | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Declined by your bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Connection error | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Declined by your bank | This transaction couldn’t be completed due to a temporary connection issue. Please check your connection and try again. | | | | Acquiring | Insufficient PayPal balance | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | Change funding source | | | Acquiring | Paypal account unlinked | Your PayPal account is no longer linked. Please relink your PayPal account.There aren't enough funds in your PayPal balance. Please select a different funding source. | Relink PayPal account | | | Acquiring | Paypal account closed | Your PayPal account can't be used for payments. Please select a different funding source. | Change funding source | | | Acquiring | Declined by bank | Your bank blocked this transaction for security reasons. Please confirm the transaction with your bank and try again. | | | | Acquiring | Unknown reason | This transaction was declined for an unknown reason. Please contact support for assistance. | Contact support | | | Gift Card | Unknown reason | This transaction was declined for an unknown reason. Please contact support for assistance. | Contact support | | | Gift Card | Merchant unavailable | This merchant is no longer available for purchases. Please choose a different merchant. | Choose merchant | | | Gift Card | Merchant unavailable | The merchant is temporarily unavailable. Please choose a different merchant or try again later. | Choose merchant | | | Gift Card | Limit exceeded | This transaction exceeds one of your limits. Would you like to update the card limit? | Update card limit | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Gift Card | Vendor insufficient funds | This purchase couldn’t be completed at this time. Please try again later. | | | | Gift Card | Unavailable amount | There's insufficient stock available from the merchant for this transaction. Please select a different amount or try again later. | Contact support | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Gift Card | Unknown reason | This transaction was declined for an unknown reason. Please contact support for assistance. | Contact support | | | Gift Card | Merchant connection error | This transaction couldn't be completed due to a temporary connection issue with the merchant. Please choose a different merchant or try again later. | | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Insufficient funds | There aren’t enough funds available on this card. Would you like to add funds? | Add money | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Insufficient funds | There aren’t enough funds available on this card. Would you like to add funds? | Add money | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Payouts | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Rewards | Unknown reason | This transaction was declined for an unknown reason. Please contact support for assistance. | Contact support | | | Rewards | No voucher available | There’s no active boost available on your account. | Add voucher | | | Risk & Verification Controls | Transaction error | Something went wrong with this transaction. Please try again or contact support. | Contact support | | | Risk & Verification Controls | Account paused | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | Contact support | | | Risk & Verification Controls | Account under review | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | | | | Risk & Verification Controls | Account paused | Your account is paused and can't be used for payments. Contact support to restore access. | Contact support | | | Risk & Verification Controls | Account under review | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | | | | Risk & Verification Controls | Verification required | Verification is required before this transaction can be completed. Please complete verification to proceed. | Complete verification | | | Risk & Verification Controls | Account under review | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | | | | Risk & Verification Controls | Feature access restricted | You do not have access to this feature on your account. Please contact support for assistance. | Contact support | | | Risk & Verification Controls | Verification required | Verification is required before this transaction can be completed. Please complete verification to proceed. | Complete verification | | | Risk & Verification Controls | Verification required | Verification is required before this transaction can be completed. Please complete verification to proceed. | Complete verification | | | Risk & Verification Controls | Verification required | Verification is required before this transaction can be completed. Please complete verification to proceed. | Complete verification | | | Risk & Verification Controls | Account under review | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | | | | Risk & Verification Controls | Unusual activity | Unusual activity was detected on your account. Please confirm your activity to remove the restriction and try again. | | | | Risk & Verification Controls | Account paused | Your account is paused and can't be used for payments. Contact support to restore access. | Contact support | | | Risk & Verification Controls | Open/unpaid balance | There's an unpaid balance on your account. Please pay the outstanding balance to continue. | Pay balance | | | Risk & Verification Controls | Verification required | Verification is required before this transaction can be completed. Please complete verification to proceed. | Complete verification | | | Risk & Verification Controls | Account under review | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | | | | Risk & Verification Controls | Account paused | Your account is paused and is restricted from payments. Please wait while the review is completed and try again. | Contact support | | | Risk & Verification Controls | Account paused | Your account is paused and is restricted from payments. Please wait while the review is completed and try again. | Contact support | | | Risk & Verification Controls | Verification required | Verification is required before this transaction can be completed. Please complete verification to proceed. | Complete verification | | | Risk & Verification Controls | Account under review | Your account is under review and temporarily restricted from payments. Please wait while the review is completed and try again. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your account's daily limit. Please try again later. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your account's daily limit. Please try again later. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your account's monthly limit. Please try again later. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your account's monthly limit. Please try again later. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your account's weekly limit. Please try again later. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your account's weekly limit. Please try again later. | | | | Risk & Verification Controls | Merchant limit reached | The amount exceeds this merchant's daily limit. Please try another merchant or try again later. | | | | Risk & Verification Controls | Merchant limit reached | The amount exceeds this merchant's daily limit. Please try another merchant or try again later. | | | | Risk & Verification Controls | Merchant limit reached | The amount exceeds your available spend power. Please select a different funding source. | | | | Risk & Verification Controls | Merchant limit reached | The amount exceeds this merchant's monthly limit. Please try another merchant or try again later. | | | | Risk & Verification Controls | Merchant limit reached | The amount exceeds this merchant's weekly limit. Please try another merchant or try again later. | | | | Risk & Verification Controls | Merchant limit reached | The amount exceeds this merchant's weekly limit. Please try another merchant or try again later. | | | | Risk & Verification Controls | Spend power too low | The amount exceeds your available spend power. Please complete verification to proceed. | Get verified | | | Risk & Verification Controls | Payment method not allowed | This payment method can't be used for this transaction. Please select a different funding source. | Change funding source | | | Risk & Verification Controls | Spend power too low | The amount exceeds your available spend power. Please select a different funding source. | Change funding source | | | Risk & Verification Controls | Feature access restricted | You do not have access to this feature on your account. Please contact support for assistance. | Contact support | | | Risk & Verification Controls | Incorrect PIN | The PIN entered doesn’t match the PIN set for this card. Please double-check your PIN and try again. | | | | Virtual Card | Verification required | Verification is required before this transaction can be completed. Would you like to complete verification? | Complete verification | | | Virtual Card | Account restricted | Your account has restrictions that prevent this transaction. Would you like to contact support for more information? | Contact support | | | Virtual Card | Incorrect details | Some of the card details entered are incorrect. Would you like to update the card details? | Update card details | | | Virtual Card | Incorrect details | Some of the card details entered are incorrect. Would you like to update the card details? | Update card details | | | Virtual Card | Incorrect details | Some of the card details entered are incorrect. Would you like to update the card details? | Update card details | | | Virtual Card | Incorrect details | Some of the card details entered are incorrect. Would you like to update the card details? | Update card details | | | Virtual Card | Incorrect details | Some of the card details entered are incorrect. Would you like to update the card details? | Update card details | | | Virtual Card | Incorrect details | Some of the card details entered are incorrect. Would you like to update the card details? | Update card details | | | Virtual Card | Insufficient program funds | There aren’t enough funds available in the program balance. Please try again later. | | | | Virtual Card | System error | This transaction couldn’t be completed due to a temporary system issue. Please try again later. | | | | Virtual Card | Blocked transaction | This transaction was blocked due to a restriction on this card. Would you like to contact support for more information? | Contact support | | | Virtual Card | Blocked transaction | This transaction was blocked due to a restriction on this card. Would you like to contact support for more information? | Contact support | | | Virtual Card | Blocked transaction | This transaction was blocked due to a restriction on this card. Would you like to contact support for more information? | Contact support | | | Virtual Card | Card locked | This card is locked and can’t be used for payments. Would you like to contact support to unlock the card? | Contact support | | | Virtual Card | Card locked | This card is locked and can’t be used for payments. Would you like to contact support to unlock the card? | Contact support | | | Virtual Card | Admin locked card | This card was locked by an administrator. Please contact your account admin to unlock the card. | | | | Virtual Card | Locked to another merchant | This card is locked for use with a different merchant and can’t be used here. Would you like to lock your card to a different merchant? | Change merchant | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Spend limit exceeded | This transaction would exceed the merchant limit on this card. Please try again later | | | | Virtual Card | Spend limit exceeded | This transaction would exceed the merchant limit on this card. Please try again later | | | | Virtual Card | Spend limit exceeded | This transaction would exceed the merchant limit on this card. Please try again later | | | | Virtual Card | Transaction limit exceeded | This transaction exceeds the allowed amount for this card. Would you like to update your card limits? | Update card limit | | | Virtual Card | Vendor limit exceeded | This transaction exceeds the vendor’s allowed limits. Please try again later | | | | Virtual Card | Spend limit exceeded | This transaction would exceed the merchant limit on this card. Please try again later | | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Card limit exceeded | This transaction would exceed your card’s lifetime limit. Would you like to update your card limit? | Update card limit | | | Virtual Card | Unknown reason | This transaction was declined for an unknown reason. Would you like to contact support? | Contact support | | | Virtual Card | System error | This transaction couldn’t be completed due to a temporary system issue. Please try again later. | | | | Virtual Card | Bank account unlinked | Your bank account is no longer linked. Would you like to relink your bank account? | Relink bank | | | Virtual Card | Bank account unresponsive | Your bank didn’t respond while processing the payment. Would you like to change your funding source? | Change funding source | | | Virtual Card | Insufficient funds | There aren’t enough funds available on this card. Would you like to add funds? | Add money | | | Virtual Card | Insufficient funds | There aren’t enough funds available on this card. Would you like to add funds? | Add money | | | Virtual Card | Insufficient funds | There aren’t enough funds available on this card. Would you like to add funds? | Add money | | | Virtual Card | Insufficient funds | There aren’t enough funds available on this card. Would you like to add funds? | Add money | | | Virtual Card | Unable to process | We can't complete this transaction right now due to your account status. Our support team can help you resolve this quickly. | | | | Virtual Card | Card already active | This user already has an active card on this account. You'll need to deactivate their existing card before issuing a new one. | | | | Virtual Card | Card profile not found | We couldn't locate the card profile needed to complete this request. Please contact support for assistance. | Contact support | | | Virtual Card | Account holder does not have an open application | Account holder does not have an open application. | Contact support | | | Virtual Card | Verification in progress | Your application is still being reviewed. We'll notify you once it's approved and you can start using your card. | | | | Virtual Card | Product not found | We're missing some account setup details. Please contact support to resolve this issue. | Contact support | | | Virtual Card | Card profile pending approval | This card profile hasn't been approved yet. Please wait for approval or contact support for more information. | Contact support | | | Virtual Card | Card product not found | We couldn't locate the card product needed for this transaciton. Please contact support for assistance. | Contact support | | | Virtual Card | Card product unavailable | This card product is either not found or no longer active. Please contact support to explore available card options. | Contact support | | | Virtual Card | Card already exists | A card has already been created for this transaction. | | | | Virtual Card | Duplicate card detected | A card with these details already exists in your account. Contact support if you need help managing your cards. | Contact support | | | Virtual Card | Card type not supported | Only Visa or Mastercard cards are accepted. Please use a different card to complete this transaction. | | | | Virtual Card | Not available in your country | This product isn't currently available in your country. Please contact support to learn about available options in your region. | Contact support | | | Virtual Card | Unable to create card | New cards can't be issued for this product right now. Contact support to explore your options. | Contact support | | | Virtual Card | Address missing | We need a recipient address to create this card. Please add a shipping address and try again. | | | | Virtual Card | Missing recipient details | We need the recipient's full name and shipping address to create this card. Please add these details and try again. | | | | Virtual Card | Cardholder account closed | We can't create a card for a closed account. Please contact support if you believe this is an error. | Contact support | | | Virtual Card | Verification code error | We couldn't generate the security code for your card. Please try again or contact support if the problem continues. | Contact support | | | Virtual Card | Unable to create card | We couldn't complete the security setup for your card. Please try again in a few moments or contact support for help. | Contact support | | | Virtual Card | Invalid BIN prefix | Only the approved BIN prefix is allowed for this operation. Please verify your card configuration. | Complete verification | | | Virtual Card | Inactive account | This account isn't currently active. Contact support to reactivate your account and resume card use. | Contact support | | | Virtual Card | Verification required | We need to verify your identity before you can use your card. Please complete the verification process or contact support for help. | Complete verification | | | Virtual Card | Conversion not available | Physical cards can't be converted to virtual cards when reissuing. Please request a new virtual card separately if needed. | | | | Virtual Card | Card can't be reissued | You can only reissue your latest active or suspended card. Please try again with the correct card. | | | | Virtual Card | Card can't be reissued | This card can't be reissued due to its current status. Contact support to discuss replacement options. | Contact support | | | Virtual Card | Can't activate this card | Only unactivated or suspended cards can be activated. This card is in a different status. Contact support for assistance. | Contact support | | | Virtual Card | Card expired | Your card is no longer valid. Contact support or create a new card. | Contact support | | | Virtual Card | Card must be active | This action requires an active card. Please try again or contact support for help. | Contact support | | | Virtual Card | Verification error | We couldn't verify your information to create your card. Please contact support for assistance. | Contact support | | | Virtual Card | Verification in progress | Your application for this product is still being reviewed. | Contact support | | | Virtual Card | Application declined | Your card application for this card program wasn't approved. | Contact support | | | Virtual Card | U.S. address required | This offer is only available to customers with a U.S. address. Please contact support if you believe this is an error. | Contact support | | | Virtual Card | Virtual card update failed | We couldn't update your virtual card settings. Please try again or contact support if the problem continues. | Contact support | | | Virtual Card | Virtual card setup issue | We couldn't access or create your virtual card settings. Please try again or contact support if the problem continues. | Contact support | | | Virtual Card | Agreement not found | We couldn't locate your approved virtual card agreement. Please contact support for assistance. | Contact support | | | Virtual Card | Invalid spend limit | Please enter a lower spend limit for this virtual card and try again. | | | | Virtual Card | Unable to create authorized user | There was a problem creating an authorized user for this virtual card. Please try again or contact support if the issue continues. | Contact support | | # Funding source activity history Source: https://docs.fluz.app/features/funding-source-activity Every event Fluz records on a bank card, bank account, or PayPal account — added, verified, edited, removed, and set as primary or backup — and how each one appears in objectChangeHistory. A funding source's activity history shows when the bank card, bank account, or PayPal account was added, every change to its details, when it became the primary or backup funding source, and when it was removed. Read it with `objectChangeHistory` and `objectType: FUNDING_SOURCES`. > [!NOTE] > > **Prerequisites:** the `VIEW_OBJECT_CHANGE_HISTORY` scope on your application, with either a user access token or your API key, and the ID of a funding source on the account: a `bankCardId`, `bankAccountId`, or `paypalVaultId` from [`getWallet`](/features/view-funding-sources). See [Activity history overview](/features/activity-history) for the full query, response fields, and access rules. ```graphql theme={null} query { objectChangeHistory( objectType: FUNDING_SOURCES objectId: "c4d3e2f1-a0b9-4c8d-9e7f-6a5b4c3d2e1f" ) { changeType change createdAt subjectUserId actor { userId system applicationId applicationName } } } ``` ## At a glance A funding source is one of three things, and each records its own events. ```mermaid theme={null} flowchart LR FS["Funding source
FUNDING_SOURCES"] FS --> BC["Bank card"] FS --> BA["Bank account"] FS --> PP["PayPal"] BC --> BC1["Added, removed, verified,
type corrected, nickname,
billing address, preferred category"] BA --> BA1["Linked, relinked, removed,
enabled or disabled, nickname,
billing address, owner"] PP --> PP1["Added, removed"] BC1 --> P["Set or replaced as primary or backup"] BA1 --> P PP1 --> P ``` For one table of every funding source event, see the [Event catalog](/features/activity-history-events#funding-source-events). A change records one `FIELD_CHANGED` event per field, with `change: { "field", "old", "new" }`. *** ## Bank cards | Event | `changeType` / `field` | `old` → `new` | Recorded when | | - | - | - | - | | Added | `OBJECT_CREATED` | `change: {}` | [`addBankCard`](/features/add-bank-card), or adding a card in the Fluz web or mobile app. | | Added again after removal | `status` | `REMOVED` or `PENDING_REMOVAL` → `ACTIVE` | A removed card is added again. | | Removed | `status` | `ACTIVE` or `PENDING_REMOVAL` → `REMOVED` | [`deleteBankCard`](/features/delete-bank-card), or removing the card in the Fluz app. | | Removal requested | `status` | The previous status → `PENDING_REMOVAL` | Several cards are removed at once in the Fluz web app. One event per card. | | Verified | `verified` | `false` → `true` | The user confirms the verification charge amount in the Fluz app. | | Card type corrected | `card_type` | The previous type, or `null` → `DEBIT`, `CREDIT`, or `PREPAID` | Fluz corrects the card type from the card's BIN data while processing a purchase or deposit. Every card on the same BIN can record this event. The actor is `system: "purchase-service"`. | | Nickname changed | `nickname` | The previous and new nickname, or `null` | `updateBankCardNickname`, or the Fluz app. A nickname passed to `addBankCard` is recorded as a change from the card's previous nickname, which is `null` for a new card. | | Billing address changed | `billing_address` | The previous and new address ID, or `null` | The billing address is changed in the Fluz app. The address itself is never recorded. | | Preferred category changed | `preferred_mcc` | The previous and new merchant category ID, or `null` | `updateBankCardPreferredMerchantCategoryCode`, or the Fluz app. The value is Fluz's ID for the merchant category, not the four-digit code you pass in. When the Fluz mobile app moves a category to another card, the card that loses it records a change to `null`. | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` → `true`, or `true` → `false` | See [Primary and backup changes](#primary-and-backup-changes). | A card removed with `deleteBankCard` records no `subjectUserId`. A card removed, or marked for removal, in the Fluz app records the user who added it as `subjectUserId`, when that's someone other than the actor. When several cards are removed at once, the card's history ends at `PENDING_REMOVAL`. The final removal isn't recorded. *** ## Bank accounts | Event | `changeType` / `field` | `old` → `new` | Recorded when | | - | - | - | - | | Linked | `OBJECT_CREATED` | `change: {}` | [`completePlaidLink`](/features/link-via-plaid), or linking the account in the Fluz web or mobile app through Plaid or MX. | | Relinked | `status` | `DISABLED` → `ENABLED` | [`completePlaidRelink`](/features/relink-accounts), or linking a previously removed account again in the Fluz app. | | Removed | `status` | The previous status → `DISABLED` | `removePlaidBankInstitution`, removing the account or its bank in the Fluz app, or Fluz disabling it. One event per account whose status changed. | | Status changed | `status` | `ENABLED` ↔ `DISABLED` | The account is enabled or disabled in the Fluz mobile app. | | Nickname changed | `nickname` | The previous and new nickname, or `null` | The nickname is changed in the Fluz app. | | Billing address changed | `billing_address` | The previous and new address ID, or `null` | The billing address is changed in the Fluz mobile app. The address itself is never recorded. | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` → `true`, or `true` → `false` | See [Primary and backup changes](#primary-and-backup-changes). | When a bank account is linked or relinked through the API, the event names the connected application. Changes that Fluz makes on its own, such as disabling an account after a notice from the bank connection provider, are attributed to `system: "identity-service"`. An account that Fluz support disables through a bulk import is also attributed to `system: "identity-service"`, with `"via": "CMS_IMPORT"`. Bank accounts are the only funding sources that support ownership. [`assignObjectOwner`](/api-reference/mutations/assign-object-owner) and [`transferObjectOwner`](/api-reference/mutations/transfer-object-owner) with `objectType: FUNDING_SOURCES` record `OWNER_ASSIGNED` and `OWNER_TRANSFERRED` on the bank account, as described in [Ownership and access](/features/activity-history#ownership-and-access). *** ## PayPal | Event | `changeType` / `field` | `old` → `new` | Recorded when | | - | - | - | - | | Added | `OBJECT_CREATED` | `change: {}` | A PayPal account is added in the Fluz web or mobile app. | | Added again | `status` | The previous status → `ACTIVE` | A removed PayPal account is added again. | | Removed | `status` | The previous status → `REMOVED` | The PayPal account is removed in the Fluz app. | | Set or replaced as primary or backup | `primary_funding_source`, `backup_funding_source` | `false` → `true`, or `true` → `false` | See [Primary and backup changes](#primary-and-backup-changes). | *** ## Primary and backup changes Choosing a new [primary or backup funding source](/features/primary-and-backup-funding) touches two payment methods, and each records its own event. Through the API, the primary is a bank account and the backup is a bank card. In the Fluz app, either role can be a bank card, bank account, PayPal account, or spend account. * The newly selected funding source records `primary_funding_source` or `backup_funding_source`: `false` → `true`. * The funding source it replaced, if there was one, records the same field: `true` → `false`. When the other side is a spend account, the Fluz web app records its event in the [spend account's history](/features/spend-account-activity-history). The Fluz mobile app and the API record nothing you can read for it. These events are recorded for `setPrimaryFundingSource`, `setBackupFundingSource`, and the payment method settings in the Fluz web and mobile apps. Selecting the funding source that's already set records nothing. When the Fluz mobile app picks a backup automatically, the event is attributed to `system: "fluz-mobile-backend"`. A primary funding source that Fluz sets automatically after a bank account is linked isn't recorded. *** ## Systems you can see | `actor.system` | What it does | | - | - | | `purchase-service` | Corrects a bank card's type. | | `identity-service` | Links, relinks, and disables bank accounts from background jobs, bank connection notices, and Fluz support bulk imports (`"via": "CMS_IMPORT"`). | | `fluz-mobile-backend` | Picks a backup funding source automatically. | *** ## What isn't recorded * Failed verification attempts. * The final step of a bulk card removal. * A primary funding source set automatically after linking. * Balance, transaction, and spend power refreshes for linked bank accounts. * Charges and deposits made with the funding source. Use [Transaction activity](/features/transactions-details-overview) for those. ## Next steps > **[Activity history overview](/features/activity-history)** > > The query, response fields, actors, approvals, and access levels. > **[Event catalog](/features/activity-history-events)** > > Every event on every object, in one place. > **[Primary & backup funding](/features/primary-and-backup-funding)** > > Read and change the funding sources that appear in this history. # Get all transactions Source: https://docs.fluz.app/features/get-all-transactions ## Overview The Transactions API allows you to retrieve a comprehensive history of all financial transactions associated with your account. This includes purchases, deposits, withdrawals, transfers, bill payments, and all other financial activities. **Endpoint Type**: GraphQL Query\ **Authentication**: Required (JWT Bearer Token)\ **Required Scopes**: `LIST_PAYMENT` AND `LIST_PURCHASES`\ **Rate Limit**: Standard GraphQL [rate limits](/concepts/rate-limits) apply *** ## Quick Start ### Basic Query ```graphql theme={null} query GetTransactions { getTransactions { transactions { recordId transactionType amount status channel connectedAppId connectedAppName createdAt } totalCount hasNextPage } } ``` *** ## Query Structure ```graphql theme={null} getTransactions( filter: TransactionFilterInput paginate: OffsetInput ): TransactionConnection! ``` ### Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `filter` | `TransactionFilterInput` | No | Filtering criteria for transactions | | `paginate` | `OffsetInput` | No | Pagination parameters (default: limit=20, offset=0) | *** ## Response Structure ### TransactionConnection ```graphql theme={null} type TransactionConnection { transactions: [Transaction] totalCount: Int! hasNextPage: Boolean! } ``` | Field | Type | Description | | - | - | - | | `transactions` | `[Transaction]` | Array of transaction records | | `totalCount` | `Int!` | Total count of transactions matching filter (for pagination) | | `hasNextPage` | `Boolean!` | Whether more results are available | *** ## Filter Options ### TransactionFilterInput ```graphql theme={null} input TransactionFilterInput { # Record & Status recordId: [UUID] status: [TransactionStatus] # Amount Filters amount: Float amountGte: Float amountLte: Float finalAmount: Float finalAmountGte: Float finalAmountLte: Float # Cashback Filters cashbackAmount: Float cashbackAmountGte: Float cashbackAmountLte: Float cashbackPercentage: Float cashbackPercentageGte: Float cashbackPercentageLte: Float # Fee Filters feeAmount: Float feeAmountGte: Float feeAmountLte: Float # Date Filters createdGte: DateTime createdLte: DateTime updatedGte: DateTime updatedLte: DateTime # Merchant Filters merchantId: [UUID] merchant: [String] # Transaction Properties transactionType: [String] channel: [String!] category: [String] # Virtual Card Filters virtualCardProgram: [String] virtualCard: [UUID] # Other fundingSource: [String] userCashBalanceId: [UUID] referenceId: String liabilityId: UUID } ``` ### Filter Field Details #### Record & Status Filters ##### `recordId` **Type**: `[UUID]`\ **Description**: Filter by specific transaction record IDs. **Example**: ```graphql theme={null} filter: { recordId: ["550e8400-e29b-41d4-a716-446655440000"] } ``` ##### `status` **Type**: `[TransactionStatus]`\ **Description**: Filter by transaction status. **Options**: * `PENDING` - Transaction is being processed * `SETTLED` - Transaction completed successfully **Example**: ```graphql theme={null} filter: { status: [SETTLED] } ``` *** #### Amount Filters ##### `amount`, `amountGte`, `amountLte` **Type**: `Float`\ **Description**: Filter by exact amount or amount range in USD. * `amount` - Exact amount match * `amountGte` - Minimum amount (greater than or equal) * `amountLte` - Maximum amount (less than or equal) **Example**: ```graphql theme={null} # Transactions between $10 and $500 filter: { amountGte: 10.00 amountLte: 500.00 } ``` ##### `finalAmount`, `finalAmountGte`, `finalAmountLte` **Type**: `Float`\ **Description**: Filter by final amount (amount + fees). **Example**: ```graphql theme={null} filter: { finalAmountGte: 25.00 } ``` *** #### Cashback Filters ##### `cashbackAmount`, `cashbackAmountGte`, `cashbackAmountLte` **Type**: `Float`\ **Description**: Filter by cashback amount earned. **Example**: ```graphql theme={null} # Transactions that earned $5 or more in cashback filter: { cashbackAmountGte: 5.00 } ``` ##### `cashbackPercentage`, `cashbackPercentageGte`, `cashbackPercentageLte` **Type**: `Float`\ **Description**: Filter by cashback rate percentage. **Example**: ```graphql theme={null} # Transactions with 5% or higher cashback filter: { cashbackPercentageGte: 5.0 } ``` *** #### Fee Filters ##### `feeAmount`, `feeAmountGte`, `feeAmountLte` **Type**: `Float`\ **Description**: Filter by transaction fee amount. **Example**: ```graphql theme={null} # Transactions with fees filter: { feeAmountGte: 0.01 } ``` *** #### Date Filters ##### `createdGte`, `createdLte` **Type**: `DateTime`\ **Format**: ISO 8601 (e.g., `2025-01-01T00:00:00Z`)\ **Description**: Filter by transaction creation date range. **Example**: ```graphql theme={null} filter: { createdGte: "2025-01-01T00:00:00Z" createdLte: "2025-01-31T23:59:59Z" } ``` ##### `updatedGte`, `updatedLte` **Type**: `DateTime`\ **Description**: Filter by transaction last update date range. *** #### Merchant Filters ##### `merchantId` **Type**: `[UUID]`\ **Description**: Filter by specific merchant IDs. **Example**: ```graphql theme={null} filter: { merchantId: ["550e8400-e29b-41d4-a716-446655440000"] } ``` ##### `merchant` **Type**: `[String]`\ **Description**: Filter by merchant names (matches against destination field). **Example**: ```graphql theme={null} filter: { merchant: ["Amazon", "Walmart"] } ``` *** #### Transaction Properties ##### `transactionType` **Type**: `[String]`\ **Description**: Filter by specific transaction types. **Common Values**: * `Add Money` - Deposits * `Gift Card Purchase` - Gift card purchases * `Transfer - In` - Incoming transfers * `Transfer - Out` - Outgoing transfers * `Virtual Card Purchase` - Virtual card purchases * `Withdrawal` **Example**: ```graphql theme={null} filter: { transactionType: ["Gift Card Purchase", "Add Money"] } ``` ##### `channel` **Type**: `[String!]`\ **Description**: Filter by platform channel. **Common Values**: * `WEB` - Web browser * `MOBILE` - Mobile app * `API` - API requests **Example**: ```graphql theme={null} filter: { channel: ["WEB", "MOBILE"] } ``` ##### `category` **Type**: `[String]`\ **Description**: Filter by transaction category. **Example**: ```graphql theme={null} filter: { category: ["Shopping", "Travel"] } ``` *** #### Virtual Card Filters ##### `virtualCardProgram` **Type**: `[String]`\ **Description**: Filter by virtual card program/issuer. **Example**: ```graphql theme={null} filter: { virtualCardProgram: ["TRANSPECOS", "SUTTON"] } ``` ##### `virtualCard` **Type**: `[UUID]`\ **Description**: Filter by specific virtual card IDs. **Example**: ```graphql theme={null} filter: { virtualCard: ["vc-1", "vc-2"] } ``` *** #### Other Filters ##### `fundingSource` **Type**: `[String]`\ **Description**: Search for funding source names (partial match on source or destination). **Example**: ```graphql theme={null} filter: { fundingSource: ["Visa"] } ``` ##### `referenceId` **Type**: `String`\ **Description**: Filter by external reference ID (e.g., purchase display ID). **Example**: ```graphql theme={null} filter: { referenceId: "1000123" } ``` ##### `liabilityId` **Type**: `UUID`\ **Description**: Filter by liability ID (for bill payments). *** ## Pagination ### OffsetInput ```graphql theme={null} input OffsetInput { limit: Int = 20 offset: Int = 0 } ``` | Field | Type | Default | Max | Description | | - | - | - | - | - | | `limit` | Int | 20 | 20 | Number of transactions to return per page | | `offset` | Int | 0 | - | Number of transactions to skip | **Example - Page 1**: ```graphql theme={null} paginate: { limit: 20 offset: 0 } ``` **Example - Page 2**: ```graphql theme={null} paginate: { limit: 20 offset: 20 } ``` **Example - Check if more pages exist**: ```graphql theme={null} query GetTransactions { getTransactions(paginate: { limit: 20, offset: 0 }) { transactions { recordId } hasNextPage # Use this to determine if there are more results totalCount # Total matching records } } ``` *** ## Transaction Type ```graphql theme={null} type Transaction { recordId: UUID user: String! accountId: UUID! userId: UUID transactionType: String amount: Float! destination: String source: String externalFundingSourceActivity: Float fluzBalanceActivity: Float fee: Float cashback: Float # Ending balances after transaction giftCardPrepaymentBalanceAvailableBalance: Float giftCardPrepaymentBalanceTotalBalance: Float cashBalanceAvailableBalance: Float cashBalanceTotalBalance: Float seatBalanceAvailableBalance: Float seatBalanceTotalBalance: Float reserveBalanceAvailableBalance: Float reserveBalanceTotalBalance: Float otherCashBalanceAvailableBalance: Float otherCashBalanceTotalBalance: Float status: TransactionStatus referenceId: String description: String note: String category: String cardLastFour: String cardDisplayName: String originalCurrencyAmount: Float originalCurrencyCode: String conversionRate: Float merchantId: UUID descriptorId: UUID virtualCardProgram: String cashbackRate: Float bonusCashbackRate: Float channel: String sourceType: String logoUrl: String platformInstitutionLogo: String challengeLogoUrl: String invitedAccountId: UUID expectedClearedDate: DateTime liabilityId: UUID isGiftCardBalanceAffected: Boolean isCashBalanceAffected: Boolean isSeatBalanceAffected: Boolean isReserveBalanceAffected: Boolean transferId: UUID usedUserCashBalanceId: UUID connectedAppId: UUID connectedAppName: String memo: String transactionCategory: String attachmentUrl: String createdAt: DateTime updatedAt: DateTime } ``` ### Field Descriptions #### Core Transaction Fields | Field | Type | Description | | - | - | - | | `recordId` | UUID | Unique identifier for this transaction record | | `user` | String | Display name of the user associated with transaction | | `accountId` | UUID | Account that owns this transaction | | `userId` | UUID | User who initiated the transaction | | `transactionType` | String | Type of transaction (PURCHASE, DEPOSIT, etc.) | | `amount` | Float | Primary transaction amount in USD | | `source` | String | Where the funds came from (e.g., "Bank Card \*\*\*\*1234") | | `destination` | String | Where the funds went (e.g., merchant name) | | `status` | TransactionStatus | Current status (PENDING or SETTLED) | #### Financial Details | Field | Type | Description | | - | - | - | | `externalFundingSourceActivity` | Float | Change to external funding sources (bank cards/accounts) | | `fluzBalanceActivity` | Float | Change to Fluz internal balances | | `fee` | Float | Fees charged for this transaction | | `cashback` | Float | Cashback earned from this transaction | | `cashbackRate` | Float | Cashback rate percentage | | `bonusCashbackRate` | Float | Additional bonus cashback rate | #### Balance Snapshots **Important**: All balance fields show the balance **after** this transaction was applied. | Field | Description | | - | - | | `cashBalanceAvailableBalance` | Available cash balance after transaction | | `cashBalanceTotalBalance` | Total cash balance after transaction | | `seatBalanceAvailableBalance` | Available rewards balance after transaction | | `seatBalanceTotalBalance` | Total rewards balance after transaction | | `giftCardPrepaymentBalanceAvailableBalance` | Available prepayment balance after transaction | | `giftCardPrepaymentBalanceTotalBalance` | Total prepayment balance after transaction | | `reserveBalanceAvailableBalance` | Available reserve balance after transaction | | `reserveBalanceTotalBalance` | Total reserve balance after transaction | | `otherCashBalanceAvailableBalance` | Other cash available balance after transaction | | `otherCashBalanceTotalBalance` | Other cash total balance after transaction | #### Transaction Details | Field | Type | Description | | :- | :- | :- | | `referenceId` | String | External reference (e.g., purchase display ID) | | `description` | String | Human-readable description | | `note` | String | Additional notes | | `category` | String | Transaction category | | `cardLastFour` | String | Last 4 digits of card used (if applicable) | | `cardDisplayName` | String | Display name of payment method | | `memo` | String | A free-text memo attached to this transaction. Set via [`updateTransactionMetadata`](/features/add-expense-details) or at transaction time on deposits, purchases, and transfers. | | `transactionCategory` | String | Category label attached to this transaction. Set via [`updateTransactionMetadata`](/features/add-expense-details) or at transaction time on deposits, purchases, and transfers. | | `attachmentUrl` | String | A signed URL for the file attached to this transaction. Set via [`updateTransactionMetadata`](/features/add-expense-details) or at transaction time. **This URL expires — do not store it.** Re-fetch the transaction when you need to access the file. | #### Merchant Information | Field | Type | Description | | - | - | - | | `merchantId` | UUID | Merchant identifier | | `descriptorId` | UUID | Transaction descriptor ID | | `logoUrl` | String | URL to merchant/source logo | #### Virtual Card Information | Field | Type | Description | | - | - | - | | `virtualCardProgram` | String | Virtual card program (LITHIC, MARQETA, etc.) | #### Currency Conversion | Field | Type | Description | | - | - | - | | `originalCurrencyAmount` | Float | The amount the card issuer reports as requested, in minor units of the merchant's currency (`6300` HKD is 63.00 HKD). On a return it is the amount still on hold before that return, so reconcile on `amount` | | `originalCurrencyCode` | String | Original currency code (e.g., "EUR") | | `conversionRate` | Float | Exchange rate applied | #### Metadata | Field | Type | Description | | - | - | - | | `channel` | String | Platform channel (WEB, MOBILE, API) | | `sourceType` | String | Type of funding source | | `liabilityId` | UUID | Associated liability (for bill payments) | | `transferId` | UUID | Associated transfer (for P2P) | | `usedUserCashBalanceId` | UUID | Specific cash balance used | | `isGiftCardBalanceAffected` | Boolean | Whether gift card balance changed | | `isCashBalanceAffected` | Boolean | Whether cash balance changed | | `isSeatBalanceAffected` | Boolean | Whether rewards balance changed | | `isReserveBalanceAffected` | Boolean | Whether reserve balance changed | | `connectedAppId` | UUID | Associated application ID | | `connectedAppName` | String | Associated application name | | `createdAt` | DateTime | When transaction was created | | `updatedAt` | DateTime | When transaction was last updated | *** ## Examples ### Example 1: Basic Transaction List **Query**: ```graphql theme={null} query GetRecentTransactions { getTransactions( paginate: { limit: 10, offset: 0 } ) { transactions { recordId transactionType amount description status cashback createdAt } totalCount hasNextPage } } ``` **Response**: ```json theme={null} { "data": { "getTransactions": { "transactions": [ { "recordId": "550e8400-e29b-41d4-a716-446655440000", "transactionType": "GIFT_CARD_PURCHASE", "amount": 50.00, "description": "Gift card purchase at Amazon", "status": "SETTLED", "cashback": 2.50, "createdAt": "2025-01-15T14:30:00Z" }, { "recordId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "transactionType": "DEPOSIT", "amount": 100.00, "description": "Cash Balance Deposit", "status": "SETTLED", "cashback": 0.00, "createdAt": "2025-01-14T10:15:00Z" } ], "totalCount": 2, "hasNextPage": false } } } ``` *** ### Example 2: Filtered by Date Range **Query**: ```graphql theme={null} query GetJanuaryTransactions { getTransactions( filter: { createdGte: "2025-01-01T00:00:00Z" createdLte: "2025-01-31T23:59:59Z" } paginate: { limit: 20, offset: 0 } ) { transactions { recordId transactionType amount description status createdAt } totalCount hasNextPage } } ``` *** ### Example 3: Purchases Only with Balances **Query**: ```graphql theme={null} query GetPurchaseHistory { getTransactions( filter: { transactionType: ["GIFT_CARD_PURCHASE"] status: [SETTLED] } paginate: { limit: 20, offset: 0 } ) { transactions { recordId referenceId amount description source destination cashback cashbackRate cashBalanceAvailableBalance seatBalanceAvailableBalance createdAt } totalCount hasNextPage } } ``` **Response**: ```json theme={null} { "data": { "getTransactions": { "transactions": [ { "recordId": "550e8400-e29b-41d4-a716-446655440000", "referenceId": "1000123", "amount": 50.00, "description": "Gift card purchase at Amazon", "source": "Visa ****1234", "destination": "Amazon", "cashback": 2.50, "cashbackRate": 5.0, "cashBalanceAvailableBalance": 102.50, "seatBalanceAvailableBalance": 25.00, "createdAt": "2025-01-15T14:30:00Z" } ], "totalCount": 1, "hasNextPage": false } } } ``` *** ### Example 4: High-Cashback Transactions **Query**: ```graphql theme={null} query GetHighCashbackTransactions { getTransactions( filter: { cashbackPercentageGte: 5.0 status: [SETTLED] } paginate: { limit: 10, offset: 0 } ) { transactions { recordId transactionType amount description cashback cashbackRate status createdAt } totalCount } } ``` *** ### Example 5: Amount Range Filter **Query**: ```graphql theme={null} query GetMediumTransactions { getTransactions( filter: { amountGte: 50.00 amountLte: 200.00 status: [SETTLED] } paginate: { limit: 20, offset: 0 } ) { transactions { recordId amount transactionType description createdAt } totalCount hasNextPage } } ``` *** ### Example 6: Virtual Card Transactions **Query**: ```graphql theme={null} query GetVirtualCardTransactions { getTransactions( filter: { virtualCardProgram: ["LITHIC", "MARQETA"] } paginate: { limit: 20, offset: 0 } ) { transactions { recordId amount description virtualCardProgram merchantId status createdAt } totalCount } } ``` *** ### Example 7: Pagination Example **Query - Get first page and check for more**: ```graphql theme={null} query GetFirstPage { getTransactions(paginate: { limit: 20, offset: 0 }) { transactions { recordId amount transactionType } totalCount hasNextPage } } ``` **Response shows hasNextPage=true**: ```json theme={null} { "data": { "getTransactions": { "transactions": [...], "totalCount": 150, "hasNextPage": true } } } ``` **Query - Get second page**: ```graphql theme={null} query GetSecondPage { getTransactions(paginate: { limit: 20, offset: 20 }) { transactions { recordId amount transactionType } totalCount hasNextPage } } ``` *** ## Error Handling ### Common Errors #### Missing or Invalid Token ```json theme={null} { "errors": [ { "message": "Authentication required", "extensions": { "code": "UNAUTHENTICATED" } } ] } ``` **HTTP Status**: 401 Unauthorized *** #### Insufficient Permissions ```json theme={null} { "errors": [ { "message": "Insufficient permissions: LIST_PAYMENT and LIST_PURCHASES scopes required", "extensions": { "code": "FORBIDDEN", "requiredScopes": ["LIST_PAYMENT", "LIST_PURCHASES"] } } ] } ``` **HTTP Status**: 403 Forbidden *** #### Invalid Filter Parameters ```json theme={null} { "errors": [ { "message": "Invalid date format for createdGte", "extensions": { "code": "BAD_USER_INPUT" } } ] } ``` **HTTP Status**: 400 Bad Request *** #### Rate Limit Exceeded ```json theme={null} { "message": "Rate limit exceeded" } ``` **HTTP Status**: 429 Too Many Requests This is a plain JSON body, not a GraphQL `errors` array. See [Rate limits](/concepts/rate-limits) for how long the block lasts. *** ## Best Practices ### 1. Use Pagination Effectively Always check `hasNextPage` to determine if more results exist: ```javascript theme={null} async function fetchAllTransactions() { const allTransactions = []; let offset = 0; const limit = 20; while (true) { const result = await getTransactions({ paginate: { limit, offset } }); allTransactions.push(...result.transactions); if (!result.hasNextPage) break; offset += limit; } return allTransactions; } ``` ### 2. Request Only Needed Fields Specify only the fields you need to reduce response size: ```graphql theme={null} # Good - minimal fields getTransactions { transactions { recordId amount transactionType createdAt } totalCount hasNextPage } # Less efficient - requesting all 40+ fields getTransactions { transactions { recordId transactionType amount ... (all fields) } } ``` ### 3. Use Date Filters for Historical Queries When querying older transactions, always use date filters: ```graphql theme={null} # Good filter: { createdGte: "2024-01-01T00:00:00Z" createdLte: "2024-12-31T23:59:59Z" } ``` ### 4. Cache Settled Transactions Transactions with `status: SETTLED` are immutable and can be cached: ```javascript theme={null} // Example caching strategy const cacheKey = `transactions:${accountId}:${createdGte}:${createdLte}`; let result = cache.get(cacheKey); if (!result) { result = await getTransactions({ filter: { status: ['SETTLED'], createdGte, createdLte } }); cache.set(cacheKey, result, '1 hour'); } ``` ### 5. Combine Filters Efficiently Use range filters to narrow results before applying other filters: ```graphql theme={null} # Efficient - date range first filter: { createdGte: "2025-01-01T00:00:00Z" createdLte: "2025-01-31T23:59:59Z" transactionType: ["GIFT_CARD_PURCHASE"] amountGte: 50.00 } ``` *** ## Rate Limits This query shares the GraphQL API's limits. See [Rate limits](/concepts/rate-limits) for the numbers and what a rejected request looks like. *** ## Code Examples ### JavaScript/TypeScript ```typescript theme={null} import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; const client = new ApolloClient({ uri: 'https://transactional-graph.fluzapp.com/api/v1/graphql', cache: new InMemoryCache(), headers: { authorization: `Bearer ${accessToken}`, }, }); const GET_TRANSACTIONS = gql` query GetTransactions($filter: TransactionFilterInput, $paginate: OffsetInput) { getTransactions(filter: $filter, paginate: $paginate) { transactions { recordId transactionType amount description status cashback createdAt } totalCount hasNextPage } } `; async function fetchTransactions() { const { data } = await client.query({ query: GET_TRANSACTIONS, variables: { filter: { status: ['SETTLED'], createdGte: '2025-01-01T00:00:00Z', }, paginate: { limit: 20, offset: 0, }, }, }); return data.getTransactions; } ``` *** ### Python ```python theme={null} import requests def get_transactions(access_token, created_gte=None, created_lte=None, limit=20, offset=0): url = "https://transactional-graph.fluzapp.com/api/v1/graphql" query = """ query GetTransactions($filter: TransactionFilterInput, $paginate: OffsetInput) { getTransactions(filter: $filter, paginate: $paginate) { transactions { recordId transactionType amount description status cashback createdAt } totalCount hasNextPage } } """ variables = { "filter": {}, "paginate": { "limit": limit, "offset": offset } } if created_gte: variables["filter"]["createdGte"] = created_gte if created_lte: variables["filter"]["createdLte"] = created_lte headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } response = requests.post( url, json={"query": query, "variables": variables}, headers=headers ) return response.json()["data"]["getTransactions"] # Usage result = get_transactions( access_token="your_token_here", created_gte="2025-01-01T00:00:00Z", limit=20 ) print(f"Found {result['totalCount']} transactions") print(f"Has more pages: {result['hasNextPage']}") ``` *** ### cURL ```bash theme={null} curl -X POST https://transactional-graph.fluzapp.com/api/v1/graphql \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "query GetTransactions($filter: TransactionFilterInput, $paginate: OffsetInput) { getTransactions(filter: $filter, paginate: $paginate) { transactions { recordId transactionType amount description status cashback createdAt } totalCount hasNextPage } }", "variables": { "filter": { "status": ["SETTLED"], "createdGte": "2025-01-01T00:00:00Z" }, "paginate": { "limit": 20, "offset": 0 } } }' ``` *** ## FAQ ### Q: What's the maximum number of transactions I can retrieve in one request? **A**: The maximum limit is 20 transactions per request. Use the `hasNextPage` field to implement pagination. ### Q: How far back does transaction history go? **A**: All transactions from account creation are available indefinitely. ### Q: Are pending transactions included? **A**: Yes, pending transactions are included by default. Filter by `status: [SETTLED]` to exclude them. ### Q: What timezone are the timestamps in? **A**: All timestamps are in UTC (ISO 8601 format). ### Q: What scopes do I need? **A**: You need **both** `LIST_PAYMENT` **AND** `LIST_PURCHASES` scopes. ### Q: Can I filter by user ID within my account? **A**: No, the API always returns all transactions for your account. There is no user-level filtering. ### Q: What's the difference between `amount` and `finalAmount` filters? **A**: * `amount` filters the base transaction amount * `finalAmount` filters amount + fees (the total charged to the user) ### Q: How do I filter by date range? **A**: Use `createdGte` and `createdLte` for creation date: ```graphql theme={null} filter: { createdGte: "2025-01-01T00:00:00Z" createdLte: "2025-01-31T23:59:59Z" } ``` *** ## Support * **API Status**: [https://status.fluz.app](https://status.fluz.app) * **Developer Portal**: [https://developers.fluz.app](https://developers.fluz.app) * **Support Email**: [api-support@fluz.app](mailto:api-support@fluz.app) * **Slack Community**: [https://fluz-dev.slack.com](https://fluz-dev.slack.com) *** ## Changelog ### v1.0.0 (Branch: 13-fluz-15659-add-transactions-query-and-webhook-to-api) * Initial release of Transactions Query API * Support for comprehensive filtering (15+ filter types) * Pagination with `TransactionConnection` response type * Balance snapshots included in transaction records * Requires `LIST_PAYMENT` and `LIST_PURCHASES` scopes * Account-level transaction access only *** **Need help?** Contact our developer support team at [api-support@fluz.app](mailto:api-support@fluz.app) or visit our [Developer Portal](https://developers.fluz.app). # Get bulk order status Source: https://docs.fluz.app/features/get-bulk-order-status Use the `getVirtualCardBulkOrderStatus` query to check the status of a bulk virtual card creation order and retrieve the details of the created cards as the order is processed. > [!NOTE] > > **Prerequisites:** a user access token with the `CREATE_VIRTUALCARD` scope, and the `orderId` returned by [Create Bulk Order](/features/create-bulk-order). > [!WARNING] > > The response includes full card numbers, CVVs, and expiry dates in plaintext. Handle it as sensitive cardholder data — transmit over TLS, never log it, and surface it only to authorized users. ## Arguments * `input` (`GetVirtualCardBulkOrderStatusInput!`): The input object containing the identifier for the bulk order. ## GetVirtualCardBulkOrderStatusInput fields | Field | Type | Description | Required | | :- | :- | :- | :- | | `orderId` | `String!` | The unique identifier for the bulk order, which you receive from the `createVirtualCardBulkOrder` mutation. | Yes | ## cURL example ```bash theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "query GetVirtualCardBulkOrderStatus { getVirtualCardBulkOrderStatus( input: { orderId: \"ZTBhYTg2YmQtZWQwYS00OWQxLTk5ZmUtZDBhZGY3ZjYxZGUyfDI0NjMyM2NmLTBkMzEtNGI2YS04NGQ2LTRmNGI2NzMzNmU5Yw==\" } ) { orderStatus orderId virtualCards { cardNumber expiryMMYY cvv cardHolderName virtualCardId billingAddress { city } } successfulCardCreations failedCardCreations totalCards } }" }' ``` ## Sample query ```graphql theme={null} query GetVirtualCardBulkOrderStatus { getVirtualCardBulkOrderStatus( input: { orderId: "<>" } ) { orderStatus orderId virtualCards { cardNumber expiryMMYY cvv cardHolderName virtualCardId billingAddress { city } } successfulCardCreations failedCardCreations totalCards } } ``` ## Sample response ```json theme={null} { "data": { "getVirtualCardBulkOrderStatus": { "orderStatus": "COMPLETED", "orderId": "<>", "virtualCards": [ { "cardNumber": "5111147189413564", "expiryMMYY": "08/29", "cvv": "143", "cardHolderName": "Test", "virtualCardId": "104b50d2-97e5-49ee-b9fd-b42120d68212", "billingAddress": { "city": "San Jose" } }, { "cardNumber": "5111143166431356", "expiryMMYY": "08/29", "cvv": "135", "cardHolderName": "Test", "virtualCardId": "03291f04-8358-44f7-ac58-73a6643ac07f", "billingAddress": { "city": "San Jose" } } ], "successfulCardCreations": 4, "failedCardCreations": 0, "totalCards": 4 } } } ``` ## Response fields | Field | Type | Description | | - | - | - | | `orderId` | `String` | The unique identifier for the bulk creation order. | | `orderStatus` | `VirtualCardBulkOrderStatus!` | The status of the bulk order (e.g., `COMPLETED`, `PENDING`, `FAILED`). | | `virtualCards` | `[VirtualCardDetails]` | Full details for each successfully created card. Populated as cards are created. | | `virtualCards[].cardNumber` | `String` | The full 16-digit virtual card number. | | `virtualCards[].expiryMMYY` | `String` | The expiration date of the card in MM/YY format. | | `virtualCards[].cvv` | `String` | The 3-digit security code for the card. | | `virtualCards[].cardHolderName` | `String` | The name assigned to the cardholder. | | `virtualCards[].virtualCardId` | `UUID` | The unique identifier for the virtual card. | | `virtualCards[].billingAddress` | `Object` | The billing address associated with the virtual card. | | `successfulCardCreations` | `Int` | The number of cards successfully created so far. | | `failedCardCreations` | `Int` | The number of cards that failed to be created. | | `totalCards` | `Int` | The total number of cards requested in the order. | > [!TIP] > > Poll this query until `orderStatus` is `COMPLETED` (or `FAILED`). While `PENDING`, `virtualCards` fills in incrementally and `successfulCardCreations` climbs toward `totalCards`. ## Next steps > **[Reveal a virtual card](/recipes/reveal-virtual-card)** > > Retrieve sensitive card details securely for a single card. > **[Virtual card error codes](/features/virtual-card-error-codes)** > > Interpret `failedCardCreations` and handle partial-failure cases. # Get declined transactions Source: https://docs.fluz.app/features/get-decline-transactions ## Overview The Declined Transactions API allows you to retrieve a comprehensive history of all financial transactions associated with your account that were declined/failed. This includes purchases, deposits, withdrawals, transfers, bill payments, and all other financial activities. **Endpoint Type**: GraphQL Query\ **Authentication**: Required (JWT Bearer Token)\ **Rate Limit**: Standard GraphQL [rate limits](/concepts/rate-limits) apply > [!WARNING] > > **Authorization required** > > This mutation requires `LIST_PAYMENT` & `LIST_PURCHASES` scopes. Ensure your access token has been granted this scope before attempting to get a list of declined transactions. *** ## Declined Transaction The DeclineTransaction object contains information. Here's the table: | Field name | Type | Description | | :- | :- | :- | | `transactionId` | `UUID!` | Unique identifier of the declined transaction | | `transactionType` | `String!` | Type of transaction that was declined (e.g. "Gift Card Purchase", "Virtual Card Purchase", "Add Money", "Withdrawal", "Transfer - Out", "Bill Payment Initiation", "Issue Virtual Card") | | `amount` | `Float!` | Total transaction amount attempted, in the transaction's currency | | `fluzAmount` | `Float!` | Portion funded from Fluz internal balances (cash/rewards/reserve/prepayment). Defaults to 0 when not applicable | | `externalFundingAmount` | `Float!` | Portion funded from an external source (e.g. linked bank/card). Defaults to 0 when not applicable | | `currency` | `String!` | ISO 4217 currency code. Defaults to "USD" when not specified | | `sourceRecordType` | `String` | Type of the originating source record (e.g. originating domain/entity type) | | `sourceRecordId` | `String` | Identifier of the originating source record | | `accountId` | `String!` | Identifier of the Fluz account that owns the transaction | | `userId` | `String` | Identifier of the user who attempted the transaction. May be absent for account-level/system transactions | | `source` | `String` | Source of funds / origin label (e.g. funding source or originating party) | | `destination` | `String` | Destination label (e.g. merchant or recipient). Typically the merchant for merchant transactions | | `status` | `DeclinedTransactionStatus!` | Outcome status: `DECLINED` or `FAILED` | | `merchantId` | `String` | Identifier of the associated merchant, when known | | `descriptorId` | `String` | Identifier of the resolved merchant descriptor used to enrich merchant metadata | | `liabilityId` | `String` | Identifier of the associated liability record, when applicable | | `merchantName` | `String` | Display name of the merchant. Populated from destination when a merchant is identified | | `merchantCountry` | `String` | Country of the merchant | | `merchantCity` | `String` | City of the merchant | | `merchantState` | `String` | State/region of the merchant | | `logoUrl` | `String` | URL of the logo to display (typically the merchant/brand logo) | | `category` | `String` | Category/classification of the transaction or merchant | | `cardLastFour` | `String` | Last four digits of the card used | | `cardDisplayName` | `String` | Human-friendly display name of the card used | | `virtualCardId` | `String` | Identifier of the virtual card involved, when applicable | | `channel` | `String` | Channel through which the transaction was initiated (e.g. app, web, integration) | | `externalFundingSourceType` | `String` | Type of external funding source used (e.g. bank account, debit card) | | `externalFundingSourceId` | `String` | Identifier of the external funding source used | | `fundingSourceSubtype` | `String` | Subtype of the funding source (e.g. checking vs. savings, card network subtype) | | `isCashBalanceUsed` | `Boolean!` | True if the user's cash balance was applied/attempted | | `isPrepaymentBalanceUsed` | `Boolean!` | True if the gift-card prepayment balance was applied/attempted | | `isRewardsBalanceUsed` | `Boolean!` | True if the rewards balance was applied/attempted | | `isReserveBalanceUsed` | `Boolean!` | True if the reserve balance was applied/attempted | | `transactionDateTime` | `String!` | Timestamp of the transaction as an ISO 8601 string (derived from creation time) | | `spendAccountNickname` | `String` | Nickname of the spend account used. Falls back to "Main account" when not set | | `declineTitle` | `String` | Short user-facing title summarizing the decline | | `declineReason` | `String` | User-facing reason explaining why the transaction was declined | | `declineDescription` | `String` | Longer user-facing description with additional decline detail | | `declineCtaText` | `String` | Call-to-action text suggesting the next step for the user | | `declineCategory` | `String` | Categorization of the decline (used to group/route decline messaging) | | `offerId` | `UUID` | Identifier of the associated offer, when applicable | | `issuer` | `String` | Card issuer associated with the card/transaction | | `bin` | `String` | Bank Identification Number (first digits) of the card used | | `limitAmount` | `String` | Spend-limit amount relevant to the decline (e.g. the limit exceeded) | | `limitDuration` | `String` | Duration/window the spend limit applies to (e.g. per-transaction, daily, monthly) | | `lockOnNextUse` | `String` | Indicates whether the card is set to lock on next use | | `lockDate` | `String` | Date associated with a card lock, when applicable | | `bankAccountNickname` | `String` | Nickname of the bank account associated with the funding source | | `bankAccountLastFour` | `String` | Last four digits of the bank account associated with the funding source | | `isPrivate` | `Boolean` | Privacy flag indicating whether the transaction should be treated as private | | `createdAt` | `DateTime!` | Timestamp when the transaction record was created | | `updatedAt` | `DateTime!` | Timestamp when the transaction record was last updated | ## Quick Start To obtain a list of declined transactions associated with your account, use the `getDeclinedTransactions` query. This call returns a list of declined transactions details, including the `transactionId`, `amount` , `status` & `destination`. ### Variables | Field | Type | Required | Description | | - | - | - | - | | `filter` | `UserCashBalanceFilterInput` | No | Filtering criteria for spend accounts | | `paginate` | `OffsetInput` | No | Pagination parameters (default: limit=20, offset=0) | ### Sample Request ```graphql theme={null} query GetDeclinedTransactions( $filter: DeclinedTransactionFilterInput $paginate: OffsetInput ) { getDeclinedTransactions(filter: $filter, paginate: $paginate) { totalCount hasNextPage transactions { transactionId transactionType amount fluzAmount externalFundingAmount currency status accountId userId source destination merchantId merchantName merchantCity merchantState merchantCountry logoUrl category cardLastFour cardDisplayName virtualCardId declineTitle declineReason declineDescription declineCtaText declineCategory isCashBalanceUsed isPrepaymentBalanceUsed isRewardsBalanceUsed isReserveBalanceUsed externalFundingSourceType externalFundingSourceId fundingSourceSubtype bankAccountNickname bankAccountLastFour channel offerId issuer bin limitAmount limitDuration lockOnNextUse lockDate isPrivate createdAt updatedAt } } } ``` > [!NOTE] > > Each page returns at most 20 declined transactions, so paginate to fetch the full list. ### Sample Response The response will contain a list of declined transactions ```json theme={null} { "data": { "getDeclinedTransactions": { "totalCount": 1, "hasNextPage": false, "transactions": [ { "transactionId": "237b8450-7d2c-4233-9227-420bd7ed569d", "transactionType": "Virtual Card Purchase", "amount": 125.00, "fluzAmount": 0.00, "externalFundingAmount": 0.00, "currency": "USD", "status": "DECLINED", "accountId": "58c46f99-472f-4c5b-a4b5-776fa757263a", "userId": "23dff16a-773f-4c5d-accf-a61e70f1afa7", "source": "ACH Plaid Silver Standard 0.1% Interest Saving 1111", "destination": "Uber", "sourceRecordType": "VIRTUAL_CARD_TRANSACTION_ACTIVITY", "sourceRecordId": "3b6e6b77-d720-4679-8461-919e0b28df00", "merchantId": "9f83fa82-435e-4553-9525-8c7871825f74", "descriptorId": "2f30bea0-33a9-4b4b-95ce-b19662821d45", "liabilityId": null, "merchantName": "Uber", "merchantCountry": "USA", "merchantCity": null, "merchantState": null, "logoUrl": "https://storage.googleapis.com/fluz-fluz-file-uploads-prod-ricuyxowbwlfprel/UBER-logo.jpg", "category": "Miscellaneous Specialty Retail", "cardLastFour": "3258", "cardDisplayName": null, "virtualCardId": "73b9708a-92e4-45d5-930c-ae24e4997f91", "channel": null, "externalFundingSourceType": "BANK_ACCOUNT", "externalFundingSourceId": "7bc2c9ad-fd58-4be1-b782-284c59f54900", "fundingSourceSubtype": "SAVING", "isCashBalanceUsed": true, "isPrepaymentBalanceUsed": true, "isRewardsBalanceUsed": true, "isReserveBalanceUsed": false, "transactionDateTime": "2026-06-23T15:44:33.928Z", "spendAccountNickname": "Main account", "declineTitle": "Insufficient funds", "declineReason": "There aren't enough funds available on this card. Would you like to add funds?", "declineDescription": "Your $125.00 transaction was declined because your funding source has insufficient funds.", "declineCtaText": "Add money", "declineCategory": "Virtual Card", "offerId": null, "issuer": null, "bin": null, "limitAmount": null, "limitDuration": null, "lockOnNextUse": null, "lockDate": null, "bankAccountNickname": "ACH Plaid Silver Standard 0.1% Interest Saving", "bankAccountLastFour": "1111", "isPrivate": false, "createdAt": "2026-06-23T15:44:33.928Z", "updatedAt": "2026-06-23T15:44:41.205Z" } ] } } } ``` ### Filter Options | Field | Type | Description | | :- | :- | :- | | `transactionId` | `[UUID]` | Filter by specific transaction record IDs | | `status` | `[DeclinedTransactionStatus]` | Filter by status (`DECLINED` or `FAILED`). Defaults to both when omitted | | `amount` | `Float` | Filter by exact amount | | `amountGte` | `Float` | Filter by minimum amount (greater than or equal) | | `amountLte` | `Float` | Filter by maximum amount (less than or equal) | | `fluzAmount` | `Float` | Filter by exact Fluz amount | | `fluzAmountGte` | `Float` | Filter by minimum Fluz amount (greater than or equal) | | `fluzAmountLte` | `Float` | Filter by maximum Fluz amount (less than or equal) | | `createdGte` | `DateTime` | Filter by creation date (greater than or equal) | | `createdLte` | `DateTime` | Filter by creation date (less than or equal) | | `updatedGte` | `DateTime` | Filter by last updated date (greater than or equal) | | `updatedLte` | `DateTime` | Filter by last updated date (less than or equal) | | `merchantId` | `[UUID]` | Filter by merchant IDs | | `merchant` | `[String]` | Filter by merchant names (matched against `destination` field) | | `transactionType` | `[String]` | Filter by transaction types (e.g. `"Gift Card Purchase"`, `"Virtual Card Purchase"`) | | `channel` | `[String!]` | Filter by channel (e.g. `"UWP"`, `"app"`) | | `category` | `[String]` | Filter by transaction or merchant category | | `virtualCardId` | `[UUID]` | Filter by specific virtual card IDs | | `fundingSource` | `[String]` | Filter by funding source names | | `userCashBalanceId` | `[UUID]` | Filter by spend account IDs | | `liabilityId` | `UUID` | Filter by liability ID (for bill payment transactions) | **Example - Get only** `DECLINED `**spend accounts** ```json theme={null} { "filter": { "status": [DECLINED] } } ``` ### Pagination | Field | Type | Default | Max | Description | | - | - | - | - | - | | `limit` | `Int` | 20 | 20 | Number of transactions to return per page | | `offset` | `Int` | 0 | - | Number of transactions to skip | **Example - Page 1**: ```json theme={null} { "paginate": { "limit": 20, "offset": 0 } } ``` **Example - Page 2**: ```json theme={null} { "paginate": { "limit": 20, "offset": 20, } } ``` *** ## Best Practices ### 1. Use Pagination Effectively Always check `hasNextPage` to determine if more results exist: ```javascript theme={null} async function fetchDeclinedTransactions() { const transactions = []; let offset = 0; const limit = 20; while (true) { const result = await getDeclinedTransactions({ paginate: { limit, offset } }); transactions.push(...result.transactions); if (!result.hasNextPage) break; offset += limit; } return transactions; } ``` ### 2. Request Only Needed Fields Specify only the fields you need to reduce response size: ```graphql theme={null} # ✅ Good - minimal fields getDeclinedTransactions { transactions { transactionId transactionType amount createdAt } totalCount hasNextPage } # ❌ Less efficient - requesting all 40+ fields getDeclinedTransactions { transactions { transactionId transactionType amount ... (all fields) } } ``` ### 3. Use Date Filters for Historical Queries When querying older transactions, always use date filters: ```graphql theme={null} # ✅ Good filter: { createdGte: "2024-01-01T00:00:00Z" createdLte: "2024-12-31T23:59:59Z" } ``` ### 5. Combine Filters Efficiently Use range filters to narrow results before applying other filters: ```graphql theme={null} # ✅ Efficient - date range first filter: { createdGte: "2025-01-01T00:00:00Z" createdLte: "2025-01-31T23:59:59Z" transactionType: ["GIFT_CARD_PURCHASE"] amountGte: 50.00 } ``` *** ## Rate Limits This query shares the GraphQL API's limits. See [Rate limits](/concepts/rate-limits) for the numbers and what a rejected request looks like. # Get gift card purchases Source: https://docs.fluz.app/features/get-gift-card-purchases To view historical gift card purchases and virtual cards on your account, use the `getUserPurchases` [query](/api-reference/overview). The query requires `LIST_PURCHASES` scope. This query response can get bulky, so we ask you to provide a pagination input, and an optional filter input: ## Pull Purchase History You can use the following query in order to view your transactions. 1. `getUserPurchases` — Get User Purchases ## Pagination Input The `OffsetInput` argument is used to manage pagination in API responses, allowing you to control how many items are returned per page and how many items should be skipped from the beginning of the list. ### Fields **`limit`** **(Int)**: This field specifies the number of items to return per page. The maximum number of items you can request per page is 20. If you do not specify a value for limit, it will default to 20. **`offset`** **(Int)**: This field specifies the number of items to skip before starting to return the items. If you do not specify a value for offset, it will default to 0, meaning it will start from the first item in the list. ## Filter Input The `UserPurchaseFilterInput` argument is used to filter the purchases returned in the response. It allows you to retrieve purchases made by the user associated to your app, or purchases made under the account associated to your app, etc. It is optional. If it is not provided, the response will return purchases made by the user associated to your app, across all accounts. ### Fields **`scope`** **(Array of `PurchaseScopeFilter`)**: This field specifies the type of filter to use. ##### `PurchaseScopeFilter` enum * TOKEN\_USER: filter based on userId * TOKEN\_ACCOUNT: filter based on accountId ### Examples ```javascript theme={null} // QUERY VARIABLES // returns purchases made by the user across all accounts { "filter": { "scope": ["TOKEN_USER"] } } ``` ```javascript theme={null} // QUERY VARIABLES // returns purchases made under the account across all users { "filter": { "scope": ["TOKEN_ACCOUNT"] } } ``` ```javascript theme={null} // QUERY VARIABLES // returns purchases made by the user and account { "filter": { "scope": ["TOKEN_USER", "TOKEN_ACCOUNT"] } } ``` # Response When you run this query, it will respond with an array of your historical gift card purchases and generated virtual cards. ```json theme={null} { "data": { "getUserPurchases": [ { "purchaseId": "255f8245-02c7-4817-901e-15fe265f6968", "purchaseDisplayId": "1019688", "purchaseBankCardId": "255f8245-02c7-4817-901e-15fe265f6968", "bankAccountId": "255f8245-02c7-4817-901e-15fe265f6968", "purchaseAmount": 987.65, "fluzpayAmount": 123.45, "seatRewardValue": 987.65, "paypalVaultId": "255f8245-02c7-4817-901e-15fe265f6968", "createdAt": "2007-12-03T10:15:30Z", "purchaserUserId": "4c42e368-d150-492d-8dec-6cda1b58ffa9", "accountId": "d67af480-21dc-4c23-93ff-3b7288e52e1a", "giftCard": { // GiftCardFragment details }, "virtualCard": { // VirtualCardFragment details } }, { "purchaseId": "4f4b3c5d-6f7e-8a9b-0c1d-23e4f5678a90", "purchaseDisplayId": "1019688", "purchaseBankCardId": "4f4b3c5d-6f7e-8a9b-0c1d-23e4f5678a90", "bankAccountId": "4f4b3c5d-6f7e-8a9b-0c1d-23e4f5678a90", "purchaseAmount": 250.00, "fluzpayAmount": 50.00, "seatRewardValue": 10.00, "paypalVaultId": "4f4b3c5d-6f7e-8a9b-0c1d-23e4f5678a90", "createdAt": "2024-01-10T08:20:00Z", "giftCard": { // GiftCardFragment details }, "virtualCard": { // VirtualCardFragment details } }, { "purchaseId": "7e8f9a0b-1c2d-3e4f-5g6h-789i0jklm1no", "purchaseDisplayId": "1019688", "purchaseBankCardId": "7e8f9a0b-1c2d-3e4f-5g6h-789i0jklm1no", "bankAccountId": "7e8f9a0b-1c2d-3e4f-5g6h-789i0jklm1no", "purchaseAmount": 450.75, "fluzpayAmount": 75.00, "seatRewardValue": 30.00, "paypalVaultId": "7e8f9a0b-1c2d-3e4f-5g6h-789i0jklm1no", "createdAt": "2024-02-15T14:45:00Z", "giftCard": { // GiftCardFragment details }, "virtualCard": { // VirtualCardFragment details } } ] } } ``` ## Fields in the UserPurchase Object | Field name | Type | Description | | :- | :- | :- | | purchaseId | String | A unique identifier for the purchase. | | purchaseDisplayId | String | The display ID of the purchase. | | purchaseBankCardId | String | The identifier of the bank card used for the purchase. | | bankAccountId | String | The identifier of the bank account associated with the purchase. | | purchaseAmount | Float | The total amount spent in the purchase. | | fluzpayAmount | Float | The amount of available Fluz balance used in the transaction. | | seatRewardValue | Float | The value of any network seat rewards generated from the purchase. | | paypalVaultId | String | The identifier of the PayPal vault used for the purchase. | | createdAt | DateTime | The timestamp indicating when the purchase was made. | | purchaserUserId | String | The identifier of the user who made the purchase. | | accountId | String | The identifier of the account who made the purchase. | | giftCard | GiftCard | Contains details about any gift card used in the purchase, represented by the GiftCardFragment. | | virtualCard | VirtualCard | Contains details about any virtual card used in the purchase, represented by the VirtualCardFragment. | # Get virtual card transactions Source: https://docs.fluz.app/features/get-virtual-card-transactions Retrieve transactions for one or more virtual cards, with support for filtering and pagination. Use `getVirtualCardTransactions` to query virtual card activity by card ID, transaction type, date range, or pagination options. * Provide `virtualCardIds` to fetch transactions for specific cards. * Omit `virtualCardIds` to fetch transactions across all cards on the authenticated account. **Required scopes:** `REVEAL_VIRTUALCARD` ## Arguments ### `input` — `GetVirtualCardTransactionsInput` | Field | Type | Required | Description | | - | -: | :-: | - | | `virtualCardIds` | `[UUID!]` | No | List of virtual card IDs to retrieve transactions for. Limited to 10 IDs when provided. If omitted, transactions are returned across all virtual cards on the authenticated account. | | `filters` | `VirtualCardTransactionFiltersInput` | No | Filters used to narrow the transaction results. | | `paginate` | `OffsetInput` | No | Pagination controls for limiting and offsetting results. | ## Filters | Field | Type | Required | Description | | - | -: | :-: | - | | `transactionTypes` | `[VirtualCardTransactionListType!]` | No | Filter by transaction type. Supported values include `PURCHASE`, `REFUND`, and `DECLINE`. | | `dateRangeStart` | `String` | No | Inclusive start of the transaction date range in ISO 8601 format. Must be used with `dateRangeEnd`. | | `dateRangeEnd` | `String` | No | Inclusive end of the transaction date range in ISO 8601 format. Must be greater than or equal to `dateRangeStart`. | ## Pagination | Field | Type | Description | | - | - | - | | `limit` | `Int` | Maximum number of transactions to return. Capped at `500`. | | `offset` | `Int` | Number of transactions to skip. | **Defaults:** * When `virtualCardIds` is omitted and `limit` is not specified, the default `limit` is **100**. * When `virtualCardIds` is provided and `limit` is not specified, all matching transactions for each card may be returned. We recommend specifying a `limit` to control response size. **Behavior:** * When `virtualCardIds` is provided, `limit` and `offset` apply **per card**. * When `virtualCardIds` is omitted, `limit` and `offset` apply **across the authenticated account**. > **Note:** Requesting multiple cards with a high `limit` may result in large responses. ## Validation Errors The following inputs are rejected before execution: * `dateRangeStart` and `dateRangeEnd` must be provided together. * `dateRangeEnd` must be greater than or equal to `dateRangeStart`. * Both must be valid ISO 8601 timestamps. * `paginate.limit` cannot exceed 500. * `virtualCardIds`, when provided, must contain between 1 and 10 valid UUIDs. ## Example: Specific cards ```graphql theme={null} query GetVirtualCardTransactions { getVirtualCardTransactions( input: { virtualCardIds: [ "2ed71ba0-d457-47ed-8ceb-d3fe6ce5c900" "bd3be748-a0fb-4193-80a7-88419bc72dab" ] filters: { transactionTypes: [PURCHASE, REFUND] } paginate: { limit: 20 offset: 0 } } ) { virtualCardId transactions { transactionDate transactionType transactionStatus transactionAmount merchantName mcc merchantCountryCode originalCurrencyCode originalCurrencyAmount currencyConversionRate } } } ``` ## Example: All cards (date range) Use this when you do not know which cards had activity during a specific time period. ```graphql theme={null} query GetAccountTransactionsByDate { getVirtualCardTransactions( input: { filters: { dateRangeStart: "2026-04-01T00:00:00Z" dateRangeEnd: "2026-04-30T23:59:59Z" } paginate: { limit: 100 offset: 0 } } ) { virtualCardId transactions { transactionDate transactionType transactionAmount merchantName mcc merchantCountryCode originalCurrencyCode originalCurrencyAmount currencyConversionRate } } } ``` ## cURL Example ```bash theme={null} curl -X POST \ https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer YOUR_USER_ACCESS_TOKEN' \ -d '{ "query": "query { getVirtualCardTransactions(input: { filters: { dateRangeStart: \"2026-04-01T00:00:00Z\", dateRangeEnd: \"2026-04-30T23:59:59Z\" }, paginate: { limit: 100, offset: 0 } }) { virtualCardId transactions { transactionDate transactionType transactionAmount merchantName mcc merchantCountryCode originalCurrencyCode originalCurrencyAmount currencyConversionRate } } }" }' ``` ## Response Fields * `virtualCardId` (`UUID`) — The virtual card associated with the returned transactions. * `transactions` (`[VirtualCardTransaction!]`) — List of transactions for the virtual card. * `transactionDate` (`String`) — Date and time the transaction occurred in ISO 8601 format. * `transactionType` (`String`) — Transaction type, such as `PURCHASE`, `REFUND`, or `DECLINE`. * `transactionAmount` (`Float`) — Transaction amount in USD. May be `null`. * `transactionStatus` (`String`) — Lifecycle status, such as `CLEARED` or `PROCESSING`. * `transactionApproval` (`String`) — Approval status of the transaction. * `transactionResponseCode` (`String`) — Card network response code. * `merchantName` (`String`) — Merchant name when available. May be `null`. * `paymentMethod` (`String`) — Payment method used for the transaction. * `mcc` (`Int`) — Merchant category code. May be `null`. * `merchantCountryCode` (`String`) — Merchant country code. May be `null`. * `originalCurrencyCode` (`String`) — ISO 4217 currency code of the original transaction (e.g. USD, HKD, EUR). May be null when the original currency is unknown or not applicable. * `originalCurrencyAmount` (`Float`) — Original amount in the currency’s minor units. For example, `6300` for HKD means `HK$63.00`. * `currencyConversionRate` (`Float`) — FX conversion rate used to convert the original currency to USD. `1.0` for USD transactions. ## Notes * FX fields are returned together. They are either all populated or all null. * `originalCurrencyAmount` is returned in minor units: * HKD 6300 = HK\$63.00 * JPY 100 = ¥100 * KWD 1000 = 1.000 KD ### Code Example: **Get Virtual Card Transactions** # Gift card activity history Source: https://docs.fluz.app/features/giftcard-activity-history Every event Fluz records on a gift card — purchase, archive and used status, balance edits, expense details, sharing, and approvals — and how each one appears in objectChangeHistory. A gift card's activity history shows who bought the card, every change to its status and balance, who it was shared with, and the approval behind the purchase. Read it with `objectChangeHistory` and `objectType: GIFT_CARDS`. > [!NOTE] > > **Prerequisites:** the `VIEW_OBJECT_CHANGE_HISTORY` scope on your application, with either a user access token or your API key, and the `giftCardId` of a gift card on the account. See [Activity history overview](/features/activity-history) for the full query, response fields, and access rules. ```graphql theme={null} query { objectChangeHistory( objectType: GIFT_CARDS objectId: "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c" ) { changeType change createdAt subjectUserId approvalId actor { userId system applicationId applicationName } } } ``` ## At a glance ```mermaid theme={null} flowchart LR GC["Gift card
GIFT_CARDS"] GC --> A["Purchased
API, schedule, approval, Fluz app"] GC --> B["Status
archived, reactivated"] GC --> C["Balance
edited by a user"] GC --> D["Expense details
memo, receipt, category"] GC --> E["Ownership and sharing
owner, access, invites"] GC --> F["Approvals
purchase and access requests"] ``` Each box is a group of events below. For one table of every gift card event, see the [Event catalog](/features/activity-history-events#gift-card-events). *** ## Purchase The gift card records `OBJECT_CREATED`, with `change: {}`, when it's purchased. | How the card was purchased | `actor` | `subjectUserId` | | - | - | - | | [`purchaseGiftCard`](/purchase-gift-card) | The calling user and the connected application | — | | A scheduled purchase | `system: "scheduled-task-handler"` | The user the card was bought for | | An approved [gift card purchase request](/features/request-gift-card-purchase-approval) | The requester, with `approvalId` set | — | | The Fluz web or mobile app | The signed-in user | — | A card bought through an approval also gets the request's `APPROVAL_REQUESTED` and `APPROVAL_APPROVED` events, dated when they happened. A declined request doesn't buy a card, so there's no history to read. New gift cards also record `OWNER_ASSIGNED` for their first owner, and an `ACCESS_GRANTED` event with `"reason": "owner-floor"` for the owner's `SPENDER` access. See [Ownership and access](/features/activity-history#ownership-and-access). *** ## Status changes Archiving or reactivating a gift card records `FIELD_CHANGED` with `field: "status"`. | Event | `old` → `new` | Made in | | - | - | - | | Archived | `ACTIVE` → `ARCHIVED` | The Fluz web or mobile app. Marking a card as spent in the web app archives it. | | Reactivated | `ARCHIVED` → `ACTIVE` | The Fluz web or mobile app | The actor is the user who made the change. A card with the older used status can show `old` as `USED` when the change is made in the web app, or `NOT ACTIVE` when it's made in the mobile app. ## Balance changes Editing a gift card's remaining balance records `FIELD_CHANGED` with `field: "balance"`. `old` and `new` are numbers in the card's currency, for example `25` → `12.5`. Balance edits are made in the Fluz web and mobile apps. The balance event records an edit by a user. Spending the card at the merchant isn't recorded. *** ## Expense details Memos, receipts, and categories belong to a transaction, not to the card. When one changes on a gift card purchase, Fluz records a `FIELD_CHANGED` event on the gift card. | `field` | `old` / `new` | Set with | | - | - | - | | `memo` | `old` is `null`. `new` is `ADDED`, `CHANGED`, or `REMOVED`. The memo text is never recorded. | `memo` | | `receipt` | `old` is `null`. `new` is `ADDED`, `CHANGED`, or `REMOVED`. The file is never recorded. | `attachmentId` | | `expense_category` | The previous and new category names, or `null` | `transactionCategory` | | `erp_category` | The previous and new QuickBooks category names, or `null` | `erpTransactionMetadata.categoryReferenceItemId` or `categoryName` | Each event also carries `transaction_log_id`, `transaction_record_id` (the transaction's `recordId`, when it's known), and, on category events, `old_id` and `new_id` for the side that has a category. These events are recorded for [`updateTransactionMetadata`](/features/add-expense-details), the `UPDATE_TRANSACTION_METADATA` [bulk operation](/submit-a-bulk-operation) (memo and category), `bulkUpdateErpTransactionMetadata` (QuickBooks category), edits in the Fluz web app, and receipt uploads, receipt removals, and QuickBooks category changes in the Fluz mobile app. A category counts as changed only when its ID changes. A memo, category, or receipt passed to `purchaseGiftCard` is part of the purchase. It doesn't record a separate expense details event. *** ## Sharing and access A gift card's ownership, access, and invite events follow the shared rules in [Ownership and access](/features/activity-history#ownership-and-access). ## Approvals | Request | `approval_type` | `approval_code` | Resulting event | | - | - | - | - | | [Purchase a gift card](/features/request-gift-card-purchase-approval) | `GIFT_CARD` | `400007` | `OBJECT_CREATED` on the new gift card | | Access to the gift card | `ACCOUNT_ACCESS` | `100004`, `100005` | `ACCESS_GRANTED`, or `ACCESS_LEVEL_CHANGED` if the requester already had lower access | An access request that nobody decides in time records `APPROVAL_EXPIRED` on the gift card. See [Approvals](/features/activity-history#approvals) for how the trail is linked. *** ## Systems you can see | `actor.system` | What it does | | - | - | | `scheduled-task-handler` | Buys scheduled gift cards. | | `approvals-service` | Records expired requests and decisions made through an approval link that names no approver. | | `erp-service` | Applies a QuickBooks category automatically. | | `transactional-graph-service` | Records an expense details change made through the API when the caller can't be resolved to a user. | *** ## What isn't recorded * Revealing the gift card code, with `revealGiftCardByGiftCardId` or in the Fluz app. * Redeeming or spending the card at the merchant. * A memo or category set at purchase time. * Archiving or reactivating a gift card in Fluz Enterprise. ## Next steps > **[Activity history overview](/features/activity-history)** > > The query, response fields, actors, approvals, and access levels. > **[Event catalog](/features/activity-history-events)** > > Every event on every object, in one place. > **[View gift cards](/view-gift-card)** > > List the gift cards on an account and get their IDs. # Request account transfer approval Source: https://docs.fluz.app/features/request-account-transfer-approval `requestAccountTransfer` Submit a manager approval request for an account-to-account transfer. When approved, Fluz executes the transfer using the same semantics as `createTransfer`. Use this when a user or application needs manager sign-off before moving funds between Fluz accounts. For transfers between spend accounts on the same account, use `requestInternalTransfer` instead. ## Scopes | Action | Scope | | - | - | | Create request | `REQUEST_ACCOUNT_TRANSFER` | | List requests | `LIST_APPROVALS` | | Approve or decline | `MANAGE_APPROVALS` | ## Webhook Identifiers | Field | Value | | - | - | | `approvalType` | `PAYOUT` | | `approvalCode` | `500402` | Webhook events: `APPROVAL_CREATE`, `APPROVAL_APPROVE`, `APPROVAL_DECLINE`, and `APPROVAL_HANDLER_ERROR` on execution failure. > Internal transfers and account transfers share the same `approvalType` and `approvalCode`. Use the original request mutation or inspect the approval payload in your integration to distinguish them. ## Create a Request Input mirrors [Transfer to Another Fluz Account](/features/transfer-to-another-fluz-wallet), including `useRewardsBalance`. The sender is determined from authentication; destination and funding sources follow the same rules as `createTransfer`. ### RequestAccountTransferInput | Field | Type | Required | Description | | - | - | - | - | | `idempotencyKey` | UUID | Yes | Unique key to prevent duplicate transfers on approval | | `amount` | Float | Yes | Amount to transfer. Must be greater than zero | | `destination` | TransferDestination | Depends | Recipient account. Required for Basic Auth; optional for Bearer (defaults to your application) | | `bankCardId` | UUID | No | Fund from a linked bank card. Bearer token only | | `bankAccountId` | UUID | No | Fund via ACH. Bearer token only | | `paypalVaultId` | UUID | No | Fund from PayPal. Bearer token only | | `memo` | String | No | Transaction memo | | `transactionCategory` | String | No | Category label; created automatically on first use | | `attachmentId` | String | No | ID of a previously uploaded transaction attachment | | `useRewardsBalance` | Boolean | No | When `true`, fund from the sender's rewards balance in addition to the resolved spend account. Defaults to `false`. See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). | ### TransferDestination | Field | Type | Description | | - | - | - | | `accountId` | UUID | Recipient Fluz account ID | | `externalReferenceId` | String | Recipient external reference ID from your system | | `userCashBalanceId` | UUID | Optional target spend account on the recipient account | Provide either `accountId` or `externalReferenceId`, not both. ### Authentication Notes | Method | Sender | Notes | | - | - | - | | Bearer token | Authenticated user | Funding sources allowed; destination optional (defaults to your application) | | Basic Auth | Your application | Destination required; funding sources not allowed | ### Sample Mutation — User to Application ```graphql theme={null} mutation { requestAccountTransfer( input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440000" amount: 100.00 } ) { success messageId error { code message } } } ``` ### Sample Mutation — Application to User ```graphql theme={null} mutation { requestAccountTransfer( input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440001" amount: 50.00 destination: { accountId: "c3d4e5f6-a7b8-9012-cdef-345678901234" } } ) { success messageId error { code message } } } ``` ### Sample Mutation — Fund with rewards ```graphql theme={null} mutation { requestAccountTransfer( input: { idempotencyKey: "550e8400-e29b-41d4-a716-446655440011" amount: 25.00 useRewardsBalance: true destination: { accountId: "c3d4e5f6-a7b8-9012-cdef-345678901234" } } ) { success messageId error { code message } } } ``` See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). ### Sample Response ```json theme={null} { "data": { "requestAccountTransfer": { "success": true, "messageId": "1234567890" } } } ``` The `idempotencyKey` is preserved and used when the transfer executes on approval. ## Approve a Request Call `approveApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` On approval, Fluz executes the account transfer. ## Decline a Request Call `declineApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` No transfer is executed when a request is declined. # Request gift card purchase approval Source: https://docs.fluz.app/features/request-gift-card-purchase-approval `requestGiftCardPurchase` Submit a manager approval request to purchase a gift card. When approved, Fluz completes the purchase using the submitted parameters. ## Scopes | Action | Scope | | - | - | | Create request | `REQUEST_GIFT_CARD` | | List requests | `LIST_APPROVALS` | | Approve or decline | `MANAGE_APPROVALS` | ## Webhook Identifiers | Field | Value | | - | - | | `approvalType` | `GIFT_CARD` | | `approvalCode` | `400007` | Webhook events: `APPROVAL_CREATE`, `APPROVAL_APPROVE`, `APPROVAL_DECLINE`, and `APPROVAL_HANDLER_ERROR` on execution failure. ## Create a Request Input fields mirror the direct `purchaseGiftCard` mutation. See [Purchase Gift Card](/purchase-gift-card) for offer and funding source details. ### RequestGiftCardPurchaseInput | Field | Type | Required | Description | | - | - | - | - | | `offerId` | UUID | Yes | Gift card offer ID | | `purchaseAmount` | Float | Yes | Purchase amount | | `channel` | SourcePlatformChannel | Yes | Request origin channel (for example, `API`) | | `fluzpayAmount` | Float | No | Amount to fund from Fluz balance | | `bankCardId` | UUID | No | Bank card funding source | | `bankAccountId` | UUID | No | Bank account funding source | | `paypalVaultId` | UUID | No | PayPal funding source | | `userCashBalanceId` | UUID | No | Spend account to fund the purchase | | `seatId` | UUID | No | Seat associated with the purchase | | `exclusiveRateId` | UUID | No | Exclusive rate ID | | `merchantSlug` | String | No | Merchant slug | | `gcWarehouseMerchantId` | UUID | No | Gift card warehouse merchant ID | | `gcWarehouseUserRewardRate` | Float | No | Warehouse user reward rate | | `gcWarehouseDiscountRate` | Float | No | Warehouse discount rate | | `memo` | String | No | Transaction memo | | `categoryId` | UUID | No | Transaction category ID | ### Sample Mutation ```graphql theme={null} mutation { requestGiftCardPurchase( input: { offerId: "ed669305-5e43-40a0-9a25-7a15ed174628" purchaseAmount: 25.00 channel: API fluzpayAmount: 25.00 userCashBalanceId: "b1155504-ad30-4b2f-873d-b8795277b128" } ) { success messageId error { code message } } } ``` ### Sample Response ```json theme={null} { "data": { "requestGiftCardPurchase": { "success": true, "messageId": "1234567890" } } } ``` ## Approve a Request Call `approveApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` On approval, Fluz purchases the gift card. ## Decline a Request Call `declineApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` No gift card is purchased when a request is declined. # Request internal transfer approval Source: https://docs.fluz.app/features/request-internal-transfer-approval `requestInternalTransfer` Submit a manager approval request to move funds between two spend accounts on the same Fluz account. When approved, Fluz executes the internal transfer. This is different from account-to-account transfers. Use `requestAccountTransfer` when moving funds between Fluz accounts. See [Request Account Transfer Approval](/features/request-account-transfer-approval). ## Scopes | Action | Scope | | - | - | | Create request | `REQUEST_INTERNAL_TRANSFER` | | List requests | `LIST_APPROVALS` | | Approve or decline | `MANAGE_APPROVALS` | ## Webhook Identifiers | Field | Value | | - | - | | `approvalType` | `PAYOUT` | | `approvalCode` | `500402` | Webhook events: `APPROVAL_CREATE`, `APPROVAL_APPROVE`, `APPROVAL_DECLINE`, and `APPROVAL_HANDLER_ERROR` on execution failure. > Internal transfers and account transfers share the same `approvalType` and `approvalCode`. Use the original request mutation or inspect the approval payload in your integration to distinguish them. ## Create a Request ### RequestInternalTransferInput | Field | Type | Required | Description | | - | - | - | - | | `amount` | Float | Yes | Amount to transfer. Must be greater than zero | | `sourceUserCashBalanceId` | UUID | Yes | Spend account to withdraw from | | `destinationUserCashBalanceId` | UUID | Yes | Spend account to deposit to | Source and destination must be different spend accounts. ### Sample Mutation ```graphql theme={null} mutation { requestInternalTransfer( input: { amount: 50.00 sourceUserCashBalanceId: "6d1b4b19-deef-42f5-80d7-ec34804ce090" destinationUserCashBalanceId: "bb584e49-d030-4a6e-a5a9-1c34368cbaed" } ) { success messageId error { code message } } } ``` ### Sample Response ```json theme={null} { "data": { "requestInternalTransfer": { "success": true, "messageId": "1234567890" } } } ``` ## Approve a Request Call `approveApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` On approval, Fluz transfers funds between the specified spend accounts. ## Decline a Request Call `declineApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` No transfer is executed when a request is declined. # Request reimbursement approval Source: https://docs.fluz.app/features/request-reimbursement-approval `requestReimbursement` Submit a manager approval request to reimburse a user by moving funds between spend accounts on the same account. When approved, Fluz processes the reimbursement. ## Scopes | Action | Scope | | - | - | | Create request | `REQUEST_REIMBURSEMENT` | | List requests | `LIST_APPROVALS` | | Approve or decline | `MANAGE_APPROVALS` | ## Webhook Identifiers | Field | Value | | - | - | | `approvalType` | `PAYOUT` | | `approvalCode` | `500401` | Webhook events: `APPROVAL_CREATE`, `APPROVAL_APPROVE`, `APPROVAL_DECLINE`, and `APPROVAL_HANDLER_ERROR` on execution failure. ## Create a Request ### RequestReimbursementInput | Field | Type | Required | Description | | - | - | - | - | | `amount` | Float | Yes | Reimbursement amount. Must be greater than zero | | `sourceUserCashBalanceId` | UUID | Yes | Spend account to withdraw from (typically a company or shared balance) | | `destinationUserCashBalanceId` | UUID | Yes | Spend account to credit (typically the requester's balance) | | `memo` | String | No | Transaction memo | | `categoryId` | UUID | No | Transaction category ID | Source and destination must be different spend accounts. ### Sample Mutation ```graphql theme={null} mutation { requestReimbursement( input: { amount: 75.00 sourceUserCashBalanceId: "6d1b4b19-deef-42f5-80d7-ec34804ce090" destinationUserCashBalanceId: "bb584e49-d030-4a6e-a5a9-1c34368cbaed" memo: "Client dinner reimbursement" } ) { success messageId error { code message } } } ``` ### Sample Response ```json theme={null} { "data": { "requestReimbursement": { "success": true, "messageId": "1234567890" } } } ``` ## Approve a Request Call `approveApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` On approval, Fluz processes the reimbursement transfer. ## Decline a Request Call `declineApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` No reimbursement is processed when a request is declined. # Request virtual card approval Source: https://docs.fluz.app/features/request-virtual-card-approval `requestVirtualCard` Submit a manager approval request to create a virtual card. When approved, Fluz creates the card using the submitted parameters. ## Scopes | Action | Scope | | - | - | | Create request | `REQUEST_VIRTUAL_CARD` | | List requests | `LIST_APPROVALS` | | Approve or decline | `MANAGE_APPROVALS` | ## Webhook Identifiers | Field | Value | | - | - | | `approvalType` | `VIRTUAL_CARD_CREATION` | | `approvalCode` | `600003` | Webhook events: `APPROVAL_CREATE`, `APPROVAL_APPROVE`, `APPROVAL_DECLINE`, and `APPROVAL_HANDLER_ERROR` on execution failure. ## Create a Request ### RequestVirtualCardInput | Field | Type | Required | Description | | - | - | - | - | | `seatId` | UUID | Yes | Seat associated with the requester | | `offerId` | UUID | Yes | Virtual card offer ID. Use `getVirtualCardOffers` to list active offers | | `purchaseAmount` | Float | Yes | Purchase amount for the card | | `channel` | SourcePlatformChannel | Yes | Request origin channel (for example, `API`) | | `fluzpayAmount` | Float | Yes | Amount to fund from Fluz balance | | `spendLimitDuration` | VirtualCardSpendLimitDuration | Yes | Limit duration: `DAILY`, `WEEKLY`, `MONTHLY`, `ANNUAL`, or `LIFETIME` | | `virtualCardPIN` | String | No | Optional card PIN | | `brandLocked` | Boolean | No | Lock card to a specific merchant brand | | `mccId` | String | No | Merchant category restriction | | `shareWith` | \[String] | No | User IDs to share the card with | | `pinAuthToken` | String | No | PIN authorization token when required | ### Sample Mutation ```graphql theme={null} mutation { requestVirtualCard( input: { seatId: "db11ec6c-c371-4184-ab6a-3dfb2c92c4d3" offerId: "b23630f6-8d91-43df-84aa-a541e7691197" purchaseAmount: 100.00 channel: API fluzpayAmount: 100.00 spendLimitDuration: MONTHLY } ) { success messageId error { code message } } } ``` ### Sample Response ```json theme={null} { "data": { "requestVirtualCard": { "success": true, "messageId": "1234567890" } } } ``` ## Approve a Request Call `approveApprovalRequest` with the `approvalId` from `approvalRequests` or the `APPROVAL_CREATE` webhook. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` On approval, Fluz creates the virtual card with the parameters from the original request. ## Decline a Request Call `declineApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` No virtual card is created when a request is declined. # Request virtual card limit change Source: https://docs.fluz.app/features/request-virtual-card-limit-change `requestVirtualCardLimitChange` Submit a manager approval request to change a virtual card spend limit. When approved, Fluz updates the card limit. ## Scopes | Action | Scope | | - | - | | Create request | `REQUEST_VIRTUAL_CARD_LIMIT_CHANGE` | | List requests | `LIST_APPROVALS` | | Approve or decline | `MANAGE_APPROVALS` | ## Webhook Identifiers | Field | Value | | - | - | | `approvalType` | `VIRTUAL_CARD` | | `approvalCode` | `600002` | Webhook events: `APPROVAL_CREATE`, `APPROVAL_APPROVE`, `APPROVAL_DECLINE`, and `APPROVAL_HANDLER_ERROR` on execution failure. ## Create a Request ### RequestVirtualCardLimitChangeInput | Field | Type | Required | Description | | - | - | - | - | | `virtualCardId` | UUID | Yes | ID of the virtual card to update | | `spendLimit` | Float | Yes | New spend limit. Must be greater than zero | | `limitDuration` | VirtualCardSpendLimitDuration | Yes | Limit duration: `DAILY`, `WEEKLY`, `MONTHLY`, `ANNUAL`, or `LIFETIME` | ### Sample Mutation ```graphql theme={null} mutation { requestVirtualCardLimitChange( input: { virtualCardId: "07df5653-43a8-4532-9881-3ab5857bbe11" spendLimit: 500.00 limitDuration: MONTHLY } ) { success messageId error { code message } } } ``` ### Sample Response ```json theme={null} { "data": { "requestVirtualCardLimitChange": { "success": true, "messageId": "1234567890" } } } ``` ## Approve a Request Call `approveApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { approveApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` On approval, Fluz updates the virtual card spend limit. ## Decline a Request Call `declineApprovalRequest` with the `approvalId`. Requires `MANAGE_APPROVALS`. ```graphql theme={null} mutation { declineApprovalRequest(approvalId: "07df5653-43a8-4532-9881-3ab5857bbe12") { success approvalId action } } ``` No limit change is applied when a request is declined. # Spend account activity history Source: https://docs.fluz.app/features/wallet-spend-account-activity-history Every event Fluz records on a spend account — creation, renames, default changes, images, closure, ownership, and access — and how each one appears in objectChangeHistory. A spend account's activity history shows how the account was created, every rename, default switch, and closure, and who was given access to it. Read it with `objectChangeHistory` and `objectType: SPEND_ACCOUNTS`. > [!NOTE] > > **Prerequisites:** the `VIEW_OBJECT_CHANGE_HISTORY` scope on your application, with either a user access token or your API key, and the `userCashBalanceId` of a spend account on the account. For an OAuth grant restricted to specific spend accounts, only those spend accounts are available. See [Activity history overview](/features/activity-history) for the full query, response fields, and access rules. ```graphql theme={null} query { objectChangeHistory( objectType: SPEND_ACCOUNTS objectId: "b7c6d5e4-f3a2-4b1c-9d8e-7f6a5b4c3d2e" ) { changeType change createdAt subjectUserId actor { userId system applicationId applicationName } } } ``` ## At a glance ```mermaid theme={null} flowchart LR SA["Spend account
SPEND_ACCOUNTS"] SA --> A["Created
API, registration, first deposit, Fluz app, support"] SA --> B["Changes
nickname, image, default, primary or backup"] SA --> C["Closed"] SA --> D["Ownership and access
owner, authorized user access"] SA --> E["Approvals
access requests"] ``` Each box is a group of events below. For one table of every spend account event, see the [Event catalog](/features/activity-history-events#spend-account-events). *** ## Creation The spend account records `OBJECT_CREATED`, with `change: {}`, when it's opened. Spend accounts are opened directly, and also automatically when another action needs one. | How the spend account was created | `actor` | `subjectUserId` | | - | - | - | | [`createUserCashBalance`](/features/create-spend-accounts) | The calling user and the connected application | — | | [`registerUser`](/user-registration) creates the new user's default spend account | The calling user and the connected application | The new user | | [`registerBusiness`](/business-registration) creates a spend account on the business account | The calling user and the connected application | — | | The first deposit to an account that has no spend account | The depositing user | — | | The first scheduled deposit to an account that has no spend account | `system: "scheduled-task-handler"` | The user the deposit is for | | A deposit by Fluz support to an account that has no spend account | `system: "cms"` | The user the deposit is for | | A share recipient is registered to receive a shared card | The sender and the connected application | The recipient | | An embedded widget sets up the account | The user, or `system: "fluz-web-unified"` | — | | The Fluz web or mobile app, including registration | The signed-in user | — | | Fluz support | `system: "cms"` | The spend account's user | | Business onboarding, or converting a business account | `system: "business-service"` | The account owner, on conversion | *** ## Changes A change records one `FIELD_CHANGED` event per field, with `change: { "field", "old", "new" }`. | `field` | `old` / `new` | Recorded when | | - | - | - | | `nickname` | The previous and new nickname | [`updateUserCashBalance`](/features/edit-spend-accounts), or the Fluz app. | | `image` | The previous and new image URL, or `null` | The image is changed in the Fluz app. | | `is_default` | `true` or `false` | A spend account becomes, or stops being, the default, in the Fluz app or by Fluz support. | | `primary_funding_source`, `backup_funding_source` | `true` or `false` | The spend account is chosen as, or replaced as, the primary or backup payment method in the Fluz web app. | | `status` | The previous status, for example `ACTIVE` → `CLOSED` | [`closeUserCashBalance`](/features/close-spend-accounts), closing the account in the Fluz app, or removing a person and closing their spend accounts in the Fluz app. | ### Default changes Switching the default touches two spend accounts, and each records its own event: * The previous default records `is_default`: `true` → `false`. * The new default records `is_default`: `false` → `true`. A spend account that's created as the default may not record this event, because it was created that way. Closing the default spend account in the Fluz web app also records `is_default`: `true` → `false` on the closed account, and `false` → `true` on the spend account that replaces it. *** ## Ownership and access A spend account's ownership, access, and invite events follow the shared rules in [Ownership and access](/features/activity-history#ownership-and-access). * **Owner.** Spend accounts created in the Fluz app record `OWNER_ASSIGNED` for the owner picked, or for the creator if no one was picked. It's also recorded when you call [`assignObjectOwner`](/api-reference/mutations/assign-object-owner) with `objectType: SPEND_ACCOUNTS`. See [Create a wallet for an authorized user](/create-wallet-for-authorized-user). [`transferObjectOwner`](/api-reference/mutations/transfer-object-owner) records `OWNER_TRANSFERRED`. * **Access.** Changing the spend accounts an authorized user can use, in the Fluz app, records `ACCESS_GRANTED`, `ACCESS_LEVEL_CHANGED`, or `ACCESS_REVOKED` on each spend account whose access changed. `subjectUserId` is the authorized user. * **Access requests.** A request for access to the spend account records `APPROVAL_REQUESTED`, then `APPROVAL_APPROVED`, `APPROVAL_DECLINED`, or `APPROVAL_EXPIRED`. The `approval_type` is `ACCOUNT_ACCESS` and the `approval_code` is `100004` or `100005`. See [Approvals](/features/activity-history#approvals). *** ## Systems you can see | `actor.system` | What it does | | - | - | | `transactional-graph-service` | Creates a business spend account through the API when the caller can't be resolved to a user. | | `business-service` | Creates spend accounts during business onboarding and account conversion. | | `fluz-web-unified` | Creates a spend account during embedded widget setup when no user is signed in. | | `scheduled-task-handler` | Creates a spend account for a scheduled deposit to an account that has none. | | `approvals-service` | Records expired access requests and decisions made through an approval link that names no approver. | | `cms` | Fluz support. | *** ## What isn't recorded * Balances, and the money that moves through the account: deposits, withdrawals, transfers, and spend. Use [Transaction activity](/features/transactions-details-overview) for those. * Requests for payment instructions, status letters, and other account documents. ## Next steps > **[Activity history overview](/features/activity-history)** > > The query, response fields, actors, approvals, and access levels. > **[Event catalog](/features/activity-history-events)** > > Every event on every object, in one place. > **[Spend accounts](/features/spend-accounts)** > > Create, edit, and close the spend accounts that appear in this history. # Finix Source: https://docs.fluz.app/finix Email sales to request access to our BETA program of Payment Processing API connectors. # Formatting referral links Source: https://docs.fluz.app/formatting-referral-links Referral links allow you to invite individuals into your Fluz referral network and receive royalties for those referrals. You can also utilize your referral link if you have an affiliate feed where you direct your users to buy gift cards. You will then earn a commission from Fluz from those referrals who use your link. ## Format a referral URL To format the referral URL you will need the `merchantSlug` and your referral code. To view the `merchantSlug` field, you can use the [getMerchants](/get-catalog) query. If you know the name of the merchant, include that input argument to refine the response. Add the `merchantSlug` field into the URL structure below to form the merchant URL: `https://fluz.app/store/{merchant-slug}` Next, you will need your referral code. You can find your referral code on the homepage in the Fluz web portal under **Maximize**. Click on **Invite a friend** to access your referral code. ![Invite a friend](https://storage.googleapis.com/fluz-public-docs/public-doc-assets/Invite%20a%20friend.png) Then, under **Share your referral code** select **Copy** to save your code. ![Referral code](https://storage.googleapis.com/fluz-public-docs/public-doc-assets/referral%20code.png) Once you have your referral code, you can add that information to the merchant URL from above to create your referral code URL: `https://fluz.app/store/{merchant-slug}?referred_by={Referral_Code}` ## Using getReferralUrl You can also run the `getReferralUrl` query to automatically display your referral URL link. This query takes the `MerchantInput` as the optional input field. The order of precedence is `id` -> `slug` -> `name`. ```text theme={null} { "id": "f37cff96-6ca7-4197-b4ff-32ce94433455", "slug": "xyz789", "name": "xyz789" } ``` Your referrals can then use this URL to pull the cashback rates from a specific merchant. If no `MerchantInput` is entered or the slug does not exists, it will return the default string: `https://fluz.app/referred-by/{Referral_Code}` Otherwise, the response will be: `https://fluz.app/store/{merchant-slug}?referred_by={Referral_Code}` The full catalog updates twice daily, and stock availability may change. Review [Inventory on Stocked Offers](/get-inventory) to learn how to retrieve stock information. To learn more about the merchant catalog, visit [Catalog Overview](/merchant-catalog). # Get bulk wallet balances Source: https://docs.fluz.app/get-bulk-balances Return cash balances across up to 100 connected users in one call, one result per target. Returns cash balances across up to **100 connected users** in one call, one result per target. ## Requirements * `Authorization: Basic ` (your application API key) * The **bulk API capability** on your application * The `LIST_PAYMENT` scope on each target user's grant Failures are reported per target and never fail the whole request. See [Bulk API Overview](/bulk-api) for the targeting model and error codes. ## Query ```graphql theme={null} query GetBulkBalances($targetSpec: BulkTargetSpecInput!) { getBulkBalances(targetSpec: $targetSpec) { targetCount successCount failureCount results { externalReferenceId accountId success error { code message } balances { userCashBalanceId nickname status totalCashBalance availableCashBalance lifetimeCashBalance createdAt } } } } ``` ### Variables — selected users ```json theme={null} { "targetSpec": { "mode": "SELECTED", "targets": ["your-user-001", "your-user-002"] } } ``` ### Variables — all connected users ```json theme={null} { "targetSpec": { "mode": "ALL_CONNECTED" } } ``` ## Response ```json theme={null} { "data": { "getBulkBalances": { "targetCount": 2, "successCount": 1, "failureCount": 1, "results": [ { "externalReferenceId": "your-user-001", "accountId": "8f3c…", "success": true, "error": null, "balances": [ { "userCashBalanceId": "ucb-1", "nickname": "Main account", "status": "ACTIVE", "totalCashBalance": "125.50", "availableCashBalance": "100.00", "lifetimeCashBalance": "500.00", "createdAt": "2026-01-01T00:00:00.000Z" } ] }, { "externalReferenceId": "your-user-002", "accountId": "a90d…", "success": false, "error": { "code": "INSUFFICIENT_SCOPE", "message": "The user has not granted the scopes this operation requires. Missing scopes: LIST_PAYMENT." }, "balances": null } ] } } } ``` ## Arguments | Argument | Type | Description | | - | - | - | | `targetSpec.mode` | `BulkTargetMode!` | `ALL_CONNECTED` or `SELECTED`. | | `targetSpec.targets` | `[String!]` | `externalReferenceId`s to target. Required for `SELECTED`; ignored for `ALL_CONNECTED`. Duplicates removed; result order follows this list. Max 100. | ## Response fields | Field | Description | | - | - | | `results[].success` | Whether balances were returned for this target. | | `results[].error` | Present when `success` is `false`. See error codes in the [Overview](/bulk-api). | | `results[].balances` | The user's cash balances. Restricted to permitted spend accounts when the grant is spend-account-scoped. | | `targetCount` / `successCount` / `failureCount` | Request-level summary. | > [!NOTE] > > A target whose grant is scoped to specific spend accounts returns only those balances — automatically, with no extra parameters. # Get bulk transactions Source: https://docs.fluz.app/get-bulk-transactions Fetch transactions across your connected users as a single flat, keyset-paginated list. Returns transactions across your connected users as a **single flat, keyset-paginated list**, newest first. Every row carries the `externalReferenceId` and `accountId` of the user it belongs to. There is **no per-user cap** — page with `nextCursor` to walk every transaction in the window across every target user. Defaults to the **last 90 days** when no date filter is supplied. > [!NOTE] > > **Breaking change.** This query previously returned `results`, one entry per target user, each holding up to 20 transactions. It now returns a flat `transactions` list with cursor pagination. See [Migrating from the per-target response](#migrating-from-the-per-target-response). ## Requirements * `Authorization: Basic ` (your application API key) * The **bulk API capability** on your application * The `LIST_PAYMENT` and `LIST_PURCHASES` scopes on each target user's grant Failures are reported per target and never fail the whole request. See [Bulk API Overview](/bulk-api) for the targeting model and error codes. ## Query ```graphql theme={null} query GetBulkTransactions( $targetSpec: BulkTargetSpecInput! $createdGte: DateTime $createdLte: DateTime $includeMetadata: Boolean $limit: Int $after: String $includeTotalCount: Boolean ) { getBulkTransactions( targetSpec: $targetSpec createdGte: $createdGte createdLte: $createdLte includeMetadata: $includeMetadata limit: $limit after: $after includeTotalCount: $includeTotalCount ) { transactions { externalReferenceId accountId recordId transactionType amount status createdAt memo transactionCategory } errors { externalReferenceId accountId code message } totalCount targetCount succeededTargetCount failedTargetCount nextCursor hasMore } } ``` ### Variables ```json theme={null} { "targetSpec": { "mode": "SELECTED", "targets": ["your-user-001", "your-user-002"] }, "createdGte": null, "createdLte": null, "includeMetadata": false, "limit": 100, "after": null, "includeTotalCount": true } ``` ## Response ```json theme={null} { "data": { "getBulkTransactions": { "transactions": [ { "externalReferenceId": "your-user-001", "accountId": "8f3c…", "recordId": "f85b1ce1-1766-4af5-b539-7f3fab4db33d", "transactionType": "Account Transfer - In", "amount": 25.0, "status": "SETTLED", "createdAt": "2026-07-01T00:00:00.000Z", "memo": null, "transactionCategory": null }, { "externalReferenceId": "your-user-001", "accountId": "8f3c…", "recordId": "ede29c85-6d9c-4a7e-b2be-9b6ec7b93cb5", "transactionType": "Add Money", "amount": 100.0, "status": "SETTLED", "createdAt": "2026-06-30T18:12:04.000Z", "memo": null, "transactionCategory": null } ], "errors": [ { "externalReferenceId": "your-user-002", "accountId": null, "code": "TARGET_NOT_CONNECTED", "message": "The id does not correspond to a user currently connected to your application." } ], "totalCount": 1284, "targetCount": 2, "succeededTargetCount": 1, "failedTargetCount": 1, "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA2…", "hasMore": true } } } ``` ## Arguments | Argument | Type | Description | | - | - | - | | `targetSpec` | `BulkTargetSpecInput!` | Target selection. See [Targeting](#targeting). | | `createdGte` | `DateTime` | Only include transactions created at/after this time. Defaults to 90 days before `createdLte`. Must not be after `createdLte`. | | `createdLte` | `DateTime` | Only include transactions created at/before this time. Defaults to now. | | `includeMetadata` | `Boolean` | Also return `memo` and `transactionCategory`. Slower; defaults to `false`. | | `limit` | `Int` | Maximum transactions in this page. Default and maximum are both `100`. Larger values are clamped, not rejected. | | `after` | `String` | Opaque cursor from a previous response's `nextCursor`. Omit for the first page. | | `includeTotalCount` | `Boolean` | Also return `totalCount`. Costs a second scan of the window, so it defaults to `false`. Prefer `hasMore`/`nextCursor` to drive paging. | The date window bounds the data; `limit` bounds a single page. **The window may span at most 365 days.** Transactions are stored in monthly partitions and each partition in the window is scanned per target, so a wider window costs more per page — it isn't free. For longer history, use the export. ### Targeting `targetSpec.mode` is either `SELECTED` (name the users in `targets`, using the `externalReferenceId` you connected them with) or `ALL_CONNECTED` (every user with an active grant on your application). Duplicate `targets` are removed. This query accepts up to **1,000 target users**. For more than that, use the asynchronous export. ## Response fields | Field | Type | Description | | - | - | - | | `transactions` | `[BulkTransaction!]!` | This page of transactions across all target users, newest first, each attributed to its user. | | `errors` | `[BulkTargetFailure!]!` | Targets that were skipped or failed. **First page only** (empty on subsequent pages), and capped at a **sample of 100** so a large sweep can't return a huge payload. Use `failedTargetCount` for the true total. | | `totalCount` | `Int` | Total transactions matching the request across **all pages** — every target, whole window. Opt-in via `includeTotalCount`, and only ever computed on the first page; `null` otherwise. It costs a second scan of the window, so ask for it only when you need to show a total. | | `targetCount` | `Int!` | Number of target users addressed by the request. | | `succeededTargetCount` | `Int!` | Number of target users this request was authorized to read — resolved, and holding the required scopes. Independent of this page's contents: an authorized user with no transactions in the window still counts. | | `failedTargetCount` | `Int!` | Number of target users that could not be read. Returned on **every** page (unlike `errors`), and the true total even when `errors` is truncated to its 100-item sample. | | `nextCursor` | `String` | Opaque cursor for the next page; `null` when this is the last page. Keyset-based, so it's stable across concurrent writes. | | `hasMore` | `Boolean!` | Whether more transactions remain beyond this page. | ### `BulkTransaction` | Field | Type | Description | | - | - | - | | `externalReferenceId` | `String` | Your reference id for the user this transaction belongs to. | | `accountId` | `UUID!` | The Fluz account id of the user this transaction belongs to. | | `recordId` | `UUID` | Unique identifier for the transaction record. | | `transactionType` | `String` | Display-style label, e.g. `"Account Transfer - In"`, `"Account Transfer - Out"`, `"Spend Account Transfer - In"`, `"Add Money"`. Not an enum — see the note below. | | `amount` | `Float!` | The transaction amount. | | `status` | `TransactionStatus` | `PENDING` or `SETTLED`. | | `createdAt` | `DateTime` | When the transaction was created. | | `memo` | `String` | Populated only when `includeMetadata: true`. | | `transactionCategory` | `String` | Populated only when `includeMetadata: true`. | Four behaviors to be aware of: * **`transactionType` is a human-readable label, not a stable enum** — values look like `"Account Transfer - In"` or `"Add Money"`, not `TRANSFER` or `DEPOSIT`. The set is open and the wording may change. Don't switch on it in code; use `recordId` and the amount/sign for logic, and treat this field as display text. * **Only `PENDING` and `SETTLED` transactions are returned** — declined and other non-baseline records are excluded, matching the single-user transactions surface. * **Ordering is newest first** across the whole flat stream (`createdAt` descending, ties broken by `recordId`, then by user). * **A transaction on a shared account appears once per grant that can see it** — two attributed rows with the same `recordId` and different `externalReferenceId`. `BulkTransaction` is a purpose-built, narrow type. It is **not** the full `Transaction` type returned by the single-user [transactions query](/features/get-all-transactions) — the fields above are the complete set. For richer per-transaction data, use the asynchronous export. ### `BulkTargetFailure` | Field | Type | Description | | - | - | - | | `externalReferenceId` | `String` | Your reference id for the target, when available. | | `accountId` | `UUID` | The target's account id, when the target was resolved. | | `code` | `String!` | Machine-readable failure reason (see below). | | `message` | `String!` | Human-readable failure detail. | | `code` | Meaning | | - | - | | `TARGET_NOT_CONNECTED` | The id is not a user currently connected to your application. | | `INVALID_TARGET_IDENTIFIER` | The id is not a valid identifier. | | `INSUFFICIENT_SCOPE` | The user's grant is missing `LIST_PAYMENT` and/or `LIST_PURCHASES`. | | `ACCOUNT_NOT_PERMITTED` | The grant does not permit this operation on the requested spend account. | A failed target never fails the request. **Select both identifiers.** `externalReferenceId` is `null` for any grant you connected without one, so on its own it may not tell you which user failed. `accountId` is populated whenever the target resolved — which is the case for `INSUFFICIENT_SCOPE` and `ACCOUNT_NOT_PERMITTED` — and is `null` only for `TARGET_NOT_CONNECTED` and `INVALID_TARGET_IDENTIFIER`, where nothing resolved. Querying `errors { externalReferenceId accountId code message }` means every entry is identifiable by at least one of the two. ### Request-level errors Everything above is per-target. These reject the whole request, returning `data.getBulkTransactions: null` plus a GraphQL error. **Branch on `extensions.code`, never on the message text** — wording can change, codes won't. | Condition | `extensions.code` | `extensions.errorName` | `statusCode` | | - | - | - | - | | Window wider than 365 days | `APPLICATIONS-0009` | `InvalidDateWindow` | 422 | | `createdGte` after `createdLte` | `APPLICATIONS-0009` | `InvalidDateWindow` | 422 | | Malformed or tampered `after` cursor | `APPLICATIONS-0010` | `InvalidPaginationCursor` | 422 | | No eligible targets — none connected, or none with the required scopes | `APPLICATIONS-0011` | `InvalidBulkRequest` | 422 | | Malformed request payload (bad `targetSpec`, empty `targets`) | `APPLICATIONS-0011` | `InvalidBulkRequest` | 422 | | More than 1,000 target users | `APPLICATIONS-0008` | `TargetLimitExceeded` | 422 | | Bulk API not enabled for your application | `APPLICATIONS-0003` | `BulkApiAccessDenied` | 403 | | Missing credentials, or a non-Basic scheme | `AUTH-0002` | `InvalidCredentials` | 401 | The two date failures share a code — both mean "the range you asked for isn't usable" — and the `message` distinguishes them: ```json theme={null} { "errors": [ { "message": "The requested window exceeds the 365-day maximum. Narrow the range, or use the asynchronous transactions export for a wider pull.", "path": ["getBulkTransactions"], "extensions": { "errorName": "InvalidDateWindow", "code": "APPLICATIONS-0009", "statusCode": 422, "userFriendly": true } } ], "data": { "getBulkTransactions": null } } ``` A cursor altered in any way — truncated, re-encoded, hand-edited — fails as `APPLICATIONS-0010` before it reaches the database. Treat it as "start again from page one", not as retryable. ## Paging Read the first page, then follow `nextCursor` until `hasMore` is `false`. Keep every other argument identical across pages — the cursor encodes a position in that specific query's ordering. ```graphql theme={null} # Page 1: omit `after`. Read errors here — they are first-page only. Ask for totalCount only # if you actually need it; it costs an extra scan. { getBulkTransactions(targetSpec: { mode: ALL_CONNECTED }, limit: 100, includeTotalCount: true) { transactions { recordId externalReferenceId amount createdAt } errors { externalReferenceId accountId code message } totalCount failedTargetCount nextCursor hasMore } } # Page 2+: pass the previous nextCursor. { getBulkTransactions( targetSpec: { mode: ALL_CONNECTED } limit: 100 after: "eyJjcmVhdGVkQXQiOiIyMDI2LTA2…" ) { transactions { recordId externalReferenceId amount createdAt } nextCursor hasMore } } ``` Because paging is keyset-based rather than offset-based, transactions written while you page won't shift rows across page boundaries or produce duplicates. ## Migrating from the per-target response | Before | Now | | - | - | | `results[]`, one entry per target user | `transactions[]`, one entry per transaction. Group client-side by `externalReferenceId` to rebuild the old shape. | | `results[].transactions` (full `Transaction`) | Top-level `transactions` of the narrower `BulkTransaction` type. | | `results[].error` | `errors[]` — a flat list of failed targets, first page only, capped at a 100-item sample. | | `results[].totalCount` (per target) | `totalCount` (request-wide, opt-in via `includeTotalCount`, first page only). Per-target totals are no longer returned. | | `results[].hasNextPage` (per target) | `hasMore` + `nextCursor` (request-wide keyset paging). | | `successCount` / `failureCount` | `succeededTargetCount` / `failedTargetCount`. | | 20 transactions per target, hard cap | No per-user cap. `limit` (default and max 100) bounds the page. | | 90-day window was a hard cap | 90 days is now the default; the hard cap is 365 days. | | 100 target users, hard cap | 1,000 target users. | ## When to use the export instead `getBulkTransactions` can walk complete history by paging, so the export is for pulls you'd rather not page through — more than 1,000 users, windows longer than a year, scheduled batch jobs, or when you need richer per-transaction fields than `BulkTransaction` carries. Submit `GET_TRANSACTIONS_EXPORT` via `submitBulkOperation`, poll `getBulkOperationJob`, and download the NDJSON from the short-lived `resultUrl`. See [Bulk Operations](/bulk-api). # Business KYB status Source: https://docs.fluz.app/get-business Read a business account's KYB status and each owner's identity-verification progress with the getBusiness query. ## Overview The **getBusiness** query returns the current KYB status for a business account, together with a snapshot of the ownership roster and where each owner stands in identity verification. It is how you answer two different questions after [registering a business](/business-registration): * **Is the business approved yet?** Read `kybStatus`. * **What is holding it up?** Read `owners` — an owner still on `PENDING_CIP` or `PENDING_INVITE` is usually the answer. > [!NOTE] > > You can also receive status changes by subscribing to the `KYB_STATUS_UPDATE` webhook, which avoids scheduled reads. The webhook carries the business status only, so call this query when you need the owner roster. See [Tracking an application](/kyb-overview#tracking-an-application). ## Required scopes | Property | Value | | - | - | | Endpoint | GraphQL API | | Authentication | OAuth Bearer Token, account type `BUSINESS` | | Required Scopes | `REGISTER_BUSINESS` | The query takes no arguments — it always resolves the business tied to the calling token's `accountId`. Use a token minted for the business account that `registerBusiness` returned; the consumer token you registered with will not work. > [!NOTE] > > When Fluz creates the business OAuth grant during registration, `REGISTER_BUSINESS` is force-included in that grant's scopes, so a token minted for the new business account can always call this query. ## Basic query structure ```graphql theme={null} query GetBusiness { getBusiness { accountId kybStatus owners { id name verificationType status } } } ``` ## Response details | Field | Type | Description | | - | - | - | | `accountId` | `UUID` | The business account this response describes | | `kybStatus` | `ExternalKybStatus` | `PENDING`, `APPROVED`, or `DECLINED` | | `owners` | `[BusinessOwnerStatus]` | Current roster snapshot, including owners added or updated after registration | ### BusinessOwnerStatus | Field | Type | Description | | - | - | - | | `id` | `UUID` | The owner's identity ID, or `null` for invited owners who have not accepted yet. See [Which IDs can be used](#which-ids-can-be-used) | | `name` | `String` | Display name derived from the linked identity record | | `verificationType` | `BusinessOwnerVerificationType` | What identity verification is *expected* for this owner | | `status` | `ExternalBusinessOwnerStatus` | The owner's current verification progress | ### ExternalKybStatus (enum) | Value | What it means | What your app should do | | - | - | - | | `PENDING` | The case is under review, awaiting screening results, or waiting on an owner to verify | Keep tracking. Do not fund the account or issue cards | | `APPROVED` | KYB cleared | Provision spend accounts and issue cards | | `DECLINED` | KYB did not clear | Surface a neutral message and direct the user to support. Do not auto-retry | `registerBusiness` returns `SUBMITTED`, which is not a value of this enum — that same moment reads as `PENDING` here. > [!WARNING] > > `PENDING` collapses several internal states into one value. That matters in one place: [requestOwnerDocumentVerificationLink](/owner-verification-link) only works while the case is in one specific internal state, and `kybStatus` cannot tell you which one you are in. ### BusinessOwnerVerificationType (enum) | Value | When you see it | | - | - | | `SSN` | The owner verifies via SSN (CIP) — they were submitted with `isUsPerson: true` | | `DOCUMENTS` | The owner verifies by uploading identity documents — they were submitted with `isUsPerson: false` | | `NOT_REQUIRED` | No additional verification is expected: a provided owner who owns under 25% and is not the control person | ### ExternalBusinessOwnerStatus (enum) | Value | What it means | What your app should do | | - | - | - | | `PENDING_INVITE` | The owner has an outstanding invite and has not accepted yet | Nothing. Fluz emails them; there is no API to resend or accept | | `PENDING_CIP` | The owner still needs the expected identity verification | For `DOCUMENTS` owners, [send them a verification link](/owner-verification-link) | | `READY` | The owner's verification requirement is satisfied | Nothing | | `FAILED` | Present in the schema for forward compatibility; not currently returned | — | ## cURL Example ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "query GetBusiness { getBusiness { accountId kybStatus owners { id name verificationType status } } }" }' ``` ## Example Response ```json theme={null} { "data": { "getBusiness": { "accountId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "kybStatus": "PENDING", "owners": [ { "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c", "name": "John Doe", "verificationType": "SSN", "status": "READY" }, { "id": "d4e5f6a7-b8c9-0123-def0-234567890123", "name": "Jane Smith", "verificationType": "DOCUMENTS", "status": "PENDING_CIP" }, { "id": null, "name": "Carol Investor", "verificationType": "SSN", "status": "PENDING_INVITE" } ] } } } ``` Jane needs a [verification link](/owner-verification-link). Carol was invited and will be emailed by Fluz. The business cannot be approved until both reach `READY`. ## Error Codes This query returns all failures in the top-level `errors` array — there is no `success: false` payload. | Code | Name | Description | How to resolve | | - | - | - | - | | `AUTH-0002` | InvalidCredentials | Basic auth was used; the token's account type is not `BUSINESS`; the token is missing `userId` or `accountId`; or no business exists for that `accountId` | Use a token minted for the business `accountId` that `registerBusiness` returned | | `AUTH-0031` | InvalidScope | The token is missing the `REGISTER_BUSINESS` scope | Add the scope and re-mint the token | | `G-0001` | InternalError | Internal error | Retry once. If it persists, contact support with the `accountId` | ## Best practices * **Prefer the webhook, read for reconciliation.** Subscribe to `KYB_STATUS_UPDATE` for latency, and read this query when the user returns to your onboarding screen or on a low-frequency background schedule — hourly, not per page load. * **Wait for both signals before going live.** `kybStatus: APPROVED` and every owner at `READY`. * **Do not treat a link request as progress.** An owner's `status` changes when they actually complete verification, not when you generate their link. ## Related pages > **[KYB overview](/kyb-overview)** > > The status lifecycle and how to track a case to a decision. > **[Owner verification link](/owner-verification-link)** > > Generate a link for owners reported as `DOCUMENTS` and `PENDING_CIP`. > **[Register a business](/business-registration)** > > The mutation that creates the account this query reads. # Owner verification link Source: https://docs.fluz.app/get-owner-verification-link Generate a shareable identity-verification link for a business owner who must verify by document. ## Overview The **requestOwnerDocumentVerificationLink** mutation generates a shareable URL that a business owner can use to complete identity verification by document. The link works on its own — the owner does not need Fluz credentials. Use it for owners that [getBusiness](/business-status) reports with `verificationType: DOCUMENTS` and `status: PENDING_CIP`. Those are beneficial owners or control persons submitted with `isUsPerson: false`, which puts them on the document path rather than the SSN path. > [!NOTE] > > Owners on the SSN path (`verificationType: SSN`) do not need to use this mutation, and neither do invited owners who have not accepted yet — Fluz emails those owners directly and they verify themselves. ## Required scopes | Property | Value | | - | - | | Endpoint | GraphQL API | | Authentication | OAuth Bearer Token, account type `BUSINESS` | | Required Scopes | `REGISTER_BUSINESS` | Use a token minted for the business account that `registerBusiness` returned. ## Prerequisites All of these must hold, or the call fails: **1. A business-account token with REGISTER\_BUSINESS** Scoped to the business that owns the roster. **2. The case is in the submitted-for-screening state** The link can only be generated during one specific state after submission. Cases that are still being created, already waiting on screening results, or in manual review are rejected — even though [getBusiness](/business-status) reports all of those as `kybStatus: PENDING`. See [Timing](#timing). **3. businessOwnerId is the owner's business-user ID** Taken from `owners[].id` in [getBusiness](/business-status), and belonging to this business. A Fluz user ID is not accepted, and invited owners who have not accepted have `id: null`. **4. The owner is on the document path** `verificationType: DOCUMENTS` with `status: PENDING_CIP`. ## Basic mutation structure ```graphql theme={null} mutation RequestOwnerDocumentVerificationLink($businessOwnerId: UUID!) { requestOwnerDocumentVerificationLink(businessOwnerId: $businessOwnerId) { success id verificationLink } } ``` ## Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `businessOwnerId` | `UUID` | Yes | The owner's business-user ID, as returned in `owners[].id` from [getBusiness](/business-status). Not a Fluz user ID | ## Response details | Field | Type | Description | | - | - | - | | `success` | `Boolean` | `true` when a link was generated | | `id` | `UUID` | The `businessOwnerId` the link was generated for | | `verificationLink` | `String` | Shareable Plaid IDV URL. Send this to the owner directly | > [!NOTE] > > Failures are returned as **top-level GraphQL errors**, not as `success: false`. Check the `errors` array, not just the data payload. This is the opposite of [registerBusiness](/business-registration), which reports validation failures inside its payload. ## Timing The state requirement in prerequisite 2 is the most common reason this call fails, and it is not visible through `kybStatus`. > [!WARNING] > > **Call it early.** Shortly after `registerBusiness` returns, as soon as [getBusiness](/business-status) shows the owner as `verificationType: DOCUMENTS` with `status: PENDING_CIP`. > > If you receive `ARG-0001` with `business is not submitted for approval`, the case has moved past — or has not yet reached — that window. Do not retry in a loop. Keep reading `getBusiness`, and contact your account manager with the `accountId` if an owner stays `PENDING_CIP` with no way to send them a link. ## cURL Example ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "query": "mutation RequestOwnerDocumentVerificationLink($businessOwnerId: UUID!) { requestOwnerDocumentVerificationLink(businessOwnerId: $businessOwnerId) { success id verificationLink } }", "variables": { "businessOwnerId": "d4e5f6a7-b8c9-0123-def0-234567890123" } }' ``` ## Example Response ### Success ```json theme={null} { "data": { "requestOwnerDocumentVerificationLink": { "success": true, "id": "d4e5f6a7-b8c9-0123-def0-234567890123", "verificationLink": "https://verify.docs../..." } } } ``` ### Error ```json theme={null} { "errors": [ { "message": "Invalid arguments received - business owner not found.", "extensions": { "code": "ARG-0001", "statusCode": 422 } } ], "data": null } ``` ## Error Codes | Code | Name | Description | How to resolve | | - | - | - | - | | `AUTH-0002` | InvalidCredentials | Basic auth was used; the token's account type is not `BUSINESS`; the token is missing `userId` or `accountId`; or no business exists for that `accountId` | Use a token minted for the business `accountId` | | `AUTH-0031` | InvalidScope | The token is missing the `REGISTER_BUSINESS` scope | Add the scope and re-mint the token | | `ARG-0002` | MissingArguments | `businessOwnerId` was not supplied | Pass the owner's business-user ID | | `ARG-0001` | InvalidArguments | `businessOwnerId` does not match a business owner on this business, or the case is not in the submitted-for-approval state | Re-read [getBusiness](/business-status) for a current ID, and see [Timing](#timing) | | `G-0001` | InternalError | The verification provider returned no link, or the request to it failed | Retry once. If it persists, contact support with the `accountId` | ## Notes * The generated link is single-purpose. If the owner's link expires or is lost, call the mutation again for a fresh one rather than reusing the old one. * Generating a link does not change the owner's `status`. It moves to `READY` only once the owner actually completes verification — keep reading [getBusiness](/business-status). * There is no bulk variant. Call once per owner who needs a link. ## Related pages > **[Business KYB status](/business-status)** > > Identify which owners need a link, and confirm when they finish. > **[KYB overview](/kyb-overview)** > > Where this step sits in the end-to-end flow. > **[Register a business](/business-registration)** > > How `isUsPerson` puts an owner on the document path. # General API error codes Source: https://docs.fluz.app/get-started/general-api-error-codes Error codes that are consistent across the Fluz API, with the usual cause of the ones integrators hit most. These error codes are consistent across our API. | Error | Code | Error Message | | - | - | - | | INVALID\_ACCOUNT | AUTH-0001 | Your account does not have the right permission to login to this portal. Please Apply for an Enterprise Account. | | INVALID\_CREDENTIALS | AUTH-0002 | Incorrect auth details. | | PHONE\_NUMBER\_NOT\_FOUND | AUTH-0003 | Unable to find provided phone number or phone number it is not verified. Please select different method or contact customer support. | | INVALID\_PHONE\_NUMBER | AUTH-0004 | Invalid phone number. | | INVALID\_2FA\_TOKEN | AUTH-0005 | Access denied or expired. | | INCORRECT\_AUTH\_CODE | AUTH-0006 | Auth code provided is incorrect, please check and re-enter received code and try again. | | UNABLE\_TO\_SEND\_AUTH\_CODE | AUTH-0007 | Sorry, we were unable to send the auth code, please try again or contact customer support. | | INVALID\_USER\_ACCESS | AUTH-0008 | Invalid user access. | | INVALID\_PASSWORD | AUTH-0009 | The provided password is not valid. | | NO\_PIN\_EXISTS | AUTH-0010 | There is no passcode set on the user, please create one instead. | | PIN\_ALREADY\_SET | AUTH-0011 | There is already a passcode set on the user, please reset instead. | | INVALID\_PIN\_CODE | AUTH-0012 | The provided passcode does not match our records, please try again or reset your passcode. | | SOCIAL\_UNABLE\_TO\_LOGIN | AUTH-0013 | Unable to login with your social account. Please verify your credentials and try again or contact customer support. | | UNABLE\_TO\_CREATE\_PIN | AUTH-0014 | Unable to create new pass-code, please try again or contact customer support. | | UNABLE\_TO\_CHECK\_PIN | AUTH-0015 | Unable to check pass-code, please try again or contact customer support. | | UNABLE\_TO\_UPDATE\_PIN | AUTH-0016 | Unable to update pass-code, please try again or contact customer support. | | UNABLE\_TO\_RESET\_PIN | AUTH-0017 | Unable to reset pass-code, please try again or contact customer support. | | TRADITIONAL\_UNABLE\_TO\_LOGIN | AUTH-0018 | Unable to login with your account. Please verify your credentials and try again or contact customer support. | | INVALID\_JWT | AUTH-0019 | Invalid JWT Token! | | UNABLE\_TO\_RESEND\_2FA | AUTH-0020 | Unable to re-send 2FA! | | UNSUPPORTED\_PHONE\_NUMBER | AUTH-0021 | Provided number is not a valid mobile phone number! Please contact support for more details. | | REGISTRATION\_NOT\_ALLOWED | AUTH-0022 | Registration not allowed, please contact customer support! | | UNABLE\_TO\_CHANGE\_PASSWORD | AUTH-0023 | Unable to change password. Please try again or contact customer support. | | UNVERIFIED\_PHONE\_NUMBER | AUTH-0024 | Your phone number requires verification. Please contact support. | | UNSUCCESSFUL\_REGISTRATION | AUTH-0025 | Please try again again or contact support. | | REGISTRATION\_PHONE\_TAKEN | AUTH-0026 | The phone number you chose is already in use. | | REGISTRATION\_EMAIL\_TAKEN | AUTH-0027 | The email address you chose is already in use. | | UNABLE\_TO\_RESET\_PASSWORD | AUTH-0028 | Unable to reset password. | | SECURITY\_CODE\_EXPIRED | AUTH-0029 | Your 6-digit security code has expired. Please resend a new code. | | APPLICATION\_INACTIVE | AUTH-0030 | Your application is not active. | | INVALID\_SCOPE | AUTH-0031 | The requested scopes must be granted by the user first. | ## The ones integrators hit most The same code can come from more than one mistake. Match on the code **and** the operation that returned it. | Code | Operation | Usual cause | Fix | | :- | :- | :- | :- | | `AUTH-0002` | `getBusinessCategories`, `registerBusiness` (message: "requires account type CONSUMER") | Called with a `BUSINESS` token | Use the applicant's `CONSUMER` token, minted with their personal `accountId`. See [Register & Verify Businesses](/kyb-overview) | | `AUTH-0002` | Any other operation | The token's account type doesn't match what the operation requires | Mint a token for the right account. See [Accounts, applications & environments](/get-started/accounts-and-applications) | | `AUTH-0008` | `generateUserAccessToken` | The user was created with `registerUser` and `deferSeatAssignment: true` | Register users without that flag for the OAuth path. See [Register Customers](/user-registration) | | `AUTH-0008` | `registerBusiness` (message: "does not have permission to access the application") | Known issue for applicants created with `registerUser` | Send the user through `/authorize` once, choose **Apply for a business account**, and reuse the same `external_id`. See [KYB Testing](/KYB-testing) | | `AUTH-0022` | `registerUser` | User registration isn't enabled on your application | Contact your account manager. Don't retry | | `AUTH-0030` | Any call (message: "current status: PERSONAL") | The application was created with **Create new app**, or Fluz hasn't activated it yet | Use an **OAuth Integration** application that Fluz has moved to `ACTIVE`. See [Prepare Your Accounts](/get-started/prepare-accounts) | | `AUTH-0031` | `generateUserAccessToken`, `getAccountsByUserId`, or any call for a user | The user hasn't completed the OAuth grant, or the scope isn't on their grant | Send the user through the [grant flow](/client-facing-o-auth-grant-flow). Registering a user doesn't grant access. If the scope is missing, add it to your app's Permissions tab and have the user re-authorize | > [!TIP] > > Send an `X-Request-Id` header (any unique value, such as a UUID) on every request and log it on your side. Quote it when you contact support so we can find the exact call. ## Address errors Address fields are validated across several flows. Constraints differ by context (for example, virtual card and bank account addresses are US-only, while business legal addresses may be international). For formatting rules and worked examples, see [Address Formatting Requirements](/concepts/address-formatting-requirements). | Error | Code | Applies to | Error Message | | - | - | - | - | | INVALID\_BILLING\_ADDRESS | VC-0025 | Virtual card issuance (billing address) | The billing address could not be verified. It must be a deliverable US address (no PO boxes) with a matching city, state, and ZIP. | | InvalidBusinessLegalAddress | BS-0002 | Business registration (KYB) | Business legal address is incorrect. | | InvalidOwnerInformation | BS-0003 | Business registration (KYB) | One or more owner details are incorrect (includes the owner's US-based address). | > [!NOTE] > > **Missing or malformed address fields** > > A missing required field or an invalid value inside an address object is returned as a general argument error — `ARG-0001` on virtual card calls, or `AR-0001` / `AR-0002` on business registration — rather than one of the address codes above. ## Next steps > **[Errors reference](/api-reference/errors)** > > The error envelope shape, domain prefixes, and how to troubleshoot with request IDs. > **[Address formatting](/concepts/address-formatting-requirements)** > > Formatting rules and worked examples for avoiding the address errors above. **Want to learn more?** Contact us at [partnerships@fluz.app](mailto:partnerships@fluz.app). Speak with our experts for more info or to request a demo. # InComm Source: https://docs.fluz.app/in-comm This document provides specific details for using the Fluz Gift Card Vendor API Connector with Incomm as the vendor. # Original Incomm API vs Fluz API Adapter ### Base URL * Original Incomm API: [https://app.giftango.com](https://app.giftango.com) * Fluz API Adapter: * Staging: [https://api-adapter.staging.fluzapp.com/incomm](https://api-adapter.staging.fluzapp.com/incomm) * Production: [https://api-adapter.fluzapp.com/incomm](https://api-adapter.fluzapp.com/incomm) ### Original Incomm * Uses Bearer Authentication with the Authorization header - need to generate authorization token first. ```text theme={null} Authorization: Bearer ``` To generate AuthToken send request to `/auth/token` endpoint with body: ```text theme={null} { grant_type: 'client_credentials', client_id: INCOMM_CLIENT_ID, client_secret: INCOMM_SECRET_KEY, } ``` ### Fluz API Adapter * Uses Basic Authentication with the `Authorization` header (same as all vendor integrations). ```text theme={null} Authorization: Basic ``` ## API Endpoint Comparison | Operation | Incomm Endpoint | Fluz Endpoint | Notes | | :- | :- | :- | :- | | Get Order Details | `GET /orders/:orderId/cards` | `GET /v1/orders/:orderId/cards` | Retrieve card details for a specific order | | Get Order | `GET /orders/:orderId` | `GET /v1/orders/:orderId` | Retrieve details for a specific order | | Create Order | `POST /orders/immediate` | `POST /v1/orders/immediate` | Create a new gift card order | | Get Balance | `GET /programs/programs/:programId/programbalance` | `GET /v1/programs/programs/:programId/programbalance` | Get your account balance. Scoped to the account your key is issued for | # Request Examples ## Create Order ### Original Incomm Request: ```text theme={null} { "PurchaseOrderNumber": "cddabd6d-1f7a-483d-8126-8bc676f7bd63", "CustomerOrderId": "cddabd6d-1f7a-483d-8126-8bc676f7bd63", "Recipients": [ { "FirstName": "testFirstName", "LastName": "testLastName", "EmailAddress": "testEmail@test.com", "DeliverEmail": false, "Products": [ { "Sku": "VUSA-D-2500-00", "Quantity": 1, "Value": 25 } ] } ] } ``` ### Fluz API Adapter Request: ```text theme={null} { "PurchaseOrderNumber": "cddabd6d-1f7a-483d-8126-8bc676f7bd63", "CustomerOrderId": "cddabd6d-1f7a-483d-8126-8bc676f7bd63", "Recipients": [ { "FirstName": "testFirstName", "LastName": "testLastName", "EmailAddress": "testEmail@test.com", "DeliverEmail": false, "Products": [ { "Sku": "VUSA-D-2500-00", "Quantity": 1, "Value": 25 } ] } ] } ``` An order covers one recipient and one product. `DeliverEmail` must not be `true`, because Fluz returns the gift card credentials in the order response rather than emailing the recipient, which keeps delivery under your control. `PurchaseOrderNumber` and `CustomerOrderId` are kept for compatibility with your existing request shape; correlate an order using the identifiers Fluz returns. ## Response timing Create order is synchronous, always running to completion. The call does not return until the purchase has finished, which can take up to 150 seconds, so set your client's read timeout above that. **Order status values are Fluz's, not Incomm's.** `OrderStatus` returns `PENDING`, `IN_PROGRESS`, `COMPLETED`, `FAILED`, or `CANCELED`. See [Connector behaviour](/connector-behaviour) for status values, error responses, limits, and request constraints. # Example Usage with cURL ## Example 1: Get a Specific Order with Gift Cards ```text theme={null} # Get gift cards from order with Incomm via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/incomm/v1/orders/353a4151-2d51-48b6-81c9-90fd6815f12b/cards" \ -H "Authorization: Basic " ``` ## Example 2: Get Order Details ```text theme={null} # Get order details with Incomm via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/incomm/v1/orders/353a4151-2d51-48b6-81c9-90fd6815f12b" \ -H "Authorization: Basic " ``` ## Example 3: Get Balance ```text theme={null} # Get Balance with Incomm via Fluz API Adapter # The balance is scoped to your key's account, so any :programId value works curl -X GET "https://api-adapter.staging.fluzapp.com/incomm/v1/programs/programs/0/programbalance" \ -H "Authorization: Basic " ``` ## Example 4: Create a New Order ```text theme={null} # Create Order with Incomm via Fluz API Adapter # Note: this call blocks until the purchase completes. Allow up to 150 seconds. curl -X POST "https://api-adapter.staging.fluzapp.com/incomm/v1/orders/immediate" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "PurchaseOrderNumber": "cddabd6d-1f7a-483d-8126-8bc676f7bd63", "CustomerOrderId": "cddabd6d-1f7a-483d-8126-8bc676f7bd63", "Recipients": [ { "FirstName": "testFirstName", "LastName": "testLastName", "EmailAddress": "testEmail@test.com", "DeliverEmail": false, "Products": [ { "Sku": "VUSA-D-2500-00", "Quantity": 1, "Value": 25 } ] } ] }' ``` # Register & verify businesses Source: https://docs.fluz.app/kyb-overview How business registration and KYB verification work end to end: what you need in place, the sequence of calls, and how to track an application to a decision. ## Overview Fluz KYB (Know Your Business) lets your platform onboard a business onto Fluz rails from your own UI. Across a small set of operations you: 1. Resolve the business category and sub-category the entity trades under, 2. Submit the legal entity record — legal name, structure, tax ID, state of incorporation, legal address, and intended use of the account — together with the beneficial ownership roster, 3. Get identity verification completed for the owners who require it, and 4. Track the resulting **KYB case** to an approval or a decline. The submission itself is a single mutation, [registerBusiness](/business-registration). It returns an `accountId` immediately with a `kybStatus` of `SUBMITTED`. > [!NOTE] > > **Registration is validation, not approval.** A `success` response confirms the payload passed validation and a KYB case was opened. It does **not** mean the business is approved. Build your integration so it waits for an approved status before attempting to fund the account or issue cards. ### What a business account unlocks Once KYB is approved, the business account can be used for the commercial side of the platform: * Business spend accounts and balances * Commercial virtual cards, including bulk issuance * Authorized users and card-level spend controls * Approval workflows for cards, transfers, and reimbursements * Business-level transaction reporting and expense annotation ### When to use these endpoints Use this flow when you want to collect entity and ownership data in your own UI rather than sending users into a Fluz-hosted experience. If you would rather Fluz host the collection and document upload, talk to your account manager about the widget-based onboarding option instead. *** ## KYB flow ### Step-by-step **1. Step 0 — Satisfy the prerequisites** None of this is part of the flow, but all of it must be true before you call `registerBusiness`. Each row links to the detail below. See [prerequisites](#prerequisites) **2. Resolve the business category and sub-category** Call [getBusinessCategories](/business-categories) and let the user pick a category and one of that category's sub-categories. **3. Upload an authorized-signer document, if applicable** Required only when the applicant owns less than 25% of the business **and** is not the control person. Upload the document first, then pass the returned URL in `authorizedSignerDocumentUrl`. See [Submit business documents](/submit-business-documents). **4. Submit the registerBusiness mutation** Send the full entity record, the ownership certifications, and every owner in one call. Validation errors are returned inside the payload — see [Response details](/business-registration#response-details). **5. Get the remaining owners verified** Read [getBusiness](/business-status) to see each owner's expected verification and current progress. For owners on the document path, generate a link with [requestOwnerDocumentVerificationLink](/owner-verification-link) and send it to them. Invited owners are emailed by Fluz and verify themselves. **6. Wait for the KYB decision, then provision** The application starts under review. Surface that state to your user rather than implying they are live. Once the status moves to `APPROVED`, create spend accounts and issue cards. ### End-to-end sequence ```mermaid theme={null} sequenceDiagram autonumber participant App as Your Application participant API as Fluz GraphQL API participant Files as Fluz File Upload (REST) participant KYB as Fluz Compliance / KYB participant Owner as Business Owner Note over App,API: Step 0 — the applicant is already a verified Fluz user App->>API: query getBusinessCategories API-->>App: businessCategoryId + businessSubCategoryId values opt Applicant is an authorized signer App->>Files: Upload authorization document Files-->>App: document URL end Note over App,API: Step 1 — submit the business App->>API: mutation registerBusiness(input) alt Validation fails API-->>App: success false, with error code and message App->>App: Correct the field and resubmit else Validation passes API-->>App: accountId plus kybStatus SUBMITTED API->>KYB: Open KYB case end Note over App,Owner: Step 2 — owners verify identity opt Owner must verify by document App->>API: requestOwnerDocumentVerificationLink API-->>App: verificationLink App->>Owner: Forward the link Owner->>KYB: Completes verification end Note over KYB,App: Step 3 — asynchronous review KYB->>KYB: Entity, tax ID, address, and owner checks KYB-->>API: Decision (or request for more documentation) App->>API: KYB_STATUS_UPDATE webhook, or query getBusiness API-->>App: Updated KYB status and owner roster Note over App,API: Step 4 — go live App->>API: Create spend accounts and issue cards ``` *** ## Prerequisites | Prerequisite | If missing | | - | - | | Your application requests the [`REGISTER_BUSINESS` permission scope](#application-permission-scopes) | `AUTH-0031` | | The applicant has [authorized your application](#applicant-authorization) for the business scopes it requests | `AUTH-0008` | | The applicant is [already CIP verified](#the-applicant-must-already-be-identity-verified) | `ARG-0001` | | The applicant has [no KYB application already in flight](#one-open-application-per-user) | `BS-0007` | | You hold [a Bearer token of the account type each operation requires](#access-tokens) | `AUTH-0002` | | An authorized-signer document is uploaded, if the applicant is an authorized signer | `ARG-0001` | | The business legal address is a real, verifiable address | `BS-0002` | ### Application permission scopes Select **`REGISTER_BUSINESS`** in your application's permission scopes in the Fluz dashboard. Every KYB operation requires it, and a token can only carry scopes your application is configured to request. See [Application Scopes](/fluz-dashboard/application-scopes). Subscribing to the [`KYB_STATUS_UPDATE` webhook](#tracking-an-application) requires the same scope. ### Applicant authorization The applicant must have authorized your application through the [business-registration flow](/business-accounts-in-o-auth#the-two-flows) — the consent step where they pick **apply for a business account** and pre-approve your **Business permissions** for the business they are about to create. That pre-approval must cover every business scope your application requests. If you later add a scope, existing users have to go through the flow again before they can register a business — otherwise registration fails with `AUTH-0008`. > [!WARNING] > > `generateUserAccessToken` does not run this flow. It can mint a `CONSUMER` token that carries `REGISTER_BUSINESS` for a user who has never seen the consent screen, and `registerBusiness` rejects that token with `AUTH-0008`. Send the user through the business-registration flow once, with the same value in the authorize URL's `external_id` that you pass as `externalReferenceId` to `generateUserAccessToken`. Tokens you mint afterwards with that `externalReferenceId` can register a business. ### The applicant must already be cip-verified The Bearer token identifies the **applicant**: the user submitting the application. Exactly one owner in the roster must match the token's user by **email** (case-insensitive) or **phone number**, and that owner cannot be marked `isInvited: true`. KYB verifies the business and the *other* owners. It does not verify the applicant, so the applicant must reach a verified state beforehand. The `isUsPerson` value you send for them decides which check applies: | Applicant `isUsPerson` | Required state before registration | | - | - | | `true` | A successful SSN (CIP) verification already on file for that user | | `false` | A successful Document verification | Identity verification happens outside the KYB surface, with the `VERIFY_KYC` scope, using `verifyUserInformation`, `verifyUserPrefillInformation`, or `requestDocumentVerificationLink`. See [identity verification (KYC)](/docs/user-kyc-verification). If the person has no Fluz account at all yet, create one with [registerUser](/user-registration) first. ### One open application per user A user cannot start a new registration while an existing application is still open — that returns `BS-0007`. > [!WARNING] > > There is no idempotency key and no API to cancel an in-flight application. A rejected application leaves nothing behind and can be resubmitted, but a **successful** one blocks the user from registering again until it resolves. Validate before submitting, and contact your account manager with the `accountId` if a case appears stalled. ### Access tokens Each operation requires a Bearer token of a specific account type: | Operation | Token account type | Required scope | | - | - | - | | [getBusinessCategories](/business-categories) | `CONSUMER` | `REGISTER_BUSINESS` | | [registerBusiness](/business-registration) | `CONSUMER` | `REGISTER_BUSINESS` | | [getBusiness](/business-status) | `BUSINESS` | `REGISTER_BUSINESS` | | [requestOwnerDocumentVerificationLink](/owner-verification-link) | `BUSINESS` | `REGISTER_BUSINESS` | > [!NOTE] > > Mint Bearer tokens with `generateUserAccessToken` — see the Authentication section of the API reference. A token for `registerBusiness` works only once the applicant has completed the business-registration flow. See [Applicant authorization](#applicant-authorization). *** ## KYB status lifecycle ```mermaid theme={null} stateDiagram-v2 [*] --> SUBMITTED: registerBusiness returns accountId SUBMITTED --> PENDING: Case under review PENDING --> PENDING: Owners still verifying, or more documentation requested PENDING --> APPROVED: Entity and ownership checks cleared PENDING --> DECLINED: Checks not cleared APPROVED --> [*]: Business can transact DECLINED --> [*]: New submission required ``` | Status | What it means | What your app should do | | - | - | - | | `SUBMITTED` | Returned by `registerBusiness` only. The submission was accepted and a KYB case was opened. | Store the `accountId`. Show an "under review" state. | | `PENDING` | Reported by `getBusiness`. The case is under review, awaiting screening results, or waiting on an owner to verify. | Keep tracking. Do not attempt to fund the account or issue cards. | | `APPROVED` | KYB cleared. The business account is usable. | Provision spend accounts, issue cards, unlock your business UI. | | `DECLINED` | KYB did not clear. | Surface a neutral message and direct the user to support. Do not auto-retry the submission. | > [!NOTE] > > `SUBMITTED` is only ever returned by `registerBusiness`. [getBusiness](/business-status) reports a three-value status, and the state immediately after registration reads as `PENDING` there — the two are the same moment described with different vocabulary. *** ## Tracking an application Two ways to follow an application to its final status. Use whichever fits your infrastructure; many integrations use the webhook for latency and an occasional read for reconciliation. **Webhook** Subscribe to the **`KYB_STATUS_UPDATE`** event in the Fluz dashboard: register your callback URL and select the event. Your application needs the `REGISTER_BUSINESS` scope to subscribe. Fluz `POST`s to your endpoint when a business's KYB status changes. ```json theme={null} { "accountId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "previousStatus": "PENDING", "newStatus": "APPROVED", "externalReferenceId": "your-partner-user-id-123" } ``` | Field | Type | Description | | - | - | - | | `accountId` | `UUID` | The business account whose status changed — the same `accountId` `registerBusiness` returned | | `previousStatus` | `String` | Status before the change: `PENDING`, `APPROVED`, or `DECLINED` | | `newStatus` | `String` | Status after the change: `PENDING`, `APPROVED`, or `DECLINED` | | `externalReferenceId` | `String` | Your own reference, when you supplied one at registration. Omitted otherwise | Two things to handle: * **Treat delivery as at-least-once.** Make your handler idempotent, keyed on `accountId` plus `newStatus`. * **You may receive events where `previousStatus` equals `newStatus`.** The case moved between internal states that both surface as the same public status. Treat those as no-ops. > [!NOTE] > > The payload carries the business status only — it does not include the owner roster. Call [getBusiness](/business-status) when you need per-owner progress. **Reading status directly** Call [getBusiness](/business-status) with a business-account token. It returns the same `kybStatus` plus the current owner roster, which makes it the only way to see whether an individual owner still needs to verify. ```graphql theme={null} query GetBusiness { getBusiness { accountId kybStatus owners { id name verificationType status } } } ``` Read it when the user returns to your business onboarding screen, and on a low-frequency background schedule — hourly, not per page load. Keep going until `kybStatus` is final **and** every owner reports `READY`. > [!NOTE] > > Reviews are typically resolved within one to two business days, but can take longer when additional documentation is requested or an owner has not completed their identity verification. If a case appears stalled, contact your account manager with the `accountId` rather than resubmitting — a second submission will be blocked by `BS-0007`. *** ## Identifying businesses with `externalReferenceId` Fluz identifies a business by its `accountId`, a UUID minted at registration. `externalReferenceId` is an optional identifier **you** supply instead, so you can work with Fluz using the ID your own system already uses for that customer. Pass it once, on [registerBusiness](/business-registration): ```json theme={null} { "externalReferenceId": "your-partner-user-id-123" } ``` It is stored on the business account and gives you three things: * **Token minting without storing Fluz IDs.** Mint a business-account access token by reference instead of by `userId` and `accountId`. * **Webhook correlation.** The reference comes back on every [`KYB_STATUS_UPDATE`](#tracking-an-application) event, so you can match an event to your own record without a lookup table. It is optional. If you omit it, everything works — you just have to store the `accountId` yourself, which you should do regardless. ### Rules | Scenario | Result | | - | - | | Omitted | The business account is created without an external reference | | Provided, not already in use | The reference is stored on the new business account | | Provided, already on the applicant's own consumer account | The reference moves off the consumer account onto the new business account, so it resolves to the business from then on | | Provided, already on another business | Registration fails with `AUTH-0008`. The existing business keeps the reference | > [!WARNING] > > **Use one unique value per business.** A reference can only point at one business account, so reusing a value across two businesses fails the second registration. Derive it from your own primary key rather than from something reusable like an email address. > [!NOTE] > > Two different references are in play and they are easy to conflate. The reference **already carried by your access token** is used to locate the applicant's existing authorization. The reference **in the `registerBusiness` input** is the one written onto the new business account. They serve different purposes. *** ## Related pages > **[Register a business](/business-registration)** > > The `registerBusiness` mutation: full parameter reference, ownership rules, and error codes. > **[Business categories](/business-categories)** > > Fetch the category and sub-category IDs required by the mutation. > **[Submit business documents](/submit-business-documents)** > > Upload authorization documents and respond to KYB documentation requests. > **[Business KYB status](/business-status)** > > Read KYB status and per-owner verification progress. > **[Owner verification link](/owner-verification-link)** > > Generate a shareable identity-verification link for an owner. > **[Register customers](/user-registration)** > > Create the Fluz user who will act as the applicant. # Managing external reference IDs Source: https://docs.fluz.app/managing-external-reference-ids Use your own user identifiers to drive the Fluz API — how to assign one, where it appears across OAuth, widgets, transfers, and webhooks, and how to choose one you won't regret. An **external reference ID** is your own identifier for a Fluz user. It lets you operate the Fluz API using the IDs you already have in your system — without storing or passing Fluz's internal `userId` and `accountId` for every user. When a user authorizes your application, you attach your identifier to that grant. Fluz stores the pairing between your identifier and the user's Fluz account, scoped to your application. From that point forward you can reference the user by your own ID when generating tokens, sending transfers, and matching webhook events. The practical payoff: **you never have to build a Fluz-ID lookup table.** A webhook arrives, it carries your user ID, you route it. No join, no cache, no reconciliation job. ## One value, three field names The same value appears under a different name depending on the surface. This is the most common source of confusion on this page, so it's worth memorizing before you start. | Surface | Field name | Direction | | :- | :- | :- | | OAuth authorization URL | `external_id` (query parameter) | You → Fluz | | Widget pre-approved transaction token | `externalId` (JWT claim) | You → Fluz | | GraphQL API | `externalReferenceId` | Both | | Webhook event payloads | `externalReferenceId` | Fluz → You | | OAuth access tokens | embedded in the issued token | Fluz → You | Because it's embedded in the access token, the association survives token refreshes — you assign it once, at grant time, and it persists. *** ## Choosing an identifier > [!WARNING] > > **Never use PII.** No email addresses, phone numbers, or names. > > Two reasons, both concrete. First, they change — people switch emails and phone numbers, and your mapping breaks permanently because the value is immutable once set. Second, this value travels in **query strings** and **JWT claims**, which means it lands in browser history, referrer headers, proxy logs, and your own application logs. Don't put personal data there. | Use | Avoid | Why | | :- | :- | :- | | Your database primary key | Email address, phone number | Mutable, and it's PII in a URL | | A UUID you mint per user | Sequential integers | Enumerable; also leaks your user count | | An opaque, prefixed ID like `usr_8f3d2a91` | An order ID, session ID, or transaction ID | Maps to one event, not one person — see below | > [!NOTE] > > **The most expensive mistake is scoping it to the wrong thing.** An external reference ID identifies a *person*, permanently — not a transaction, a payout run, a session, or an order. If you pass a per-transaction identifier, the first transfer succeeds and the second one creates a second mapping to the same human, and you've forked one user into many with no way to merge them. > > If you find yourself generating a new value for each operation, you want an idempotency key, not an external reference ID. ### The rules | Property | Rule | | :- | :- | | Type | Any string. Fluz does not impose a format. | | Uniqueness | Must be unique per user **within your application**. One external reference ID maps to exactly one Fluz user. | | Scope | Scoped to your OAuth client. The same Fluz user can carry a different external reference ID in another developer's application, and no other application can see or use yours. | | Stability | Immutable in practice. Once set on a grant, it is never overwritten. | | Environment | Mappings live with the application, so staging and production mappings are entirely separate. Nothing you create in staging exists in production. | *** ## Assigning one ### Standard OAuth applications Append `external_id` to the authorization URL when you send the user to consent (see [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow)): ```text theme={null} https://fluz.app/authorize?response_type=code&client_id=&redirect_uri=&scopes=MAKE_DEPOSIT%20LIST_PAYMENT&state=&external_id=usr_8f3d2a91 ``` When the user completes the grant, Fluz records `usr_8f3d2a91` against that user's authorization of your application. Optional here, but strongly recommended. Adopting it later means backfilling grants one re-authorization at a time. ### Widget applications > [!WARNING] > > **Required.** All widget application types — deposit, payout, pay-in, virtual card, gift card catalog, bill pay, and external payout — require an external reference ID to establish a user session. Without one the request is rejected: > > ```text theme={null} > External reference ID is required for applications > ``` For widgets, the value travels as the **`externalId` claim inside the signed pre-approved transaction token**, alongside the amount and transaction type. You generate it server-side; it is not a client-side init option. See [Set Up Your Server](/developers/setting-up-your-server). ```javascript theme={null} import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; const generatedToken = jwt.sign( { amount, apiKey: process.env.FLUZ_API_KEY, transactionType, // DEPOSIT (Pay-In) or WITHDRAW (Payout) externalId: 'usr_8f3d2a91', // your user, stable across every session jti: uuidv4(), // unique per transaction — not the same thing }, process.env.FLUZ_API_SECRET, { expiresIn: '1 day' } ); ``` > [!NOTE] > > `externalId` and `jti` sit next to each other in the same token and answer different questions. `externalId` is *who* — stable for the life of the user. `jti` is *which transaction* — new every time. Reusing `jti` breaks idempotency; changing `externalId` forks your user. *** ## Using one ### Address transfer destinations When creating a wallet transfer to another Fluz account (see [Transfer to Another Fluz Wallet](/features/transfer-to-another-fluz-wallet)), identify the destination by your own ID instead of a Fluz account ID: ```graphql theme={null} mutation { createTransfer(input: { idempotencyKey: "1f7c5a2e-9b14-4c6d-8e3f-2a90d4b7c1aa" amount: 25.00 destination: { externalReferenceId: "usr_8f3d2a91" } }) { transferId } } ``` Provide either `destination.accountId` or `destination.externalReferenceId` — never both. The destination user must have authorized your application, or the transfer is rejected. ### Match webhook events to your users Webhook payloads carry `externalReferenceId`, so you can route events without a lookup table. This includes the transaction events (`TRANSACTION_CREATE`, `TRANSACTION_UPDATE`, `TRANSACTION_DECLINE`) — currently in **staging only**, not yet in production — alongside the deposit, funding source, widget, KYC, and KYB events: ```json theme={null} { "userId": "5070d5a1-d71a-4190-91b0-f116eec51771", "accountId": "9c2e1b44-7a3d-4f08-b6e5-d18a3c7f0e22", "externalReferenceId": "usr_8f3d2a91", "eventType": "DEPOSIT_COMPLETE", "amount": 100.00 } ``` > [!WARNING] > > Handle the field being absent. `externalReferenceId` is omitted where the user's grant has no external reference ID associated, or where the event is flagged as private. A handler that assumes the field is always present will throw on those deliveries — and a webhook handler that throws is a webhook you didn't process. The value is resolved from **the grant between that user and your application**, so the same underlying event reaches each application carrying that application's own identifier for the user. See [Configure App Widget](/developers/configure-app-widget) for webhook setup. ### Getting user-scoped tokens `generateUserAccessToken` accepts `externalReferenceId` in place of `userId` and `accountId`. Fluz resolves the user and account from the grant that carries that reference, and embeds the reference in the token (see [Generate a User Access Token](/recipes/generate-user-access-token)). * The reference resolves only after the user has authorized your application with it as `external_id`. Before that, the call fails with `AUTH-0008`. * If you also pass `userId` or `accountId`, they must match the grant, or the call fails with `ARG-0001`. * When the grant carries a reference, mint tokens with it. [`registerBusiness`](/business-registration) finds the applicant's grant through the reference on the token, so a token minted by `userId` and `accountId` alone fails with `AUTH-0008`. Tokens you get by exchanging the authorization code at `/token/exchange` carry the association too. See [Exchange an OAuth Authorization Code](/exchange-an-o-auth-authorization-code). *** ## End to end One user, one identifier, four surfaces. **1. Your system already knows this person** User `usr_8f3d2a91` in your database clicks **Connect Fluz**. **2. Assign at consent** You redirect to `/authorize` with `external_id=usr_8f3d2a91`. They sign in, verify if needed, and approve your scopes. Fluz binds `usr_8f3d2a91` to their account, for your application only. **3. Exchange** Your callback exchanges the `code` for an `accessToken` and `refreshToken`. The association is embedded, so it survives every future refresh. You store the tokens against `usr_8f3d2a91` — no Fluz UUIDs in your schema. **4. Operate** You pay them out with `destination: { externalReferenceId: "usr_8f3d2a91" }`, using your own ID as the address. **5. Reconcile** The completion webhook arrives carrying `externalReferenceId: "usr_8f3d2a91"`. You route it straight to that user's record and mark the payout settled. No join, no lookup, no cache. *** ## Lifecycle ### Backfilling an existing grant If a user authorized your application before you adopted external reference IDs, supply one on a subsequent authorization and Fluz backfills it onto the existing grant — provided the grant doesn't already carry one. An existing value is never overwritten. ### Re-authorizing with a different value Because an existing value is never overwritten, passing a *different* `external_id` for a user who already has one does not change the mapping. Plan on the first value being permanent. If your user IDs are unstable, mint a dedicated immutable ID for Fluz rather than reusing something you might migrate. ### Deleting users on your side Never recycle an identifier. If you hard-delete a user and later reissue the same primary key to a different person, that new person inherits the old mapping — and the old person's Fluz account. Use UUIDs, or a monotonic sequence you never reset. *** ## Validation rules and errors | Scenario | Result | | :- | :- | | `externalReferenceId` not found for your application | `No user found with externalReferenceId .` | | Transfer destination with both `accountId` and `externalReferenceId` | `Provide either destination.accountId or destination.externalReferenceId, not both.` | | Transfer destination user has not authorized your application | `Destination account has not authorized this application.` | | Widget session created without an external reference ID | `External reference ID is required for applications` | ## Troubleshooting | Symptom | Almost always | | :- | :- | | `No user found with externalReferenceId` for a user you know exists | Wrong environment — the mapping was created in staging and you're calling production, or the reverse | | Same person appears as multiple Fluz users | A per-transaction or per-session value was passed instead of a per-user one | | Value you passed on re-authorization didn't take effect | The grant already carried one; existing values are never overwritten | | Webhook handler crashes intermittently | Field absent on some deliveries — grants without an association, or events flagged private | | Widget session rejected | Widget types require the `externalId` claim in the `patToken`; confirm it's in the signed payload, not just in your init options | | Transfer rejected as unauthorized | Destination user hasn't authorized your application, regardless of the mapping existing | *** ## What an external reference ID is not * **Not** a Fluz `userId` or `accountId`. Those are Fluz-issued UUIDs; this one is issued by you. * **Not** an idempotency key. That's `idempotencyKey` on API calls and `jti` in widget tokens, and it's unique per operation. This is unique per person. * **Not** the `state` parameter in the OAuth flow. `state` is per-authorization-attempt CSRF protection and is not stored. * **Not** the external account identifiers that appear on withdrawal records or linked funding sources. Those reference banking and processor records, not users. *** ## Next steps > **[Grant flow](/client-facing-o-auth-grant-flow)** > > Where you assign the identifier for OAuth apps. > **[Set up your server](/developers/setting-up-your-server)** > > Where you assign it for widget apps. > **[Transfer to another Fluz wallet](/features/transfer-to-another-fluz-wallet)** > > Addressing transfers by your own ID. > **[Configure app webhooks](/developers/configure-app-widget)** > > Receiving events that carry it back. # Create a virtual card Source: https://docs.fluz.app/recipes/create-virtual-card Issue a virtual card with a spend limit and funding source. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const CREATE_VIRTUAL_CARD = gql` mutation { createVirtualCard( input: { idempotencyKey: "07df5653-43a8-4532-9881-3ab5857bbe12" spendLimit: 123.45 spendLimitDuration: DAILY lockDate: "2030-10-10" lockCardNextUse: true cardNickname: "Team travel" primaryFundingSource: FLUZ_BALANCE offerId: "b3355504-ad30-4b2f-873d-b8795277b918" } ) { virtualCardId cardholderName expiryMonth expiryYear virtualCardLast4 status initialAmount createdAt } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(CREATE_VIRTUAL_CARD); console.log(response); ``` Requires the `CREATE_VIRTUALCARD` scope. Related: [Virtual cards](/features/virtual-cards). # Money movement Source: https://docs.fluz.app/recipes/deposit-funds Check balances and deposit funds into your Fluz cash balance. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; import crypto from 'crypto'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const MIN_DEPOSIT_AMOUNT = 100; async function deposit(userToken) { const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer ${userToken}`, 'Content-Type': 'application/json', }, }); const DEPOSIT = gql` mutation depositCashBalance($input: DepositCashBalanceInput!) { depositCashBalance(input: $input) { cashBalanceDeposits { cashBalanceDepositId status } } } `; const input = { idempotencyKey: crypto.randomUUID(), amount: 100, depositType: 'CASH_BALANCE', bankAccountId: '<>', }; const res = await client.request(DEPOSIT, { input }); return res.depositCashBalance; } ``` Requires the `MAKE_DEPOSIT` scope. Related: [Pay-ins](/features/deposit-from-external-accounts). # Generate a user access token Source: https://docs.fluz.app/recipes/generate-user-access-token Exchange API credentials for a scoped user access token. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const API_KEY = ''; const USER_ID = ''; const ACCOUNT_ID = ''; // Or identify the user by the external_id they authorized your app with const EXTERNAL_REFERENCE_ID = ''; const SCOPES = [ 'LIST_PURCHASES', 'LIST_OFFERS', 'LIST_PAYMENT', 'REVEAL_GIFTCARD', 'PURCHASE_GIFTCARD', 'MANAGE_PAYMENT', ]; const GENERATE_USER_ACCESS_TOKEN = gql` mutation generateUserAccessToken( $userId: UUID $accountId: UUID $externalReferenceId: String $scopes: [ScopeType!]! $seatId: UUID ) { generateUserAccessToken( userId: $userId accountId: $accountId externalReferenceId: $externalReferenceId scopes: $scopes seatId: $seatId ) { token scopes } } `; async function generateUserAccessToken() { const client = new GraphQLClient(API_URL, { headers: { Authorization: `Basic ${API_KEY}`, 'Content-Type': 'application/json', }, }); // Pass either userId and accountId, or externalReferenceId const data = await client.request(GENERATE_USER_ACCESS_TOKEN, { userId: USER_ID, accountId: ACCOUNT_ID, // externalReferenceId: EXTERNAL_REFERENCE_ID, scopes: SCOPES, }); console.log('Access Token:', JSON.stringify(data, null, 2)); } generateUserAccessToken(); ``` Pass either `userId` and `accountId`, or `externalReferenceId` — the `external_id` the user authorized your application with. With `externalReferenceId`, Fluz resolves the user and account from that grant and embeds the reference in the token, which `registerBusiness` requires when the grant carries one. See [Managing External Reference IDs](/managing-external-reference-ids#getting-user-scoped-tokens). Related: [Authentication](/get-started/api-credentials). # Get accounts Source: https://docs.fluz.app/recipes/get-accounts List all Fluz accounts associated with a user. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const API_KEY = 'your-api-key-here'; const USER_ID = 'user-id-uuid-here'; const GET_ACCOUNTS = gql` query getAccountsByUserId($userId: UUID!) { getAccountsByUserId(userId: $userId) { accountId type accountName } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Basic ${API_KEY}`, 'Content-Type': 'application/json', }, }); const data = await client.request(GET_ACCOUNTS, { userId: USER_ID }); console.log(JSON.stringify(data, null, 2)); ``` # Get virtual card offers Source: https://docs.fluz.app/recipes/get-virtual-card-offers List active virtual card offers available on your account. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const GET_VIRTUAL_CARD_OFFERS = gql` query GetVirtualCardOffers { getVirtualCardOffers( input: { cardBrandLocked: false, cardType: DEBIT, cardNetwork: MASTERCARD } ) { offerId bin bankName rewardValue programLimits { dailyLimit weeklyLimit monthlyLimit } programName } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(GET_VIRTUAL_CARD_OFFERS); console.log(response); ``` Requires the `CREATE_VIRTUALCARD` scope. Related: [Virtual cards](/features/virtual-cards). # Request a document verification link Source: https://docs.fluz.app/recipes/request-document-verification-link Generate a KYC document verification link to send to a user. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; const REQUEST_DOC_VERIFICATION = gql` mutation RequestDocumentVerificationLink($input: RequestDocumentVerificationLinkInput!) { requestDocumentVerificationLink(input: $input) { userId verificationType verificationId verificationUrl status message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(REQUEST_DOC_VERIFICATION, { input: { gaveConsent: true, prefillData: true, }, }); console.log(response); ``` Related: [KYC & KYB](/user-kyc-verification). # Request KYC autofill Source: https://docs.fluz.app/recipes/verify-user-autofill Request an identity decision using data already on file for the customer — no fields to submit. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; const VERIFY_USER_PREFILL_INFORMATION = gql` mutation verifyUserPrefillInformation { verifyUserPrefillInformation { status message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(VERIFY_USER_PREFILL_INFORMATION); console.log(response); ``` Related: [KYC & KYB](/user-kyc-verification). # Request a user KYC verification Source: https://docs.fluz.app/recipes/verify-user-kyc Submit user identity information for immediate KYC verification. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; const VERIFY_USER_INFORMATION = gql` mutation verifyUserInformation { verifyUserInformation( input: { firstName: "John" lastName: "Smith" streetLine1: "123 Main St" streetLine2: "" city: "Los Angeles" state: "CA" postalCode: "91234" country: "United States" dateOfBirth: "01/28/1975" ssnLast4: "1234" } ) { status message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(VERIFY_USER_INFORMATION); console.log(response); ``` Related: [KYC & KYB](/user-kyc-verification). # Refresh OAuth access token Source: https://docs.fluz.app/refresh-o-auth-access-token You can refresh an accessToken received from the exchange of the authorization `code` To refresh an access token, make a request to /token/refresh with the following query params: | query param | description | | - | - | | refresh\_token | The refreshToken token from the auth token exchange. | Additionally, set an Authorization header that is a base64 encoded string that is a combination of your client\_id:app\_secret. This is a Basic auth header, and so should follow the following format: `Authorization: Basic ` For example, if your `client_id` is `abc123` and your `client_secret` from the OAuth configuration is `def456`, the base64 encoded value would be `YWJjMTIzOmRlZjQ1Ng==`. The `client_id` and `client_secret` can be found in the `Overview` tab of the [Configure OAuth App](/configure-o-auth-app). Here is an example cURL command: ```text theme={null} curl -X GET "https://uni.staging.fluzapp.com/token/refresh?refresh_token=" -H "Authorization: Basic YWJjMTIzOmRlZjQ1Ng==" ``` The response will include: | query param | description | | :- | :- | | accessToken | Short-lived token for subsequent requests into Fluz‘s backend services | | refreshToken | refresh token to store and use when the accessToken becomes expired | | scopes | the values the user permitted through the previous flow | Here is an example of a full response: ```json theme={null} {"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTc1MjA5NDgwNH0.0dKCtVpN0mTHHMuGNNx4VLJisTnovFNPQKhSw6zWosc","authorizationCode":"f082972eb110b80f73b0d0f95d1de9266069a1e4","accessTokenExpiresAt":"2025-07-08T21:05:30.673Z","refreshToken":"8ec16c25951616150b0332a4a6d66547","refreshTokenExpiresAt":"2025-08-08T20:55:30.738Z","scope":\["MAKE\_WIDTHDRAW","MAKE\_DEPOSIT","LIST\_PAYMENT"],"client":\{"id":"dab5c80e-0321-4c3a-988a-ffedfd64d8db","app\_id":"0a92d46e-edf4-422e-8c62-946051e5067b","app\_name":"First OAuth integration","grants":\["authorization\_code","client\_credentials","password","refresh\_token"],"redirectUris":\["http\://localhost:3035/oauth/finalize"],"accessTokenLifetime":600},"user":\{"id":"5070d5a1-d71a-4190-91b0-f116eec51771"}} ``` # Register & verify customers overview Source: https://docs.fluz.app/register-and-verify-customers-overview Bring individuals and businesses onto Fluz over the API — the two onboarding tracks, the order operations have to happen in, and what each verification outcome means. Before anyone can hold value or move money on Fluz, two things have to be true: an account exists, and the identity behind it has been verified. This section covers doing both programmatically. There are two tracks — individuals and businesses — and a business track always contains an individual one. > **Individuals (KYC)** > > `registerUser` creates the account. `verifyUserInformation` verifies the person. Verification is synchronous, with a hard limit on attempts. > **Businesses (KYB)** > > `registerBusiness` creates the entity and starts verification. The result is asynchronous — the account opens in `PENDING` and resolves later. > [!WARNING] > > **Both are restricted.** User registration requires permission granted by Fluz per application; business registration requires the `REGISTER_BUSINESS` scope. Talk to your account manager before building against either. *** ## Do you need to do this at all? You don't have to own onboarding. An [embedded widget](/developers/widgets) runs registration, verification, and consent as a hosted flow, keeping identity data inside Fluz's compliance scope. | | Widget | API | | :- | :- | :- | | Who collects SSN, DOB, and documents | Fluz | You | | Who carries that data at rest | Fluz | You | | User re-types data you already hold | No | No | | Control over presentation | Branding only | Complete | | Build cost | Hours | Days | Register users yourself when you **already hold clean, verified profile data**. If you'd be collecting date of birth and SSN purely to forward them, use the widget — you're taking on regulated data handling for no benefit. *** ## The individual track **1. Register** `registerUser` with first name, last name, phone, region code, email, and date of birth (`YYYY-MM-DD`). Names should match the documents the user will verify with. A mismatch resurfaces at step 2 as a decline that's much harder to diagnose. → [User Registration](/user-registration) **2. Verify** `verifyUserInformation` runs KYC. Users must be verified to transact at all, or to transact at higher limits. → [User KYC Verification](/user-kyc-verification) **3. Get authorized** Registering someone does not give you permission to act for them. That's a separate, explicit grant. → [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow) ### Verification outcomes | Status | Meaning | What to do | | :- | :- | :- | | `APPROVED` | Verified | Proceed | | `DECLINED` | Not verified | Re-check the submitted data against the user's documents, then retry — but see the attempt limit | | `DUPLICATE` | Verified, but the data matches another user | Verified, so you can proceed. Investigate before scaling — it often means one person onboarding twice | | `ERROR` | Attempt limit exceeded | **Three attempts maximum.** Stop retrying | > [!WARNING] > > **Three attempts, then the door closes.** Validate aggressively *before* calling — address formatting, name spelling, date format — because a typo burns one of three tries. Never put `verifyUserInformation` behind an automatic retry loop. ### "Already in use" is normal `registerUser` returns `AUTH-0026` (phone) or `AUTH-0027` (email) when the person already has a Fluz account. Fluz accounts aren't scoped to your application, so anyone who has used Fluz before — through any app or the consumer product — already exists. That's a routing signal, not a failure. Send them through the [grant flow](/client-facing-o-auth-grant-flow) to authorize the account they already have. Build this as the normal path, not an error branch. *** ## The business track KYB has more moving parts and a strict order. **1. Register the primary owner as a user first** **This is the dependency people miss.** The primary business owner must already be a registered Fluz user, and the `emailAddress` on the owner record must be the one they registered with. Register the human before you register the company. **2. Fetch valid categories** `getBusinessCategories` returns the `businessCategoryId` and `businessSubCategoryId` values you'll need. Don't hard-code them, and don't mix a subcategory from one category with another — that's `BS-0006`. → [Business Categories](/business-categories) **3. Upload a document, if sole proprietorship** `SOLE_PROPRIETORSHIP` requires a supporting document, uploaded first via a REST endpoint as `form-data`. Pass the returned URL as `soleProprietorshipDocumentUrl`. **4. Register the business** `registerBusiness` with the entity details, legal address, tax ID, categories, account usage, and owners. Returns an `accountId` and a `kybStatus` of `PENDING`. → [Business Registration](/business-registration) **5. Wait for KYB to resolve** The account stays `PENDING` until verification completes. ### Constraints worth knowing before you build | Constraint | Detail | | :- | :- | | **One application at a time** | A user with an in-flight business application can't start another until it's approved or rejected (`BS-0007`) | | **Owners must be US-based** | `OwnerAddress` requires a valid US state name and a 5-digit ZIP | | **Ownership can't exceed 100%** | Summed across all owners | | **Tax ID format is strict** | `XX-XXXXXXX` — two digits, hyphen, seven digits (`BS-0001`) | | **Some countries are restricted** | Certain jurisdictions can't be registered for KYB | | **Account usage is required** | Either `businessAccountUsage` (from the enum) or `businessAccountUsageOther` | > [!WARNING] > > **Date of birth is formatted differently in the two flows.** `registerUser` takes `dateOfBirth` as `YYYY-MM-DD`. `BusinessOwner.dob` takes `MM/DD/YYYY`. Same concept, adjacent pages, opposite formats — and a wrong format here costs you a failed registration or a burned KYC attempt. *** ## Scopes and permissions | Operation | Requirement | | :- | :- | | `registerUser` | Application-level permission granted by Fluz | | `verifyUserInformation` | See [User KYC Verification](/user-kyc-verification) | | `registerBusiness` | `REGISTER_BUSINESS` scope | | `getBusinessCategories` | See [Business Categories](/business-categories) | Enable scopes on the app's **Permissions** tab before building. A scope you request but haven't enabled is silently dropped rather than rejected. → [Configure OAuth App](/configure-o-auth-app) *** ## Errors come back in `data` Both `registerUser` and `registerBusiness` report failure as an HTTP 200 with `success: false` and a populated `error` object — **not** as entries in the GraphQL `errors` array. Code that only inspects `errors` will read every failure as a success. Check all three layers: transport errors, then `success`, then branch on `error.code`. | Family | Covers | | :- | :- | | `AUTH-*` | User registration — invalid phone, already in use, permission, app inactive | | `BS-*` | Business registration — tax ID, address, owners, category, usage, structure | | `AR-*` | Missing or invalid arguments | | `AU-*` | General registration failure | *** ## Handling the data This section moves more regulated data than anywhere else in the API: full names, dates of birth, SSN digits, home addresses, identity documents. * Send it from your server over TLS. Never from a browser or mobile client. * Keep it out of logs, error-tracking payloads, and analytics. * Validate formats locally before calling, both to avoid burning KYC attempts and to avoid transmitting data twice. * Have an answer ready for how long you retain it and why. Your enterprise customers will ask. If none of that is appealing, that's the case for the [widget](/developers/widgets). *** ## Testing Staging mirrors these flows without real verification against real people. **Never register real individuals or businesses in staging**, and never carry staging test identities into production. → [Testing KYC Flows](/test-kyc-flows) · [Test Addresses](/test-addresses) · [Staging vs. Live](/concepts/environments) *** ## In this section > **[User registration](/user-registration)** > > Create an individual account. > **[User KYC verification](/user-kyc-verification)** > > Verify the person behind it. > **[Business registration](/business-registration)** > > Create an entity and start KYB. > **[Business categories](/business-categories)** > > Valid category and subcategory IDs. > **[Formatting referral links](/formatting-referral-links)** > > Bring users in through a referral. > **[Embedded widgets](/developers/widgets)** > > Hand the whole flow to Fluz instead. # Runa Source: https://docs.fluz.app/runa This document provides specific details for using the Fluz Gift Card Vendor API Connector with Runa as the vendor. ## Original Runa API vs Fluz API Adapter ### Base URL * **Original Runa API:** [https://playground.runa.io](https://playground.runa.io) * **Fluz API Adapter:** * Staging: [https://api-adapter.staging.fluzapp.com/runa](https://api-adapter.staging.fluzapp.com/runa) * Production: [https://api-adapter.fluzapp.com/runa](https://api-adapter.fluzapp.com/runa) ### Authentication **Runa Base URL** * Uses API Key Authentication with the header `X-Api-Key` ```text theme={null} X-Api-Key: ``` ### Fluz API Adapter * Uses Basic Authentication with the `Authorization` header (same as all vendor integrations). ```text theme={null} Authorization: Basic ``` ## API Endpoint Comparison | Operation | Runa Endpoint | Fluz Endpoint | Notes | | :- | :- | :- | :- | | Get Order | `GET /v2/order/:id` | `GET /v2/order/:id` | Retrieve details for a specific order | | Get All Orders | `GET /v2/order` | `GET /v2/order` | Retrieve the 100 most recent orders | | Create Order | `POST /v2/order` | `POST /v2/order` | Create a new gift card order | | Get Balance | `GET /v2/balance` | `GET /v2/balance` | Get your account balance. Returns an array of one | | Get Balance (single object) | `GET /v2/balance?currency=USD` | `GET /v2/balance?currency=USD` | The same balance, returned as a single object rather than an array. Balances are USD | # Request Examples ## Create Order ### **Original Runa Request:** ```text theme={null} { "payment_method": { "type": "ACCOUNT_BALANCE", "currency": "USD" }, "items": [ { "distribution_method": { "type": "PAYOUT_LINK" }, "products": { "type": "SINGLE", "value": "1800FL-US" }, "face_value": 10 } ], "description": "string" } ``` ### Fluz API Adapter Request: ```text theme={null} { "payment_method": { "type": "ACCOUNT_BALANCE", "currency": "USD" }, "items": [ { "distribution_method": { "type": "PAYOUT_LINK" }, "products": { "type": "SINGLE", "value": "1800FL-US" }, "face_value": 10 } ], "description": "d8e118ab-732b-4884-8e8a-70746b5f359e" } ``` The request shape is unchanged from Runa's. Only the brand code in `products.value` has to change, because it is resolved against the Fluz catalog. Correlate an order using the `id` from the response. ## Background Processing for Runa Orders All purchase operations through the Runa integration are processed as background operations. The Fluz API Adapter provides two response modes for Runa: ### Synchronous vs Asynchronous Processing Modes * **Synchronous Mode:** * Add header X-Execution-Mode: sync to wait for the background operation to complete * The API call will wait until the background purchase operation completes * Full operation results are returned in the response * Best for testing and low-volume flows, since the call stays open for the length of the purchase * **Asynchronous Mode (Default):** * Returns immediately with an operation reference ID * The purchase continues processing in the background * Check the status later by querying the order endpoint with the reference ID * Recommended for production, since your call returns immediately regardless of how long the purchase takes Both modes rely on background processing, but they differ in how the API responds to the client. **An order is readable once its purchase has completed.** Reading a reference ID before then returns an error rather than a pending status, so treat an error on a freshly created order as still processing and read again shortly. Order status values are Fluz's: `PENDING`, `IN_PROGRESS`, `COMPLETED`, `FAILED`, `CANCELED`. See [Connector behaviour](/connector-behaviour) for status values, error responses, limits, and request constraints. ## Example Usage with cURL ### Example 1: Get a Specific Order ```text theme={null} # Get a specific order with Runa via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/runa/v2/order/df263170-1c87-4e53-baf5-96258c3dd6b9" \ -H "Authorization: Basic " ``` ### Example 2: Get All Orders ```text theme={null} # Get all orders with Runa via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/runa/v2/order" \ -H "Authorization: Basic " ``` ### Example 3: Get Balance ```text theme={null} # Get your account balance with Runa via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/runa/v2/balance" \ -H "Authorization: Basic " ``` ### Example 4: Get Balance as a Single Object ```text theme={null} # Same balance as Example 3, returned as an object rather than an array curl -X GET "https://api-adapter.staging.fluzapp.com/runa/v2/balance?currency=USD" \ -H "Authorization: Basic " ``` ### Example 5: Create Order with Runa (Asynchronous Mode - Default) ```text theme={null} # Create Order with Runa via Fluz API Adapter (Asynchronous mode - default) # This will return quickly with a reference ID while processing continues in the background curl -X POST "https://api-adapter.staging.fluzapp.com/runa/v2/order" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "payment_method": { "type": "ACCOUNT_BALANCE", "currency": "USD" }, "items": [ { "distribution_method": { "type": "PAYOUT_LINK" }, "products": { "type": "SINGLE", "value": "1800FL-US" }, "face_value": 10 } ], "description": "d8e118ab-732b-4884-8e8a-70746b5f359e" }' ``` ### Example 6: Create Order with Runa (Synchronous Mode) ```text theme={null} # Create Order with Runa via Fluz API Adapter (Synchronous mode) # This will wait for the background process to complete before responding curl -X POST "https://api-adapter.staging.fluzapp.com/runa/v2/order" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -H "X-Execution-Mode: sync" \ -d '{ "payment_method": { "type": "ACCOUNT_BALANCE", "currency": "USD" }, "items": [ { "distribution_method": { "type": "PAYOUT_LINK" }, "products": { "type": "SINGLE", "value": "1800FL-US" }, "face_value": 10 } ], "description": "d8e118ab-732b-4884-8e8a-70746b5f359e" }' ``` # Sola / Cardknox Source: https://docs.fluz.app/sola Email sales to request access to our BETA program of Payment Processing API connectors. # Specialized verticals Source: https://docs.fluz.app/specialized-verticals Gaming, prediction markets, sweepstakes, and money services businesses run on the same Fluz APIs as everyone else. Here's the review path, the forms, and what to expect. Fluz supports licensed operators across gaming, prediction markets, and money services. These programs use the **same APIs, the same tokens, and the same capabilities** as every other Platform client — nothing about the integration changes. What's different is the review that happens before you go live. Because these programs sit inside a regulated perimeter shared with our sponsor banks and card networks, we run an enhanced review to confirm your licensing, your controls, and the specific use case you're building. > [!NOTE] > > **You don't have to wait to build.** Staging access is available while your review is in progress. Most operators complete their integration in parallel and go live the week their use case is approved. ## Verticals we support Pick the review form that most closely matches your business. If you operate across more than one — a sportsbook with a casino product, for example — start with your primary vertical and tell us about the rest in the form. > **[Casino operators](https://form.jotform.com/261466688486375)** > > Online and land-based casino operators. Start the review → > **[Licensed sports betting](https://form.jotform.com/261534192085356)** > > State-licensed sportsbooks and betting operators. Start the review → > **[Prediction markets](https://form.jotform.com/261485577360364)** > > Event contract and prediction market operators. Start the review → > **[Skill-based gaming](https://form.jotform.com/261485910444358)** > > Skill gaming and competitive play platforms. Start the review → > **[Fantasy sports](https://form.jotform.com/261485940182360)** > > Daily and season-long fantasy sports operators. Start the review → > **[Sweepstakes](https://form.jotform.com/261486105014348)** > > Sweepstakes and promotional play operators. Start the review → > **[Money services businesses](https://form.jotform.com/262085325776060)** > > Registered MSBs, including transmitters and exchangers. Start the review → ## How the review works **1. Sign up and complete KYB** Create your Fluz account and complete business verification. This establishes your legal entity, ownership, and control structure with us before anything else begins. [Start your KYB application →](http://fluz.app/kyb-application/) **2. Complete the review form for your vertical** Choose the form above that best matches your business and fill it out. If more than one applies, submit the closest match and note the others in the form. **3. Describe the end solution you're building** This is the part that moves fastest when it's specific. Tell us exactly how you intend to use Fluz — which capabilities, for which flows, funding which users, in which states or markets. Vague descriptions cost weeks. Precise ones get approved. **4. We evaluate your submission** Our compliance team reviews the form against our standards, then evaluates how your use case maps to the Fluz platform and whether it opens the door to any additional solutions we can support. **5. Follow-up questions and a risk team conversation** Expect follow-ups on licensing, funds flow, and controls. In most cases we'll also schedule a call with the Fluz risk team. This is a working conversation, not an interrogation — the operators who treat it that way move through it fastest. **6. Internal risk assessment and partner alignment** We compile the internal assessment and, where the program requires it, align with our sponsor bank, processor, or network partners. This step is largely out of your hands, and it's usually the longest one. **7. Sign-off on your defined use case** You're approved for the use case as defined. Production credentials are released and you go live. > [!NOTE] > > Approvals are scoped to the use case you described. If you later want to expand into a new flow, market, or product, come back to us first — expansions are reviewed, but they're a conversation, not a restart. ## How long it takes > **Typically 30–90 days end to end** > > The range is wide because the inputs vary. Three things drive where you land in it: the complexity of your business, the complexity of the solution you're building, and how transparently you work with our risk team. Operators who land near the fast end of the range tend to do the same few things: * **Have licensing documentation ready before you start.** State-by-state licenses, registrations, and any regulator correspondence. * **Describe one specific use case,** not a menu of everything you might eventually want to do. * **Answer follow-ups in days, not weeks.** Most of the elapsed time in a slow review is waiting on responses. * **Be upfront about the hard parts.** Prior terminations, chargeback history, ownership complexity, and pending regulatory matters all surface eventually. Surfacing them yourself is faster than us finding them, and it doesn't disqualify you. ## What we're evaluating Knowing what we look at makes the forms easier to fill out well: > **Licensing and jurisdiction** > > Which licenses you hold, which states or markets you operate in, and how you enforce those boundaries. > **Ownership and control** > > Beneficial ownership, control persons, and corporate structure. > **AML and player controls** > > Your compliance program, transaction monitoring, age and identity verification, and responsible play controls. > **Funds flow** > > Where money originates, where it settles, who holds it in between, and which Fluz capabilities sit in that path. ## Start the process > **[Not sure which form applies?](mailto:sales@fluz.app)** > > If you're working with a Fluz sales rep, reach out to them directly. Otherwise, email [sales@fluz.app](mailto:sales@fluz.app) with a short description of your business and we'll point you to the right path. # Stripe Source: https://docs.fluz.app/stripe Email sales to request access to our BETA program of Payment Processing API connectors. # Submit business documents Source: https://docs.fluz.app/submit-business-documents Upload supporting documents for a business application, and respond to documentation requests raised during KYB review. ## Overview Some business applications need a supporting document. Upload it to Fluz first, then pass the returned URL in the matching field on [registerBusiness](/business-registration). There are two document types, and only one of them is mandatory: | `documentType` | When you need it | | - | - | | `AUTHORIZED_SIGNER` | **Required** when the applicant owns less than 25% of the business **and** is not the control person | | `SOLE_PROPRIETORSHIP` | **Optional** supporting documentation for a sole proprietorship. Registration does not require it | > [!NOTE] > > The sole-proprietorship document used to be mandatory for `SOLE_PROPRIETORSHIP` businesses. It no longer is — send it only if you have it. ### Authorized signer documents An authorized signer is someone submitting on the business's behalf without being a beneficial owner or the control person. Upload the document that grants them that authority — a corporate resolution, power of attorney, or equivalent — and pass its URL as `authorizedSignerDocumentUrl`. ## Required scopes | Property | Value | | - | - | | Endpoint | REST — `POST /api/v1/file-upload/business-application-document` | | Authentication | OAuth Bearer Token | | Content-Type | `multipart/form-data` | | Required Scopes | `REGISTER_BUSINESS` | > [!NOTE] > > Unlike the KYB GraphQL operations, this endpoint enforces no scope and no account type — any valid application credential is accepted. Use the applicant's consumer token for consistency. ## Parameters Sent as `multipart/form-data` fields: | Field | Type | Required | Description | | - | - | - | - | | `file` | `File` | Yes | `application/pdf`, `image/jpeg`, or `image/png`. Maximum size 5 MB | | `documentType` | `String` | Yes | `AUTHORIZED_SIGNER` or `SOLE_PROPRIETORSHIP` | Upload one file per call. If you are sending both document types, call the endpoint twice and keep both URLs. ## cURL Example ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/file-upload/business-application-document \ -H "Authorization: Bearer " \ -F "file=@/path/to/document.pdf" \ -F "documentType=AUTHORIZED_SIGNER" ``` ## Response details ### Success — `200 OK` | Field | Type | Description | | - | - | - | | `url` | `String` | Public URL of the stored document. Pass this to `registerBusiness` | ```json theme={null} { "url": "https://storage.googleapis.com/.../business-applications/9f8e7d6c-..." } ``` Pass the returned `url` to `registerBusiness` as: * `authorizedSignerDocumentUrl` — for authorized-signer applicants * `soleProprietorshipDocumentUrl` — for sole proprietorships Pass it through unchanged. `registerBusiness` requires an absolute `http`/`https` URL of at most 255 characters and rejects anything else with `ARG-0001`. ### Error — `400 Bad Request` ```json theme={null} { "error": { "message": "Invalid arguments received - invalid document type.", "code": "ARG-0001" } } ``` ## Error Codes | Code | Name | Description | How to resolve | | - | - | - | - | | `ARG-0002` | MissingArguments | No `file` was included in the request | Send the file as a `multipart/form-data` field named `file` | | `ARG-0001` | InvalidArguments | Missing or invalid `documentType`, or the file's MIME type is not `application/pdf`, `image/jpeg`, or `image/png` | Check the `documentType` value and convert the file to a supported format | | `G-0001` | InternalError | Upload to storage failed | Retry once. If it persists, contact support with the timestamp | A missing or malformed `Authorization` header returns `401`; an expired Bearer token returns `498`. Files over 5 MB are rejected by the upload layer before the handler runs. ## Documentation requested during review Any business structure may be asked for additional documentation while its KYB case is under review — commonly formation documents, proof of address, or an ownership chart when an entity holds equity. These requests are not surfaced through the API. The case remains `PENDING` and your account manager will reach out. Do not resubmit the application in response: a second submission is blocked by `BS-0007` and creates a duplicate case. > [!NOTE] > > Missing beneficial owners is the most common reason a review stalls for extra documentation. Getting the [ownership roster](/business-registration#beneficial-ownership-requirements) complete on the first submission is the best way to avoid this step entirely. ## Notes * This is a single, consolidated endpoint for both document types — pass `documentType` in the form body rather than using a type-specific route. * The upload is stored immediately and the URL does not expire. It is not tied to any application until you submit `registerBusiness`. * Nothing verifies that `documentType` matches the URL field you later pass to `registerBusiness`. Keep the two URLs straight on your side. * Upload immediately before registering, so there is no ambiguity about which document belongs to which application. ## Related pages > **[KYB overview](/kyb-overview)** > > Prerequisites and the end-to-end registration flow. > **[Register a business](/business-registration)** > > Where the returned document URL is submitted. # TabaPay Source: https://docs.fluz.app/tabapay Email sales to request access to our BETA program of Payment Processing API connectors. # Tango Card Source: https://docs.fluz.app/tango This document provides specific details for using the Fluz Gift Card Vendor API Adapter with TangoCard as the vendor. ## Original TangoCard API vs Fluz API Adapter ### Base URL * Original TangoCard API: [https://integration-api.tangocard.com/rass](https://integration-api.tangocard.com/rass) * Fluz API Adapter: * Staging: [https://api-adapter.staging.fluzapp.com/tangocard](https://api-adapter.staging.fluzapp.com/tangocard) * Production: [https://api-adapter.fluzapp.com/tangocard](https://api-adapter.fluzapp.com/tangocard) ## Authentication ### Original TangoCard * Uses Basic Authentication with username and password. ```text theme={null} Username: Password: ``` ### Fluz API Adapter * Uses Basic Authentication with the `Authorization` header (same as all vendor integrations). ```text theme={null} Authorization: Basic ``` ## API Endpoint Comparison | Operation | TangoCard Endpoint | Fluz Endpoint | Notes | | :- | :- | :- | :- | | Get Order | `GET /v2/orders/:referenceOrderID` | `GET /v2/orders/:referenceOrderID` | Retrieve details for a specific order | | Get All Orders | `GET /v2/orders/` | `GET /v2/orders` | Retrieve the 100 most recent orders | | Create Order | `POST /v2/orders/` | `POST /v2/orders/` | Create a new gift card order | | Get Balance (All Accounts) | `GET /v2/accounts` | `GET /v2/accounts` | Get balance information for all accounts | | Get Balance (By Account) | `GET /v2/accounts/:accountId` | `GET /v2/accounts/:id` | Get balance for a specific account | # Request Examples ## Create Order ### Original TangoCard Request: ```text theme={null} { "sendEmail": false, "accountIdentifier": "testing1234account", "customerIdentifier": "johnr002", "utid": "U561593", "amount": 50 } ``` ### Fluz API Adapter Request: ```text theme={null} { "sendEmail": false, "accountIdentifier": "testing1234account", "customerIdentifier": "johnr002", "utid": "1800FL-US", "amount": 40, "externalRefID": "d8e118ab-732b-4884-8e8a-70746b5f359e" } ``` Orders are funded from the account your Fluz API key is issued for, so `accountIdentifier` and `customerIdentifier` are kept for compatibility with your existing request shape. Correlate an order using `referenceOrderID` from the response. ## Response timing Create order is synchronous, always running to completion. The call does not return until the purchase has finished, which can take up to 150 seconds, so set your client's read timeout above that. **Order status values are Fluz's, not TangoCard's.** The `status` field returns `PENDING`, `IN_PROGRESS`, `COMPLETED`, `FAILED`, or `CANCELED`. A check against a TangoCard value such as `COMPLETE` will not match. See [Connector behaviour](/connector-behaviour) for status values, error responses, limits, and request constraints. ## Example Usage with cURL ### Example 1: Get a Specific Order ```text theme={null} # Get a specific order with TangoCard via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/tangocard/v2/orders/72991f5a-b389-404e-98b0-ac5444839ca9" \ -H "Authorization: Basic " ``` ### Example 2: Get All Orders ```text theme={null} # Get all orders with TangoCard via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/tangocard/v2/orders" \ -H "Authorization: Basic " ``` ### Example 3: Get Balance for All Accounts ```text theme={null} # Get Balance for all accounts with TangoCard via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/tangocard/v2/accounts" \ -H "Authorization: Basic " ``` ### Example 4: Get Balance for a Specific Account ```text theme={null} # Get Balance for a specific account with TangoCard via Fluz API Adapter curl -X GET "https://api-adapter.staging.fluzapp.com/tangocard/v2/accounts/testing1234account" \ -H "Authorization: Basic " ``` ### Example 5: Create a New Order ```text theme={null} # Create Order with TangoCard via Fluz API Adapter # Note: this call blocks until the purchase completes. Allow up to 150 seconds. curl -X POST "https://api-adapter.staging.fluzapp.com/tangocard/v2/orders" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "sendEmail": false, "accountIdentifier": "testing1234account", "customerIdentifier": "johnr002", "utid": "1800FL-US", "amount": 40, "externalRefID": "d8e118ab-732b-4884-8e8a-70746b5f359e" }' ``` # Track a bulk job Source: https://docs.fluz.app/track-a-bulk-job Poll a submitted bulk operation for progress, read per-target results, and download the result file. `submitBulkOperation` returns immediately with a `jobId`. Everything after that is polling: **`getBulkOperationJob`** for the job's counters, **`getBulkOperationItems`** for the per-target outcomes. Completion is polling-only — there are no bulk webhooks. ## Requirements * `Authorization: Basic ` (your application API key) * The **bulk API capability** on your application * The job must belong to your application — another app's job id returns not-found, identical to an unknown id ## Which one to call | You want | Use | | - | - | | Is it done? How many succeeded? | `getBulkOperationJob` — one row read, cheap enough to poll | | Which targets failed, and why | `getBulkOperationItems(status: FAILED)` | | The Fluz resource a write produced | `getBulkOperationItems` → `resultResourceId` | | The export file, or a whole-job results ledger | `getBulkOperationJob` → `resultUrl` | `getBulkOperationJob` reads only the job row — no per-item scan — so it's the one to poll on an interval. Reach for `getBulkOperationItems` once the job is terminal, or when you specifically need per-target detail. ## Poll the job ```graphql theme={null} query TrackBulkJob($bulkJobId: UUID!) { getBulkOperationJob(bulkJobId: $bulkJobId) { jobId status operation targetMode requestedTargetCount acceptedItemCount skippedItemCount succeededItemCount failedItemCount createdAt } } ``` ```json theme={null} { "data": { "getBulkOperationJob": { "jobId": "9f53f64d-898d-45cc-b7fb-890935cca664", "status": "COMPLETED_WITH_ERRORS", "operation": "CREATE_TRANSFER", "targetMode": "SELECTED", "requestedTargetCount": 250, "acceptedItemCount": 240, "skippedItemCount": 10, "succeededItemCount": 236, "failedItemCount": 4, "createdAt": "2026-08-05T14:12:27.556Z" } } } ``` ### Job fields | Field | Type | Meaning | | - | - | - | | `jobId` | `UUID!` | Poll with this; also the key for results and cancellation. | | `status` | `BulkJobStatus!` | See below. | | `operation` | `BulkOperationType!` | Echoes what you submitted. | | `targetMode` | `BulkTargetMode!` | `SELECTED` or `ALL_CONNECTED`. | | `requestedTargetCount` | `Int!` | Resolved targets the job addresses — one item per target. | | `acceptedItemCount` | `Int!` | Items that will actually be processed. | | `skippedItemCount` | `Int!` | Resolved targets rejected **at submission**, before any work — typically missing scopes. | | `succeededItemCount` | `Int!` | Items finished successfully. | | `failedItemCount` | `Int!` | Items that ran and failed. | | `createdAt` | `DateTime!` | Submission time. | | `resultUrl` | `String` | Pre-signed download. See [Results file](#results-file). | | `resultUrlExpiresAt` | `DateTime` | When the current `resultUrl` stops working; `null` whenever `resultUrl` is. | **`skipped` and `failed` are not the same thing.** Skipped items never ran — they were rejected at submission and consumed no downstream call. Failed items ran and something went wrong. Reconciling a batch means reading both. The counters relate as `requestedTargetCount = acceptedItemCount + skippedItemCount`, and once terminal, `acceptedItemCount = succeededItemCount + failedItemCount`. ### Job status | Status | Meaning | | - | - | | `QUEUED` | Accepted, not started. | | `RUNNING` | At least one item is in flight. | | `COMPLETED` | Every accepted item succeeded. | | `COMPLETED_WITH_ERRORS` | Finished, but at least one item failed. **This is a normal outcome, not an error** — one target's failure never fails the job. | | `FAILED` | The job as a whole could not be processed. | | `CANCELLED` | Cancelled via `cancelBulkOperation`. | Terminal statuses are `COMPLETED`, `COMPLETED_WITH_ERRORS`, `FAILED` and `CANCELLED`. Stop polling on any of them. > [!NOTE] > > `UPDATE_TRANSACTION_METADATA` executes **synchronously**. Its job is already terminal when `submitBulkOperation` returns, so there is nothing to poll — go straight to `getBulkOperationItems` for the per-edit results. ## Read per-target results ```graphql theme={null} query BulkJobItems($bulkJobId: UUID!, $status: BulkItemStatus, $limit: Int, $after: String) { getBulkOperationItems(bulkJobId: $bulkJobId, status: $status, limit: $limit, after: $after) { items { itemIndex externalReferenceId accountId status resultResourceId errorCode errorMessage } totalCount hasNextPage nextCursor } } ``` ```json theme={null} { "bulkJobId": "9f53f64d-898d-45cc-b7fb-890935cca664", "status": "FAILED", "limit": 100, "after": null } ``` ### Arguments | Argument | Type | Description | | - | - | - | | `bulkJobId` | `UUID!` | The job to read. | | `status` | `BulkItemStatus` | Filter to one status. Omit for all items. | | `limit` | `Int` | Items per page. Default and maximum are both `100`. | | `after` | `String` | Opaque cursor from a previous page's `nextCursor`. Omit for the first page. | ### Item fields | Field | Type | Meaning | | - | - | - | | `itemIndex` | `Int!` | Position within the job, matching submission order. | | `externalReferenceId` | `String` | Your reference id for the target, when the grant has one. | | `accountId` | `UUID` | The target's Fluz account id. | | `status` | `BulkItemStatus!` | `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `SKIPPED`, `CANCELLED`. | | `resultResourceId` | `String` | The Fluz resource this item produced — e.g. a transfer id. Present on success. | | `errorCode` | `String` | Machine-readable reason when `FAILED` or `SKIPPED`. | | `errorMessage` | `String` | Human-readable detail. | | `metadataResults` | `[BulkMetadataEditResult!]` | Per-edit results for `UPDATE_TRANSACTION_METADATA`. `null` for every other operation. | **`totalCount` is the job's unfiltered item total**, read from the job's own counters — *not* a count of the filtered page. Filtering to `FAILED` on a 240-item job still reports `totalCount: 240`. Page with `hasNextPage`/`nextCursor`; don't infer the end from `totalCount`. `itemIndex` is the stable join key back to your submission: item *n* corresponds to the *n*-th entry in the `items` array you submitted, so you can map a failure to the exact transfer or deposit you asked for even when `externalReferenceId` is absent. ### Reconciling a batch ```graphql theme={null} # Everything that ran and failed. { getBulkOperationItems(bulkJobId: "…", status: FAILED, limit: 100) { items { itemIndex externalReferenceId errorCode errorMessage } hasNextPage nextCursor } } # Everything rejected before it ran — usually a scope or permission problem on your side. { getBulkOperationItems(bulkJobId: "…", status: SKIPPED, limit: 100) { items { itemIndex externalReferenceId errorCode errorMessage } hasNextPage nextCursor } } # Map successes back to Fluz resources. { getBulkOperationItems(bulkJobId: "…", status: SUCCEEDED, limit: 100) { items { itemIndex externalReferenceId resultResourceId } hasNextPage nextCursor } } ``` ### Metadata edit results `UPDATE_TRANSACTION_METADATA` reports per **edit**, not just per target — one item per target user, each carrying a `metadataResults` entry for every transaction it tried to change: ```graphql theme={null} { getBulkOperationItems(bulkJobId: "…") { items { externalReferenceId status metadataResults { recordId success errorCode errorMessage } } } } ``` An item can be `SUCCEEDED` while individual edits inside it failed, so check `metadataResults[].success` rather than the item status alone. ## Results file For read exports (`GET_TRANSACTIONS_EXPORT`, `GET_BALANCES_EXPORT`) the output is an **NDJSON file**. For the write operations (`CREATE_TRANSFER`, `DEPOSIT_CASH_BALANCE`) it's a per-target results ledger — one row per target with its status, `resultResourceId` and any error. ```graphql theme={null} { getBulkOperationJob(bulkJobId: "…") { status resultUrl resultUrlExpiresAt } } ``` Four things to know: * **`resultUrl` is minted only when you select it.** Polling `status` without it stays cheap; the URL is signed on request. * **It's `null` until the job is terminal**, and stays `null` for jobs that produced no file — for example when every target was denied. * **It expires shortly after issue.** Check `resultUrlExpiresAt`; to get a fresh URL, just select `resultUrl` again. * Neither Fluz's API nor your integration is in the download path — the URL points straight at storage. ## Cancelling ```graphql theme={null} mutation { cancelBulkOperation(bulkJobId: "…") { status succeededItemCount failedItemCount skippedItemCount } } ``` Best-effort and irreversible. Items not yet started are cancelled; **items already running are allowed to finish and record their results**, so counters can still move after you cancel. Safe to call more than once — an already-finished or already-cancelled job comes back unchanged rather than erroring. ## Errors | Condition | `extensions.code` | `extensions.errorName` | `statusCode` | | - | - | - | - | | Unknown job id, or a job belonging to another application | `APPLICATIONS-0006` | `BulkJobNotFound` | 404 | | Bulk API not enabled for your application | `APPLICATIONS-0003` | `BulkApiAccessDenied` | 403 | | Missing credentials, or a non-Basic scheme | `AUTH-0002` | `InvalidCredentials` | 401 | An id that belongs to another application returns the **same** not-found response as an id that doesn't exist — job ids are not enumerable across applications. Branch on `extensions.code`, not on message text. ## Polling guidance Poll `getBulkOperationJob` — it reads one row and never scans items. A few seconds between polls is reasonable for small jobs; back off for large ones, since a 10,000-item job is paced by queue throughput, not by how often you ask. Stop on any terminal status. Then: 1. `COMPLETED` — nothing to reconcile. 2. `COMPLETED_WITH_ERRORS` — read `status: FAILED` items, and `SKIPPED` too if `skippedItemCount > 0`. 3. `FAILED` or `CANCELLED` — read the items to see how far it got before stopping. For exports, select `resultUrl` only once the status is terminal; before that it's always `null`. # TSYS Source: https://docs.fluz.app/tsys Email sales to request access to our BETA program of Payment Processing API connectors. # Overview Source: https://docs.fluz.app/user-kyc-verification Verify your customers' identities through Fluz — either by embedding the Fluz widget or by submitting verifications directly through the API. Fluz requires customers to be identity-verified (KYC) before they can perform certain transactions, and verification is what unlocks higher transaction limits. Until a customer is verified, parts of the platform — funding a wallet, withdrawing funds, and virtual card issuance — remain unavailable to them. There are two ways to get a customer verified. The choice comes down to whether you want Fluz to own the verification experience, or whether you want to own it yourself. > **[Verify by Widget](/verify-customers-by-widget)** > > Embed the Fluz widget. Verification is handled as a built-in step — Fluz collects everything from the customer, in Fluz-hosted UI. You collect and store nothing. > **[Verify by API](#verifying-through-the-api)** > > Submit verifications yourself. You control the experience end to end and choose which method to use for each customer. ## Verifying through the widget If you have already embedded the Fluz widget, you may not need to build a verification flow at all. The widget includes verification as a gate: when a customer who is not yet verified enters it, the widget walks them through verification and then returns them to whatever they were doing. This is the lowest-effort path, and the only one where identity data never touches your systems. See [Verify by Widget](/verify-customers-by-widget). ## Verifying through the API If you are integrating directly against the API, you submit verifications yourself. There are four methods available today, and they differ in what you collect from the customer — and in who runs the verification. > **[KYC Autofill](/verify-customers-by-autofill)** > > You collect nothing. Fluz resolves and verifies the customer's identity from data already on file with `verifyUserPrefillInformation`. > **[Pass us the SSN information](/verify-customers-by-ssn)** > > You collect the customer's legal name, address, date of birth, and SSN, then submit it with `verifyUserInformation` for an immediate decision. > **[Request an IDV URL](/verify-customers-by-documents)** > > You request a verification link with `requestDocumentVerificationLink`. Fluz returns a hosted URL; your customer uploads their government ID and a selfie to Fluz directly. > **[Bring your own KYC](/verify-customers-by-external-provider)** > > Your platform already runs KYC with its own provider. Submit the decisioned result with `postKycVerification`; Fluz records it and updates the customer's status. ### Choosing between the API methods The first question is who runs the verification. If your platform operates its own approved KYC program, use [Verify by External Provider](/verify-customers-by-external-provider) — it reports your decision to Fluz rather than requesting one, and the escalation ladder below does not apply. For the three methods where Fluz runs the verification, most integrations escalate only when the lower-friction attempt does not succeed: **1. Try KYC Autofill first** It is a single synchronous call with no fields to collect, and it runs once per customer. Use it as your default first attempt. **2. Fall back to SSN verification** If Autofill declines, and you already hold — or can reasonably ask for — the customer's identity details, submit them directly for another immediate decision. **3. Fall back to an IDV URL** If SSN verification also declines, request a verification link and have the customer upload their ID and a selfie. This is the higher-assurance path and resolves asynchronously. > [!NOTE] > > A customer only needs to pass once, and all methods share a single verification state. Once a customer reaches `APPROVED` — by any method, including through the widget — further Fluz-run attempts are rejected with an `ERROR` status. External provider results are the exception: they are still recorded for audit, without changing the customer's status. Each method has a recipe you can walk through right here, without leaving the page: > **Recipe: Request KYC Autofill** > > **1. Prepare the verifyUserPrefillInformation mutation** > > There is no input to collect — the customer is identified entirely by the access token. > > ```javascript theme={null} > import { GraphQLClient, gql } from 'graphql-request'; > > const VERIFY_USER_PREFILL_INFORMATION = gql` > mutation verifyUserPrefillInformation { > verifyUserPrefillInformation { > status > message > } > } > `; > ``` > > **2. Prepare the GraphQL client** > > Authenticate with a [user access token](/recipes/generate-user-access-token) generated for the customer being verified. > > ```javascript theme={null} > const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; > > const client = new GraphQLClient(API_URL, { > headers: { > Authorization: `Bearer <>`, > 'Content-Type': 'application/json', > }, > }); > ``` > > **3. Process the request** > > The decision comes back in the same response. > > ```javascript theme={null} > const response = await client.request(VERIFY_USER_PREFILL_INFORMATION); > > console.log(response); > ``` > > ```json Response theme={null} > { > "data": { > "verifyUserPrefillInformation": { > "status": "APPROVED", > "message": "User verification successful" > } > } > } > ``` > > Copy-and-run version: [Request KYC Autofill](/recipes/verify-user-autofill). Full field reference: [KYC Autofill](/verify-customers-by-autofill). > **Recipe: Request a user KYC verification** > > **1. Prepare the verifyUserInformation mutation** > > Pass the user information to be verified. > > ```javascript theme={null} > import { GraphQLClient, gql } from 'graphql-request'; > > const VERIFY_USER_INFORMATION = gql` > mutation verifyUserInformation($input: VerifyUserInformationInput!) { > verifyUserInformation(input: $input) { > status > message > } > } > `; > ``` > > **2. Prepare the GraphQL client** > > Authenticate with a [user access token](/recipes/generate-user-access-token) generated for the customer being verified. > > ```javascript theme={null} > const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; > > const client = new GraphQLClient(API_URL, { > headers: { > Authorization: `Bearer <>`, > 'Content-Type': 'application/json', > }, > }); > ``` > > **3. Process the request** > > The decision comes back in the same response. > > ```javascript theme={null} > const response = await client.request(VERIFY_USER_INFORMATION, { > input: { > firstName: 'John', > lastName: 'Smith', > streetLine1: '123 Main St', > streetLine2: '', > city: 'Los Angeles', > state: 'CA', > postalCode: '91234', > country: 'United States', > dateOfBirth: '01/28/1975', > ssnLast4: '1234', > }, > }); > > console.log(response); > ``` > > ```json Response theme={null} > { > "data": { > "verifyUserInformation": { > "status": "APPROVED", > "message": "User verification successful" > } > } > } > ``` > > Copy-and-run version: [Request a User KYC Verification](/recipes/verify-user-kyc). Full field reference: [Verify by SSN](/verify-customers-by-ssn). > **Recipe: Request a document verification link** > > **1. Prepare the requestDocumentVerificationLink mutation** > > The customer is identified by the access token, so the input only carries consent and prefill flags. > > ```javascript theme={null} > import { GraphQLClient, gql } from 'graphql-request'; > > const REQUEST_DOC_VERIFICATION = gql` > mutation RequestDocumentVerificationLink($input: RequestDocumentVerificationLinkInput!) { > requestDocumentVerificationLink(input: $input) { > userId > verificationType > verificationId > verificationUrl > status > message > } > } > `; > ``` > > **2. Prepare the GraphQL client** > > Authenticate with a [user access token](/recipes/generate-user-access-token) generated for the customer being verified. > > ```javascript theme={null} > const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; > > const client = new GraphQLClient(API_URL, { > headers: { > Authorization: `Bearer <>`, > 'Content-Type': 'application/json', > }, > }); > ``` > > **3. Process the request** > > Deliver the returned `verificationUrl` to your customer, and store the `verificationId` to reconcile the webhook that carries the outcome. > > ```javascript theme={null} > const response = await client.request(REQUEST_DOC_VERIFICATION, { > input: { > gaveConsent: true, > prefillData: true, > }, > }); > > console.log(response); > ``` > > ```json Response theme={null} > { > "data": { > "requestDocumentVerificationLink": { > "userId": "eb910e93-5e39-4f53-99b9-0b033dd8e54b", > "verificationType": "DOCUMENT_VERIFICATION", > "verificationId": "idv_9MpDJC8aotDaxw", > "verificationUrl": "https://verify.fluz.app/idv/idv_9MpDJC8aotDaxw?key=27f09ec042881c2c56945680c53108a4", > "status": "OK", > "message": "Verification link request successful" > } > } > } > ``` > > Copy-and-run version: [Request a Document Verification Link](/recipes/request-document-verification-link). Full field reference: [Verify by Documents](/verify-customers-by-documents). ## Required scope Every verification method requires the **`VERIFY_KYC`** scope, which allows your application to request identity verification on a customer's behalf. > [!WARNING] > > `VERIFY_KYC` is not self-serve. It has to be enabled on your application by Fluz — reach out to your Fluz contact to have it turned on before you begin building. There are two layers of permission, and you need both: | Layer | What it is | How it is granted | | :- | :- | :- | | Application scope | Your app is permitted to request verifications at all. | Enabled on your application by Fluz. | | Customer authorization | An individual customer permits *your* app to submit a verification on their behalf. | Granted by the customer during the OAuth flow. Widget apps request it automatically. | The scope must also be included when you [generate the user access token](/recipes/generate-user-access-token) you use for the call. See [Application Scopes](/fluz-dashboard/application-scopes) for the full catalog. ## Set up a webhook Register a webhook endpoint before you send your first verification. Verification is not always resolved inside the API response — document verification in particular completes whenever the customer chooses to finish it, which may be minutes or days after you request the link. Widget verifications complete outside your application entirely. Webhooks are how you learn the outcome. > [!WARNING] > > Do not poll for verification status, and do not treat the absence of a webhook as a decline. Register an endpoint and react to the event. See [Webhooks](/fluz-dashboard/webhooks) for endpoint requirements, signature verification, retry behavior, and payload formats. In short: 1. Register an HTTPS endpoint on your application in the Developer Portal. Use the receiving URL itself — not a dashboard or inspection view of it. 2. Subscribe to the `KYC_STATUS_UPDATE` event in the webhook configuration. Without it, no KYC status updates are delivered, whichever verification method you use. 3. Verify the `X-HMAC-Signature` header against the raw request body on every delivery. 4. Deduplicate on `X-Event-ID` and respond `2xx` within 30 seconds. Your application — and, for OAuth and widget apps, the individual customer's grant — must hold `VERIFY_KYC` to receive verification events. `KYC_STATUS_UPDATE` fires only when the customer's KYC status actually **transitions**. A verification recorded for an already-verified customer preserves their status and emits no event — the API response is your confirmation in that case. The payload carries the previous and new status plus the verification data; see [KYC status update](/fluz-dashboard/webhooks#kyc-status-update-kyc_status_update) for the full shape. ## Verification statuses Every method resolves to one of the following statuses. | Status | Message | Description | | :- | :- | :- | | `APPROVED` | User verification successful | The customer is KYC verified. | | `DECLINED` | Verification declined | The customer is not KYC verified. You may escalate to another method. | | `DUPLICATE` | Duplicate user verification | The customer is verified, but their SSN matches an existing Fluz customer. | | `ERROR` | Error encountered with user verification | The request could not be processed. See [Attempt limits](#attempt-limits). | > [!NOTE] > > `DUPLICATE` is determined by **SSN** — and, for external provider results, the **document number** as well — never by address, since customers legitimately have multiple addresses over time. Fluz does not disclose which other customer matched. An `ERROR` status is returned when the customer is already verified, when the verification attempt limit has been reached, or when the document verification limit has been reached. ## Attempt limits Verification attempts are capped to prevent customers from guessing their way to an approval. * A customer may attempt SSN verification up to **3 times** per user ID through the API. * Document verification requests are capped separately. * **KYC Autofill runs once per customer**, whether it approves or declines. * Once a customer is `APPROVED`, no further attempts are accepted. * [Verify by External Provider](/verify-customers-by-external-provider) is the exception: you are reporting results rather than requesting screening, so it has no attempt limit — and a submission for an already-verified customer is recorded for audit without changing their status. ## Address formatting Every method that accepts an address expects the customer's **residential** address in the structured fields, with a consistent city, state, and postal code. A malformed or mismatched address is a common cause of an otherwise valid customer being `DECLINED`. > [!WARNING] > > PO boxes are not accepted as a legal address and will cause verification to fail. Submit a physical street address. International addresses are accepted. See [Address Formatting Requirements](/concepts/address-formatting-requirements) for the full rules. ## Testing Use the staging environment and the published test identities to exercise each path, including deliberate declines, before going live. See [Testing KYC Flows](/test-kyc-flows). # Overview Source: https://docs.fluz.app/user-registration Create a Fluz account for one of your users over the API, handle the already-exists case correctly, and hand off to authorization and verification. `registerUser` provisions a Fluz account for someone using profile data you already hold — no redirect, no hosted form, no asking the user to re-type their name and date of birth. > [!WARNING] > > **Restricted access.** This mutation requires explicit permission from Fluz. Contact your account manager to enable user registration for your application. Calls from an application without it fail with `AUTH-0022`. *** ## Where this fits Registration is one step of an onboarding arc, and it's optional — you can hand the whole thing to a widget instead. | Approach | Registration | Consent | KYC | Build cost | | :- | :- | :- | :- | :- | | **Widget** | Hosted by Fluz | Hosted by Fluz | Hosted by Fluz | Hours | | **Hybrid** | `registerUser` | Widget or redirect | `verifyUserInformation` | Days | | **API only** | `registerUser` | OAuth redirect | `verifyUserInformation` | Days, plus you handle the PII | Register users yourself when you **already hold clean profile data** and don't want the user typing it twice. If you'd be collecting name, date of birth, and contact details purely to pass them to Fluz, use an [embedded widget](/developers/widgets) instead — it keeps that collection inside Fluz's compliance scope. The full sequence for the API path: **1. Register** `registerUser` with name, phone, email, and date of birth. **2. Get authorized** The user signs in and grants your application scopes. → [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow) **3. Verify** Mint a user access token and run KYC with `verifyUserInformation`, or hand the user a document verification link. → [User KYC Verification](/user-kyc-verification) **4. Operate** Mint a user access token and use the API on their behalf. → [Authentication](/concepts/authentication) > [!WARNING] > > **Registering a user grants your application nothing.** Until the user completes the OAuth grant, you can't act for them: `generateUserAccessToken` and `getAccountsByUserId` fail with `AUTH-0031`, and the user doesn't appear in `getApplicationUsers`. That includes KYC — authorization comes **before** verification, not after. *** ## Consent mode > [!NOTE] > > Fluz turns consent mode on per application. Contact your account manager to switch. In consent mode, `registerUser` creates no user, account, or grant when you call it. It returns a link where the person signs up or logs in with the phone number you submitted and approves your application's scopes, all in one flow. When they approve, your application holds the grant, so there is no separate authorization step and no "already in use" branch to handle. One exception: if the person's Fluz account is already linked to your application under a different `externalReferenceId`, approval fails unless your application allows multiple external links. Approval also fails if the person signs in with a different phone number from the one you submitted, so submit the number they use with Fluz. ```graphql theme={null} mutation RegisterUser( $firstName: String! $lastName: String! $phoneNumber: String! $regionCode: String! $emailAddress: String! $externalReferenceId: String! $steps: [RegisterUserEnrollmentStep!] $callbackUrl: String ) { registerUser( firstName: $firstName lastName: $lastName phoneNumber: $phoneNumber regionCode: $regionCode emailAddress: $emailAddress externalReferenceId: $externalReferenceId steps: $steps callbackUrl: $callbackUrl ) { success status authorizationUrl expiresAt userId accountId error { code message } } } ``` | Parameter | Required | Description | | :- | :- | :- | | `firstName`, `lastName`, `phoneNumber`, `regionCode`, `emailAddress` | Yes | As for direct registration. The link pre-fills the phone number | | `externalReferenceId` | Yes | Your own ID for this person. It's stored on the grant. See [Managing External Reference IDs](/managing-external-reference-ids) | | `steps` | No | Setup steps after consent, in order: `KYC`, `FUNDING_SOURCE`, `PIN`. Leave it out to end the flow at consent. See [Enrollment widget](/developers/enrollment-widget) | | `callbackUrl` | No | An `https` URL for the **Back to app** button at the end of the flow | `dateOfBirth`, `billingAddress`, and `acceptCardholderAgreement` are not used. List `KYC` in `steps` to have the person verify their identity in the flow. Add the billing address with `addVirtualCardAddress` after the grant. The response depends on whether your application already holds a grant for this `externalReferenceId`: | `status` | Meaning | What to do | | :- | :- | :- | | `PENDING_CONSENT` | No grant yet. Returned the same way whether or not the person already has a Fluz account | Show `authorizationUrl` before `expiresAt`. Once the person approves, call `generateUserAccessToken` with your `externalReferenceId`. `OAUTH_USER_LINKED` usually signals this, and calling `registerUser` again returns `LINKED` | | `LINKED` | The grant already exists. `userId` and `accountId` are returned, with no link | Call `generateUserAccessToken` with your `externalReferenceId` | > [!WARNING] > > Send `authorizationUrl` only to the person you registered, and treat it like a sign-in link. It carries the name, email address and phone number you submitted, and anyone who has it can open it. Each call returns a new link, and nothing is stored until the person approves, so an expired or unused link needs no cleanup. Call `registerUser` again to get a new one. The full field and error reference is on [`registerUser`](/api-reference/mutations/register-user#consent-mode). *** ## The mutation ```graphql theme={null} mutation RegisterUser( $firstName: String! $lastName: String! $phoneNumber: String! $regionCode: String! $emailAddress: String! $dateOfBirth: String! $billingAddress: VirtualCardBillingAddressInput! $acceptCardholderAgreement: Boolean! ) { registerUser( firstName: $firstName lastName: $lastName phoneNumber: $phoneNumber regionCode: $regionCode emailAddress: $emailAddress dateOfBirth: $dateOfBirth billingAddress: $billingAddress acceptCardholderAgreement: $acceptCardholderAgreement ) { success userId accountId billingAddressId error { code message } } } ``` > [!NOTE] > > `error` is an object (`RegisterUserError`), not a string — always select `code` and `message` as subfields. Requesting bare `error` won't compile. ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `firstName` | String | Yes | The user's first name | | `lastName` | String | Yes | The user's last name | | `phoneNumber` | String | Yes | Digits and `-` accepted — `"5551234567"` or `"555-123-4567"`. This is the number the user receives sign-in codes on, so it must be a real mobile number in production | | `regionCode` | String | Yes | ISO 3166-1 alpha-2 country code for the phone number, e.g. `"US"` | | `emailAddress` | String | Yes | Must not already exist on a Fluz account | | `dateOfBirth` | String | Yes | `YYYY-MM-DD` | | `billingAddress` | VirtualCardBillingAddressInput | Yes | The user's billing address (`streetAddressLine1`, `city`, `state`, `postalCode`, `country`). Validated, saved for the user's virtual cards, and returned as `billingAddressId` | | `acceptCardholderAgreement` | Boolean | Yes | Must be `true` — registration is rejected otherwise | | `deferSeatAssignment` | Boolean | No | **Leave this out for platform onboarding.** When `true`, creates the user without a rewards-network seat, which is assigned later at card redemption (see [Register & Send](/features/open-loop-cards/register-and-send)). A user created this way can't be used for the authorize → verify → operate path: `generateUserAccessToken` for them fails with `AUTH-0008` | Names should match the identity documents the user will verify with — a mismatch surfaces later as a KYC failure that's much harder to diagnose than a registration error. ### What gets created A complete Fluz account: the user record, their wallet and balances, and their rewards eligibility. There's nothing further to provision before the user can authorize your app. *** ## Handling the response > [!WARNING] > > **Failures come back in `data`, not in `errors`.** A failed registration is an HTTP 200 with `success: false`. Code that only checks the GraphQL `errors` array will read every failure as a success. ```json Success theme={null} { "data": { "registerUser": { "success": true, "error": null } } } ``` ```json Failure theme={null} { "data": { "registerUser": { "success": false, "error": { "code": "AUTH-0026", "message": "The phone number you chose is already in use." } } } } ``` A handler that gets all three layers right: ```typescript theme={null} const res = await fetch(FLUZ.graphqlUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query: REGISTER_USER, variables: profile }), }); const body = await res.json(); // 1. Transport and GraphQL-level failures if (body.errors?.length) { throw new Error(`GraphQL error: ${body.errors[0].message}`); } const result = body.data.registerUser; // 2. Business-level failure — still an HTTP 200 if (!result.success) { switch (result.error.code) { case 'AUTH-0026': case 'AUTH-0027': // Not a failure. This person already has a Fluz account. return routeToAuthorization(profile); case 'AUTH-0004': return promptForValidPhoneNumber(); case 'AUTH-0022': case 'AUTH-0030': // Configuration problem on your side — don't retry, alert. throw new ConfigurationError(result.error.code); default: throw new RetryableError(result.error.message); } } // 3. Success — the next step is authorization, not verification return routeToAuthorization(profile); ``` *** ## "Already in use" is a routing signal, not an error `AUTH-0026` (phone) and `AUTH-0027` (email) mean the person **already has a Fluz account**. That's a normal, expected outcome — Fluz accounts are not scoped to your application, so anyone who has used Fluz before, through any app or the consumer product, already exists. Treating this as a failure is the most common integration mistake here. The right response is to stop trying to create an account and start asking for access to the existing one: send the user through the [OAuth grant flow](/client-facing-o-auth-grant-flow), or open a [widget](/developers/widgets). They log in to the account they already have and authorize you. Design your onboarding so the register-then-fall-back path is the normal case rather than an exception branch, and it stays clean at scale. Both branches end in the same place: the user authorizes your app. *** ## Retries and duplicates > [!WARNING] > > `registerUser` takes **no idempotency key**. A retry is a genuinely new attempt, and a timeout is an unknown outcome. If a call times out or the connection drops, the registration may well have succeeded. Retrying the identical request then returns `AUTH-0026` — which is indistinguishable from the user having had an account all along. That ambiguity is harmless as long as you treat both the same way: **on timeout, retry once, and route `AUTH-0026` / `AUTH-0027` to authorization rather than to an error state.** Either the account you just made or the account that already existed ends up authorized, which is the outcome you wanted. What you must not do is surface "phone number already in use" to a user who just gave you their number for the first time. *** ## How a registered user signs in A user created with `registerUser` has **no password**. When you send them to `/authorize`, Fluz signs them in with a **one-time code sent by SMS** to the `phoneNumber` you registered. * It's an SMS code, not a TOTP code. Authenticator apps such as Authy or Okta Verify can't produce it. * **In staging, no SMS is sent.** Enter `123456` as the code for any user you registered. * If the code is rejected in staging, check that the user is signing in with the same phone number you registered, then share the full `/authorize` URL with your Fluz contact. *** ## After registration A registered account is not yet an authorized or verified one. Before the user can move money, in this order: 1. **An authorization grant.** Registering someone doesn't give you permission to act for them — that's a separate, explicit step the user completes. → [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow) 2. **Your own identifier attached.** Pass `external_id` on the authorization so you can address this person by your own user ID from then on. → [Managing External Reference IDs](/managing-external-reference-ids) 3. **Identity verification.** With a token from the grant, pass the SSN and address you hold to `verifyUserInformation`, or issue a document verification link for the user to complete. → [User KYC Verification](/user-kyc-verification) > [!NOTE] > > **Known issue — business applicants created with `registerUser`.** Some users created with `registerUser` are missing an OAuth setting, so `registerBusiness` fails with `AUTH-0008` ("does not have permission to access the application") even after a valid grant. Until the fix ships, send the user through `/authorize` once, choose **Apply for a business account** on the account picker, and reuse the same `external_id` as their existing grant. `generateUserAccessToken` tokens then work for `registerBusiness`. If it still fails, contact your Fluz contact with the user ID. *** ## Handling the data You're transmitting full name, date of birth, email, and phone number — a set that identifies a real person. Send it over TLS from your server, keep it out of logs and error-tracking payloads, and don't echo it back in client-visible responses. Note that date of birth in particular is regulated identity data in most jurisdictions, and it stays sensitive on your side after the call succeeds. If you'd rather not hold any of it, that's the argument for the [widget](/developers/widgets) — Fluz collects it inside its own compliance scope and you never touch it. *** ## Error codes ### From `registerUser` `AUTH-0026` and `AUTH-0027` are never returned in consent mode. | Code | Meaning | What to do | | :- | :- | :- | | `AUTH-0004` | Phone number invalid or unparseable for the given `regionCode` | Fix the input. Check `regionCode` matches the number's country | | `AUTH-0022` | Your application isn't permitted to register users | Contact your account manager. Don't retry | | `AUTH-0025` | General registration failure | Retryable. Escalate if it persists | | `AUTH-0026` | Phone number already on an account | Route to authorization — see above | | `AUTH-0027` | Email already on an account | Route to authorization — see above | | `AUTH-0030` | Your application is not active (for example, status `PERSONAL`) | Use an **OAuth Integration** application that Fluz has moved to `ACTIVE`. See [Prepare Your Accounts](/get-started/prepare-accounts). Don't retry | | `WIDGET-003` | Consent mode: your application has no OAuth client, or no API key and secret to sign the link | Contact your account manager. Don't retry | ### Right after registration | Code | Where you see it | Cause | What to do | | :- | :- | :- | :- | | `AUTH-0031` | `generateUserAccessToken`, `getAccountsByUserId` | The user hasn't completed the OAuth grant | Send the user through [the grant flow](/client-facing-o-auth-grant-flow) | | `AUTH-0008` | `generateUserAccessToken` | The user was registered with `deferSeatAssignment: true` | Register without the flag | | `AUTH-0008` | `registerBusiness` | Known issue for users created with `registerUser` | See the known-issue note above | *** ## Environments Point at the GraphQL host for your environment — `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` for staging, `https://transactional-graph.fluzapp.com/api/v1/graphql` for production. Registration permission is granted per application, so a production app needs it enabled separately. → [Deploying to Production](/deploying-to-production) Never register real people in staging. → [Staging vs. Live](/concepts/environments) *** ## Next steps > **[Grant flow](/client-facing-o-auth-grant-flow)** > > Get permission to act on their behalf. > **[User KYC verification](/user-kyc-verification)** > > Verify the account you just created. > **[External reference IDs](/managing-external-reference-ids)** > > Address them by your own user ID. > **[Embedded widgets](/developers/widgets)** > > Hand the whole onboarding to Fluz instead. # KYC autofill Source: https://docs.fluz.app/verify-customers-by-autofill Request an identity decision using data Fluz already has on file for the customer — no identity fields to collect or submit. KYC Autofill is the lowest-effort API verification method. You don't collect or submit any identity fields — Fluz resolves and verifies the customer's identity using data already on file (such as their registered phone number) and returns a decision in the same response. Use it as your first API attempt whenever you want to avoid collecting identity data from the customer at all. If it declines, fall back to [SSN verification](/verify-customers-by-ssn) or [requesting an IDV URL](/verify-customers-by-documents). > [!NOTE] > > **Prerequisites** > > * The `VERIFY_KYC` scope enabled on your application by Fluz. See [Required scope](/user-kyc-verification#required-scope). > * A [user access token](/recipes/generate-user-access-token) generated for the customer being verified, including `VERIFY_KYC` in its scopes. > * A registered webhook endpoint. See [Verify Customers](/user-kyc-verification#set-up-a-webhook). ## How it works The `verifyUserPrefillInformation` mutation is synchronous. The customer is identified entirely by the user access token — there is no input to collect or validate. Fluz screens the customer's data on file and returns `APPROVED`, `DECLINED`, `DUPLICATE`, or `ERROR` in the response body. Fluz also emits a verification event to your webhook endpoint, so a single handler can process outcomes from every verification method consistently. > [!NOTE] > > KYC Autofill runs once per customer, whether it approves or declines — a repeat call returns without re-attempting verification. ## Request The customer being verified is identified by the user access token in the `Authorization` header. There is no input to provide. ## Example ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; const VERIFY_USER_PREFILL_INFORMATION = gql` mutation verifyUserPrefillInformation { verifyUserPrefillInformation { status message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(VERIFY_USER_PREFILL_INFORMATION); console.log(response); ``` ```json Response theme={null} { "data": { "verifyUserPrefillInformation": { "status": "APPROVED", "message": "User verification successful" } } } ``` ## Handling the response | Status | What to do | | :- | :- | | `APPROVED` | The customer is verified. Unlock the relevant functionality. | | `DECLINED` | Escalate to [SSN verification](/verify-customers-by-ssn) or [document verification](/verify-customers-by-documents). | | `DUPLICATE` | The customer is verified, but their identity data matches another Fluz customer. Treat as verified and review for duplicate accounts on your side. Fluz does not disclose which customer matched. | | `ERROR` | Inspect `message`. The customer is either already verified or has exhausted their attempts. | ## Testing See [Testing KYC Flows](/test-kyc-flows) for the general staging setup — a developer account, an active application, and a user access token for the customer you're verifying. > [!WARNING] > > Deterministic staging test identities for KYC Autofill aren't published yet. Confirm expected outcomes with the platform team before relying on them in an automated test suite. # Verify by documents Source: https://docs.fluz.app/verify-customers-by-documents Request a hosted verification link, deliver it to your customer, and let them upload their government ID and a selfie. Requesting an IDV URL is the higher-assurance API method, and the fallback when [passing us the SSN information](/verify-customers-by-ssn) does not produce an approval. Rather than collecting identity documents yourself, you request a hosted verification link from Fluz and hand it to your customer. Fluz collects and screens the documents directly. > [!NOTE] > > **Prerequisites** > > * The `VERIFY_KYC` scope enabled on your application by Fluz. See [Required scope](/user-kyc-verification#required-scope). > * A [user access token](/recipes/generate-user-access-token) generated for the customer being verified, including `VERIFY_KYC` in its scopes. > * A registered webhook endpoint. **This is required for document verification** — the outcome is not available in the API response. See [Verify Customers](/user-kyc-verification#set-up-a-webhook). ## How it works **1. You identify the customer and request a link** Call `requestDocumentVerificationLink` using a user access token generated for that customer. The token is what tells Fluz which customer the verification belongs to. **2. Fluz returns a verification link** The response contains a `verificationUrl` and a `verificationId`. Store the `verificationId` — it is how you reconcile the eventual webhook with this request. **3. You deliver the link to your customer** Send it however suits your product: email, SMS, push notification, or an in-app redirect. The customer can complete verification at any time. **4. Your customer uploads their documents** On the hosted page, the customer captures the front and back of a government-issued ID (driver's license, passport, state ID, or military ID) and takes a selfie for a biometric match. **5. Fluz notifies you of the outcome** When the customer finishes, Fluz screens the submission and sends an `APPROVED` or `DECLINED` result to your webhook endpoint. > [!WARNING] > > The verification link is unique to one customer and one verification attempt. Never reuse a link across customers, log it in a shared system, or expose it anywhere the intended customer is not the only reader — it grants access to an identity submission session. ## Request | Field | Type | Required | Description | | :- | :- | :- | :- | | `gaveConsent` | Boolean | Yes | Whether the customer consented to identity verification. Must be `true`, or the request is rejected. | | `prefillData` | Boolean | Yes | Whether to prefill the verification form with the identity information Fluz already holds. When `true`, you do not need to send the fields below. | | `firstName` | String | No | The customer's legal first name. | | `lastName` | String | No | The customer's legal last name. | | `streetLine1` | String | No | Residential street address. | | `streetLine2` | String | No | Apartment, suite, or unit. | | `city` | String | No | City. | | `region` | String | No | State or region. | | `postalCode` | String | No | ZIP or postal code. | | `country` | String | No | Country, in ISO 3166-1 alpha-2 format. | | `dateOfBirth` | String | No | Date of birth, formatted `YYYY-MM-DD`. | | `emailAddress` | String | No | The customer's email address. | | `phoneNumber` | String | No | The customer's phone number in E.164 format. | > [!NOTE] > > You must capture and record the customer's consent before setting `gaveConsent: true`. Anything you pass is used to prefill the form — the customer can review and correct it before submitting, so treat these fields as a convenience, not as the values that will be verified. > [!WARNING] > > Note the field naming difference from [SSN verification](/verify-customers-by-ssn): this mutation uses `region` where the other uses `state`, and expects `dateOfBirth` as `YYYY-MM-DD` rather than `MM/DD/YYYY`. ## Example ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; const REQUEST_DOC_VERIFICATION = gql` mutation RequestDocumentVerificationLink($input: RequestDocumentVerificationLinkInput!) { requestDocumentVerificationLink(input: $input) { userId verificationType verificationId verificationUrl status message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(REQUEST_DOC_VERIFICATION, { input: { gaveConsent: true, prefillData: true, }, }); console.log(response); ``` ```json Response theme={null} { "data": { "requestDocumentVerificationLink": { "userId": "eb910e93-5e39-4f53-99b9-0b033dd8e54b", "verificationType": "DOCUMENT_VERIFICATION", "verificationId": "idv_9MpDJC8aotDaxw", "verificationUrl": "https://verify.fluz.app/idv/idv_9MpDJC8aotDaxw?key=27f09ec042881c2c56945680c53108a4", "status": "OK", "message": "Verification link request successful" } } } ``` > [!NOTE] > > The `status` in this response (`OK`) confirms only that the **link was issued** — it is not the verification decision. The decision arrives later, by webhook. > **[Open the recipe](/recipes/request-document-verification-link)** > > A copy-and-run version of this example, ready to adapt to your integration. ## Response fields | Field | Type | Description | | :- | :- | :- | | `userId` | UUID | The Fluz customer the verification belongs to. | | `verificationType` | String | Always `DOCUMENT_VERIFICATION`. | | `verificationId` | String | Identifier for this verification attempt. Store it to reconcile the webhook. | | `verificationUrl` | String | The hosted link to deliver to your customer. | | `status` | String | `OK` when the link was issued successfully. | | `message` | String | Human-readable detail. | ## What the customer sees The hosted flow prefills known information for the customer to review, then asks them to: 1. Capture the front and back of a supported government-issued photo ID. 2. Take a selfie, which is matched biometrically against the ID photo. Document authenticity, tampering, and the biometric match are all screened before a decision is returned. ## Receiving the outcome Because the customer may complete verification long after you issue the link, the outcome arrives at your webhook endpoint. Match the incoming event to your stored `verificationId`, then act on the result: * **`APPROVED`** — the customer is verified. Unlock the relevant functionality. * **`DECLINED`** — the customer is not verified. Document verification is the final step in the escalation path; a decline here generally requires manual review rather than another automated attempt. Document verification requests are capped per customer. Once the limit is reached, further requests return `ERROR` with `Exceeded document verification limit`. ## Testing In staging you can complete the full document flow using sample driver's licenses and test identities. Document authenticity and other security features are not enforced in staging, and you may reuse the front image for the back capture. See [Testing KYC Flows](/test-kyc-flows). # Verify by external provider Source: https://docs.fluz.app/verify-customers-by-external-provider Submit a decisioned KYC result from your own identity verification provider instead of having Fluz run the verification. If your platform already runs KYC with its own identity provider, your customers should not have to verify twice. Submit the decisioned result to Fluz with `postKycVerification`. Unlike the other API methods, Fluz does not screen the identity itself here — the decision is yours. Fluz only validates and records it, and moves the customer's status accordingly. > [!NOTE] > > **Prerequisites** > > * An external KYC program approved by Fluz for your application. Talk to your account manager before building against this method. > * The `VERIFY_KYC` scope enabled on your application by Fluz. See [Required scope](/user-kyc-verification#required-scope). > * A [user access token](/recipes/generate-user-access-token) generated for the customer being verified, including `VERIFY_KYC` in its scopes. > * The dedicated secure ingestion endpoint for your environment, provided during onboarding. > [!WARNING] > > Send this mutation to the **dedicated secure ingestion endpoint**, not the standard API host. This endpoint securely tokenizes PII data like `person.ssn` in transit. A request whose SSN or other PII data arrive untokenized is rejected. Always pass the arguments as GraphQL **variables**; values inlined into the query document cannot be tokenized. > > * Staging: `https://secure.transactional-graph.staging.fluzapp.com/api/v1/graphql` > * Production: provided during onboarding ## How it works The `postKycVerification` mutation is synchronous. You submit the verified identity and the provider verifications backing your decision, and Fluz returns `APPROVED`, `DECLINED`, `DUPLICATE`, or `ERROR` in the response body. There is no customer-facing step. A few behaviors are specific to this method: * **Only decisioned results.** `decision` must be `PASSED` or `FAILED`. Do not send pending or undecisioned verifications. * **Idempotency.** Submissions are idempotent on `externalVerificationId` — your provider's unique id for the attempt. Replaying an id returns the original result and writes nothing; a re-decisioned verification must be submitted with a new id. * **Status transitions.** A `PASSED` result moves an unverified customer to verified. If the customer is already verified — by any method — the verification is still recorded for audit, but their status is never changed; the response message notes that the existing status was preserved. A `FAILED` result is recorded and leaves the status untouched. * **No attempt limit.** Because you are reporting results rather than requesting screening, this method has no attempt limit and does not count against the limits on [Verify by SSN](/verify-customers-by-ssn) or [KYC Autofill](/verify-customers-by-autofill). ## Request The customer is identified by the user access token in the `Authorization` header — `person.userId` and your application identity are filled in by Fluz, and any submitted values are overwritten. | Field | Type | Required | Description | | :- | :- | :- | :- | | `schemaVersion` | String | Yes | Version of the payload contract. Currently `"1.0"`. | | `externalVerificationProvider` | String | Yes | `IDOLOGY`, `OSCILAR`, `PERSONA`, or `CUSTOM`. | | `externalVerificationId` | String | Yes | Your provider's globally unique id for this attempt — the idempotency key. | | `decision` | String | Yes | `PASSED` or `FAILED`. | | `decisionReason` | String | No | Human-readable reason or rule that produced the decision. | | `decisionedAt` | String | No | ISO 8601 timestamp of the decision. Defaults to the time of ingestion. | | `person` | JSON | Yes | The verified identity as established by the provider. | | `verifications` | JSON | Yes | The provider verifications backing the decision — at least one of `document`, `ssn`, or `database`. | | `externalProviderData` | JSON | No | Freeform provider context for audit. | See [postKycVerification](/api-reference/mutations/post-kyc-verification) for the full field reference, including the `person` and `verifications` object shapes. ## Example ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://secure.transactional-graph.staging.fluzapp.com/api/v1/graphql'; const POST_KYC_VERIFICATION = gql` mutation postKycVerification( $schemaVersion: String! $externalVerificationProvider: String! $externalVerificationId: String! $decision: String! $decisionReason: String $person: JSON! $verifications: JSON! ) { postKycVerification( schemaVersion: $schemaVersion externalVerificationProvider: $externalVerificationProvider externalVerificationId: $externalVerificationId decision: $decision decisionReason: $decisionReason person: $person verifications: $verifications ) { status verificationId message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(POST_KYC_VERIFICATION, { schemaVersion: '1.0', externalVerificationProvider: 'PERSONA', externalVerificationId: 'inq_gCf28LrXY9wrxDoZbnbEqTrn', decision: 'PASSED', decisionReason: 'All checks passed', person: { firstName: 'JANE Q', lastName: 'SAMPLE', dateOfBirth: '1990-01-01', ssn: '900-98-7654', ssnLast4: '7654', address: { streetLine1: '123 EXAMPLE STREET', city: 'SAMPLETOWN', subdivision: 'CA', postalCode: '90001', countryCode: 'US', }, }, verifications: { document: { verificationId: 'ver_p8qmKydzkQwyLKbaAJRvADi9', status: 'PASSED', documentClass: 'DRIVER_LICENSE', documentNumber: 'X90000000000001', issuingCountryCode: 'US', photos: { front: 'https://files.provider.example/front.jpg' }, checks: [{ name: 'id_expired_detection', status: 'PASSED' }], }, ssn: { verificationId: 'ver_tin_snW2CvL2x4kTdZjbmGBjqcuV', status: 'PASSED', source: 'TIN_DATABASE', }, }, }); console.log(response); ``` ```json Response theme={null} { "data": { "postKycVerification": { "status": "APPROVED", "verificationId": "4b99e8c5-4201-45fd-a5dc-1c3b88e4f6c7", "message": "User verification successful" } } } ``` ## Handling the response | Status | What to do | | :- | :- | | `APPROVED` | The verification was recorded. If the customer was not yet verified, they now are; if they were already verified, `message` notes that the existing status was preserved. | | `DECLINED` | The `FAILED` result was recorded. The customer's status is unchanged. | | `DUPLICATE` | The verification was recorded, but the SSN or document number matches another Fluz customer. The account is placed under review — investigate on your side before proceeding. Fluz does not disclose which customer matched. | | `ERROR` | Inspect `message` — most commonly a payload that failed contract validation. Nothing was recorded. | When a submission changes the customer's KYC status, Fluz also publishes a `KYC_STATUS_UPDATE` event to your registered webhook — the same event every verification method emits. Your webhook configuration must include that event to receive it, and it fires only on status transitions; see [Set up a webhook](/user-kyc-verification#set-up-a-webhook). ## Testing Test against the staging endpoint with fabricated identities — because the decision is yours, there are no provider test-identity requirements as with the other methods. Use SSNs in the `900-XX-XXXX` range (never issued), generate a fresh `externalVerificationId` per attempt (replaying an id returns the original result rather than exercising a new one), and make sure any photo URLs you send are fetchable — Fluz retrieves and stores the images. > [!WARNING] > > Give every test customer a **unique** fabricated identity — a unique SSN and a unique document number. Submitting an SSN (last four plus date of birth) or a document number already recorded under a different customer returns `DUPLICATE` and places the account under review, exactly as it would in production. That includes the example values on this page — do not reuse them verbatim. # Verify by SSN Source: https://docs.fluz.app/verify-customers-by-ssn Submit a customer's legal name, address, date of birth, and last four SSN digits for an immediate identity decision. Passing Fluz the SSN information is the more direct of the two API methods. You collect a small set of identity fields from the customer and submit them, and Fluz returns a decision in the same response. Use it as your first API attempt whenever you already hold — or can reasonably ask for — the customer's identity details. If it declines, escalate to [requesting an IDV URL](/verify-customers-by-documents). > [!TIP] > > Collecting the SSN digits in your own web app? [Secure Identity > Input](/build-a-platform/secure-identity-input) runs this same verification > from a Fluz-hosted field in your page, so the digits never reach your > servers or your page's JavaScript. > [!NOTE] > > **Prerequisites** > > * The `VERIFY_KYC` scope enabled on your application by Fluz. See [Required scope](/user-kyc-verification#required-scope). > * A [user access token](/recipes/generate-user-access-token) generated for the customer being verified, including `VERIFY_KYC` in its scopes. > * A registered webhook endpoint. See [Verify Customers](/user-kyc-verification#set-up-a-webhook). ## How it works The `verifyUserInformation` mutation is synchronous. You submit the customer's information and Fluz screens it against identity data on file, returning `APPROVED`, `DECLINED`, `DUPLICATE`, or `ERROR` in the response body. There is no customer-facing step and nothing for the customer to complete. Fluz also emits a verification event to your webhook endpoint, so a single handler can process outcomes from every verification method consistently. ## Request The customer being verified is identified by the user access token in the `Authorization` header — you do not pass a user ID in the input. | Field | Type | Required | Description | | :- | :- | :- | :- | | `firstName` | String | Yes | The customer's legal first name. | | `lastName` | String | Yes | The customer's legal last name. | | `streetLine1` | String | Yes | Residential street address. | | `streetLine2` | String | No | Apartment, suite, or unit. Pass an empty string if unused. | | `city` | String | Yes | City. | | `state` | String | Yes | State or region. | | `postalCode` | String | Yes | ZIP or postal code. | | `country` | String | Yes | Country. | | `dateOfBirth` | String | Yes | Date of birth, formatted `MM/DD/YYYY`. | | `ssnLast4` | String | Yes | The last four digits of the customer's SSN. | > [!NOTE] > > Fluz accepts either the full SSN or just the last four digits. Submitting only the last four is recommended — it produces the same decision while reducing what you have to collect and store. > [!WARNING] > > Submit the customer's **residential** address, not a billing or mailing address. PO boxes are rejected. A mismatched address is the most common cause of a false decline — see [Address Formatting Requirements](/concepts/address-formatting-requirements). ## Example ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.fluzapp.com/api/v1/graphql'; const VERIFY_USER_INFORMATION = gql` mutation verifyUserInformation($input: VerifyUserInformationInput!) { verifyUserInformation(input: $input) { status message } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(VERIFY_USER_INFORMATION, { input: { firstName: 'John', lastName: 'Smith', streetLine1: '123 Main St', streetLine2: '', city: 'Los Angeles', state: 'CA', postalCode: '91234', country: 'United States', dateOfBirth: '01/28/1975', ssnLast4: '1234', }, }); console.log(response); ``` ```json Response theme={null} { "data": { "verifyUserInformation": { "status": "APPROVED", "message": "User verification successful" } } } ``` > **[Open the recipe](/recipes/verify-user-kyc)** > > A copy-and-run version of this example, ready to adapt to your integration. ## Handling the response | Status | What to do | | :- | :- | | `APPROVED` | The customer is verified. Unlock the relevant functionality. | | `DECLINED` | Escalate to [document verification](/verify-customers-by-documents). Do not re-submit the same information. | | `DUPLICATE` | The customer is verified, but their **SSN** matches another Fluz customer. Treat as verified and review for duplicate accounts on your side. Fluz does not disclose which customer matched. | | `ERROR` | Inspect `message`. The customer is either already verified or has exhausted their attempts. | > [!NOTE] > > A customer may attempt SSN verification up to **3 times**. After the third attempt, further requests return `ERROR` with `Exceeded user verification limit`. Move the customer to document verification rather than retrying. ## Testing Staging test identities return specific, deterministic result codes so you can exercise declines — address mismatch, deceased subject, thin file, invalid SSN, and others — without using real data. See [Testing KYC Flows](/test-kyc-flows). > [!WARNING] > > Do not modify the test identity data. Any field that does not match the expected test values will return a no-match rather than the result code you are trying to exercise. # Verify by widget Source: https://docs.fluz.app/verify-customers-by-widget Let the Fluz widget handle identity verification as a built-in step, with no identity data passing through your systems. If you embed the Fluz widget, verification comes with it. You do not build a verification flow, collect identity fields, or handle documents — the widget presents verification as a gate and clears it before letting the customer continue. This is the only path where a customer's identity information never touches your infrastructure, which is usually the deciding factor in choosing it. > [!NOTE] > > **Prerequisites** > > * A widget application configured in the Developer Portal, with OAuth settings in place. See [Configure App Widget](/developers/configure-app-widget). > * The `VERIFY_KYC` scope enabled on your application by Fluz. See [Required scope](/user-kyc-verification#required-scope). > * A registered webhook endpoint. See [Verify Customers](/user-kyc-verification#set-up-a-webhook). ## Where verification fits Verification is a step in the widget's normal sequence, not a separate integration: **1. Your page calls FluzEmbedded.init()** The widget opens in an iframe. **2. The customer signs in or registers** If they do not yet have a Fluz account, the widget creates one. **3. The widget checks verification status** If the customer is already verified, it moves straight on. If not, verification runs here. **4. The customer completes any remaining steps** PIN setup and OAuth scope authorization. **5. The customer completes their transaction** Confirmation, then completion. Because verification sits ahead of the transaction, a customer who cannot verify will not reach the transaction step at all. ## What the customer experiences When an unverified customer reaches the gate, the widget first attempts to verify them silently in the background. Many customers clear the gate at this point without being asked for anything. If that does not resolve, the widget presents a verification form inside the iframe, prefilled with whatever Fluz already holds. The customer reviews it, supplies what is missing, and submits. The widget then shows a waiting state while the result is processed and advances automatically once it resolves. If verification still does not succeed, the widget escalates the customer to document verification — capturing their government-issued ID and a selfie — within the same iframe. > [!NOTE] > > All of this happens inside the widget. You do not need to detect which stage a customer is at or trigger the escalation yourself. ## Scopes Widget applications request the scopes they need automatically during the OAuth authorization step, including `VERIFY_KYC`. You do not have to add it to the widget's scope list manually. You do still need `VERIFY_KYC` enabled on the application itself by Fluz. If it is not, the widget will fail to load with a missing-permissions error rather than skipping verification. > [!WARNING] > > When generating the short-lived token you pass to the widget as `patToken`, use your **API Key** for the `Authorization: Basic` header — not your OAuth client ID and not your `app_id`. Using the wrong value is the most common cause of a widget failing to load. See [Obtain Your API Credentials](/get-started/api-credentials). ## Knowing the outcome The widget tells the customer their result directly, but your application should not infer verification state from the widget closing. Rely on webhooks instead. Fluz emits `WIDGET_KYC_INITIATION` when a customer **begins** verification in the widget. This requires the `VERIFY_KYC` scope. ```json theme={null} { "eventType": "WIDGET_KYC_INITIATION", "userId": "550e8400-e29b-41d4-a716-446655440000", "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "externalReferenceId": "your-reference-123" } ``` > [!NOTE] > > This event marks the **start** of verification, not the result. Use `externalReferenceId` to map the event to your own customer record, and subscribe to the verification outcome events as well so you know how it resolved. See [Webhooks](/fluz-dashboard/webhooks). ## Combining the widget with the API The widget and the API share one verification state per customer, so the two approaches compose cleanly: * A customer verified through the API will pass straight through the widget's gate. * A customer verified in the widget is verified for your API calls too. * A customer who has exhausted their attempts in one channel has exhausted them in both. This matters if you verify customers through the API during onboarding and later hand them to the widget: check the customer's current verification status rather than assuming the widget will offer them another attempt. ## Testing Run the full widget flow in staging against the published test identities, including a deliberate decline so you can see the escalation to document verification. Staging widgets point at the staging environment rather than production — confirm you are loading the staging widget script and base URL. See [Testing KYC Flows](/test-kyc-flows) and [Staging vs. Live Environment](/concepts/environments). # Transaction lifecycle Source: https://docs.fluz.app/virtual-card-transaction-lifecycle How a card purchase travels from the merchant terminal to your ledger — authorization, clearing, settlement, reversals, and refunds — and what Fluz records at each step. A purchase on a Fluz-issued card is not a single event. It is a conversation between the merchant, the card network, and Fluz that plays out over seconds, hours, or sometimes weeks — and it produces several records on your side before it is finished. This page explains what happens at each stage, which Fluz records and webhooks each stage produces, and the places where a naive integration gets the arithmetic wrong. > [!NOTE] > > This page covers **open-loop card transactions** — spend on virtual cards Fluz issues onto the card networks. Gift card purchases, deposits, withdrawals, and wallet transfers do not go through this lifecycle; they settle on their own rails. See [Transactions Overview](/features/transactions-details-overview) for the unified ledger that holds all of them. *** ## The three stages | Stage | What happens | Whose money moves | | :- | :- | :- | | **Authorization** | The network asks Fluz whether the card can be charged. Fluz checks the card's controls and funding, and answers. | Nothing yet. Funds are **held**, not moved. | | **Clearing** | The merchant submits the final amount. Fluz finalizes the record against the held funds. | The hold becomes a real debit. | | **Settlement** | The network moves funds between the acquiring bank and the issuing bank. | Bank to bank. Invisible to your integration. | Settlement is a banking process that runs behind clearing on the network's own schedule. Fluz represents clearing and settlement as one event — when a transaction is cleared on Fluz, treat it as final. *** ## One purchase, several records A single purchase can produce an authorization, one or more clearings, and possibly a reversal or a refund. Fluz exposes these through three queries that answer different questions: | Feed | Query | What it shows | | :- | :- | :- | | **Card activity** | `getVirtualCardTransactions` | Spend on one or more cards, with merchant, MCC, FX, and network response fields | | **Account ledger** | `getTransactions` | Every money movement on the account, with balance snapshots after each record | | **Declined authorizations** | `getDeclinedTransactions` | Authorizations that were rejected and never became transactions | Three purchases, and the records each one leaves behind: ```text theme={null} Acme Hardware — $50.00 ├── Authorization $50.00 debit held against the card └── Clearing $50.00 debit hold converted, record final Riverside Hotel — $240.00 ├── Authorization $200.00 debit pre-auth at check-in ├── Incremental auth $75.00 debit incidentals added mid-stay └── Clearing $240.00 debit final folio, less than authorized Acme Hardware — $50.00, later refunded ├── Authorization $50.00 debit ├── Clearing $50.00 debit └── Refund $50.00 credit separate record, not a reversal ``` > [!WARNING] > > **A refund is a new record, not an edit to the old one.** > > Refunds arrive as their own `REFUND` transaction. Nothing about the original `PURCHASE` record changes — its amount stays where it was. If your system reduces the original purchase when a refund lands, you will double-count the credit. Reconcile at the record level; never mutate the original. *** ## Single-message and dual-message flows How many messages the network sends depends on the merchant and the transaction type. Both flows are normal, and your integration has to handle both. ### Single-message The network sends one message that authorizes and clears at the same time. Common for PIN debit, ATM withdrawals, and transit. There is no pending window — the transaction is final almost immediately. ```mermaid theme={null} sequenceDiagram autonumber participant M as Merchant participant N as Card network participant F as Fluz participant Y as Your integration M->>N: Purchase, $25.00 N->>F: Authorize and clear F->>F: Spend controls + funding check F-->>N: Approved N-->>M: Approved F->>Y: TRANSACTION_CREATE F->>Y: TRANSACTION_UPDATE (cleared) ``` ### Dual-message The network sends an authorization first, and the merchant submits the clearing later — typically the same night, but up to several days for hotels, car rentals, and travel. The gap between the two is the pending window, and it is where most reconciliation bugs live. ```mermaid theme={null} sequenceDiagram autonumber participant M as Merchant participant N as Card network participant F as Fluz participant Y as Your integration M->>N: Authorization request, $50.00 N->>F: Authorize F->>F: Spend controls + funding check F-->>N: Approved, funds held N-->>M: Approved F->>Y: TRANSACTION_CREATE (pending) Note over M,F: Hours to days pass M->>N: Batch submitted, $54.00 with tip N->>F: Clear F->>F: Match to authorization, finalize F->>Y: TRANSACTION_UPDATE (cleared, $54.00) ``` > [!NOTE] > > **The cleared amount can differ from the authorized amount.** Tips, fuel pumps, currency conversion, and partial shipments all produce a clearing that is higher or lower than the original hold. Take the cleared amount as authoritative, and never treat an authorization amount as final. ### Where the money sits The same lifecycle, viewed from the account rather than the network: ```mermaid theme={null} stateDiagram-v2 [*] --> Available: Funds in the spend account Available --> Held: Authorization approved Held --> Available: Reversal or expiration Held --> Cleared: Clearing received Available --> Cleared: Single-message transaction Cleared --> Credited: Refund received Credited --> [*] Cleared --> [*] ``` *** ## What Fluz records at each stage | Network message | What it means | Card feed `transactionType` | Ledger `status` | Webhook | | :- | :- | :- | :- | :- | | Verification | A $0.00 or $0.01 probe to confirm the card is live | `PURCHASE`, reversed soon | `PENDING`, released | `TRANSACTION_CREATE` + `TRANSACTION_UPDATE` | | Authorization | Reserve funds pending a final amount | `PURCHASE` | `PENDING` | `TRANSACTION_CREATE` | | Authorize and clear | Approve and finalize in one message | `PURCHASE` | `SETTLED` | `TRANSACTION_CREATE` + `TRANSACTION_UPDATE` | | Clearing | Finalize a previously authorized transaction | `PURCHASE` | `SETTLED` | `TRANSACTION_UPDATE` | | Decline | The authorization was rejected | `DECLINE` | *no ledger record* | `TRANSACTION_DECLINE` | | Reversal | An authorization released before it cleared | *hold released* | record released | `TRANSACTION_UPDATE` | | Refund | Value returned after a purchase cleared | `REFUND` | `SETTLED` credit | `TRANSACTION_CREATE` | > [!WARNING] > > **Two feeds, two status vocabularies.** > > * `getVirtualCardTransactions` returns `transactionStatus` values such as `PROCESSING` and `CLEARED`. > * `getTransactions` returns `status` values of `PENDING` and `SETTLED`. > > They describe the same lifecycle from two angles. Do not write code that expects one vocabulary in both places. → [How the GraphQL API works](/concepts/graphql) > [!NOTE] > > **A decline is not a transaction.** Declined authorizations never enter the account ledger, so they will not appear in `getTransactions` at any status. Query them through `getDeclinedTransactions` and read the reason from [Decline Codes](/features/decline-codes). *** ## Authorization When the network asks Fluz to approve a charge, Fluz evaluates the request against the card, the account, and the funding behind the card. All of it happens in well under a second, because the network will time out. > **What gets checked** > > * The card is `ACTIVE` — not locked, expired, past its `lockDate`, or already consumed by a single-use rule > * The amount fits within `spendLimit` for the card's `spendLimitDuration` > * The merchant matches the card's brand lock, if the card was issued on a brand-locked program > * The account holder has passed identity verification > * The funding sources behind the card can cover the amount > * The bank program's own limits are not exceeded > **Where the money comes from** > > A card does not hold a balance of its own. It draws, at authorization time, from the funding stack configured when the card was issued: > > 1. The spend account named in `userCashBalanceId`, or the account default > 2. The prepayment (gift card) balance, unless `usePrepaymentBalance: false` > 3. The rewards balance, unless `useRewardsBalance: false` > 4. An external bank account, when `primaryFundingSource` is `BANK_ACCOUNT` > > An authorization that exceeds what those sources can cover is declined, even when the card's `spendLimit` is higher. → [Manage Virtual Card Funding Sources](/Manage-Virtual-Card-Funding-Sources) > **Approved amount vs requested amount** > > The network requests an amount; Fluz posts what it approves. On a partial approval the two differ, and the approved amount is what is held. Read the amount from the Fluz record rather than assuming it matches what the merchant asked for. > **Declines** > > A declined authorization returns a response code to the merchant and produces a `declineReason` and `declineCategory` on the Fluz side. The most common causes are an amount over the spend limit, a locked card, insufficient funds behind the card, a brand-locked card at the wrong merchant, and CVV or AVS mismatch. → [Decline Codes](/features/decline-codes) ### Authorizations that are not purchases | Type | What it is | What to do with it | | :- | :- | :- | | $0 / $**0.01 probe** | A merchant confirming the card is live before storing it or charging it later | Expect it, and do not count it as spend. It reverses on its own. | | **Pre-authorization** | A hold placed before the final amount is known — hotels, car rentals, fuel pumps | Expect a clearing at a different amount, often days later. | | **Incremental** | An additional hold stacked on an open pre-authorization | Sum the holds; do not treat the second one as a second purchase. | | **Fuel dispenser** | A fixed amount set by network rules, unrelated to what is actually pumped | The clearing carries the real amount. | ### Held funds An approved authorization reduces what the card can still spend without moving money out of the account. Until it clears: * The card's `remainingBalance` reflects the hold * The ledger record sits at `PENDING` * `expectedClearedDate` tells you when to look again If a clearing never arrives, the hold does not sit there forever — the network's expiration rules release it, and the funds return to the card's available balance. Most authorizations expire within about a week; travel and lodging holds run longer. The exact window is set by the network and the merchant, not by Fluz. *** ## Reversals A reversal cancels an authorization *before* it clears. The hold is released and the funds return to the card. Reversals can be full or partial. Common causes: * The merchant abandoned the sale, or the terminal timed out * The item was out of stock, or the cardholder cancelled before shipment * A duplicate authorization was sent * The authorization expired without a clearing > [!WARNING] > > **A "reversal" after clearing is really a refund.** > > Once a transaction has cleared there is no hold left to release. Money that comes back after that point arrives as a credit — a separate `REFUND` record — and should be handled as one. The presence of a matching clearing is the dividing line between the two cases. *** ## Clearing and settlement Clearing is the merchant submitting the final amount, usually as part of an overnight batch. Fluz matches it to the open authorization using the network's reference identifiers and finalizes the record. Realities to build for: * **The amount changes.** Tips, fuel, FX, and partial shipments all move the number. * **There can be more than one clearing.** A split shipment clears in pieces against one authorization, and the pieces can arrive out of order. * **A clearing can arrive with no authorization.** Networks permit a merchant to force-post in some situations — offline terminals, in-flight purchases, transit fare aggregation. Fluz monitors these, but your ledger has to accept a purchase that appears already cleared with no pending phase. * **Matching is not guaranteed.** In rare cases the identifiers on a clearing do not line up with the authorization it belongs to, and the clearing appears as its own record. > [!WARNING] > > **Do not reconcile on network reference identifiers alone.** They are not guaranteed to stay consistent across the life of a transaction, and they are not stable across networks. Store the Fluz `record_id` and `reference_id` against your own order at the time you create it. → [Reconciling against your own system](/features/transactions-details-overview#reconciling-against-your-own-system) *** ## Refunds A merchant returning value sends a credit back through the network. Fluz posts it as a `REFUND` on the card feed and as a credit on the ledger. It may arrive as an authorization that clears later, or as a clearing on its own. Two cases that break naive matching: * **Unlinked refunds.** The network may send the credit with no reference to the original purchase, or with different identifiers. It arrives as a standalone credit with nothing to join it to. * **Batched refunds.** Several refunds for different original purchases can share network identifiers and arrive grouped. Because of both, do not assume a one-to-one relationship between refunds and purchases. Reconcile refunds as independent credits against the card, and let the balance be the source of truth. *** ## Foreign currency A purchase made in another currency clears in USD, with the original amount preserved on the record: | Field | Meaning | | :- | :- | | `originalCurrencyCode` | ISO 4217 code of the currency the merchant charged in | | `originalCurrencyAmount` | The amount the card issuer reports as requested, **in minor units** of the merchant's currency: `6300` HKD is HK\$63.00. On a reversal it is the amount still on hold before that reversal, not the amount released, so reconcile on the USD amount | | `currencyConversionRate` | The rate applied to reach USD. `1.0` for domestic transactions | These three fields are returned together — all populated, or all null. Conversion happens at clearing, so a foreign authorization and its clearing commonly differ in USD terms even when the merchant charged the same amount. *** ## Common message sequences Beyond the two happy paths, these are the sequences worth having test coverage for. | Sequence | What it is | | :- | :- | | `AUTHORIZE_AND_CLEAR` | PIN debit, ATM, transit — no pending window | | `AUTHORIZE` → `CLEAR` | The standard purchase | | `AUTHORIZE` → `CLEAR` (higher) | Restaurant tip added after the card was presented | | `AUTHORIZE` → `CLEAR` (lower) | Partial shipment, or a hotel folio under the pre-auth | | `AUTHORIZE` → `AUTHORIZE` → `CLEAR` | Pre-auth plus incremental auth, cleared once | | `AUTHORIZE` → `CLEAR` → `CLEAR` | Split shipment, clearing in pieces | | `AUTHORIZE` → `REVERSAL` | Sale abandoned before clearing | | `AUTHORIZE` → `REVERSAL` (partial) → `CLEAR` | Part of the hold released, the rest cleared | | `AUTHORIZE` → *(expiry)* | No clearing ever arrives; the hold is released on expiration | | `CLEAR` with no `AUTHORIZE` | Force post — offline terminal, in-flight, transit aggregation | | `AUTHORIZE` → `CLEAR` → `REFUND` | Purchase later refunded, in full or in part | | `VERIFICATION` (\$0.00) | Card-on-file validation, reversed shortly after | *** ## Building against the lifecycle **1. Treat pending and cleared as different things** Never show a pending authorization as a completed purchase, and never sum authorizations and clearings together. If you need one number, sum cleared records and show holds separately. **2. Subscribe to all three transaction events** `TRANSACTION_CREATE`, `TRANSACTION_UPDATE`, and `TRANSACTION_DECLINE`. An integration that listens only for creates will show every transaction stuck at its authorization amount forever. → [Webhooks](/fluz-dashboard/webhooks) **3. Sync on updatedGte, not createdGte** A record created as `PENDING` and later cleared changes its updated timestamp, not its created timestamp. A created-date sync silently misses every settlement. **4. Reconcile balances from snapshots** Every ledger record carries the after-state of every balance. Read those fields rather than summing amounts yourself — they already account for fees, cashback, and open holds. **5. Make handlers idempotent** Webhooks retry, and clearings can arrive out of order. Key on the Fluz record identifier and make replay a no-op. *** ## Testing the lifecycle Staging cards are real card records but are not on a live network, so transactions are injected against them rather than swiped. You can exercise an authorization, a separate clearing, a decline, a reversal, a refund, and a zero-dollar probe — each producing the same records and webhooks as production. → [Simulate Virtual Card Transactions](/Simulate-Virtual-Card-Transactions) *** ## Next steps > **[Transactions Overview](/features/transactions-details-overview)** > > The unified ledger — what a record contains and how to reconcile it. > **[Get Virtual Card Transactions](/features/get-virtual-card-transactions)** > > Card-level activity, filters, FX fields, and pagination. > **[Get Declined Transactions](/features/get-decline-transactions)** > > Authorizations that never became transactions. > **[Decline Codes](/features/decline-codes)** > > Every decline reason and category, and what to do with each. > **[Simulate Virtual Card Transactions](/Simulate-Virtual-Card-Transactions)** > > Put a test spend on a staging card and watch the lifecycle run. > **[Webhooks](/fluz-dashboard/webhooks)** > > Subscribe to transaction events, verify signatures, handle retries. # Worldpay Source: https://docs.fluz.app/world-pay Email sales to request access to our BETA program of Payment Processing API connectors. # addAuthorizedUser Source: https://docs.fluz.app/api-reference/mutations/add-authorized-user Add an authorized user to the caller's account with specified UAC roles. Add an authorized user to the caller's account with specified UAC roles. The target account is always resolved from the caller's credentials - Bearer tokens use the token's accountId; Basic (API key) callers use the application's configured operator account. Requires the MANAGE\_SUBUSERS scope. Supports both Bearer and Basic auth. The target user must already exist in Fluz. If the user previously had a DECLINED or INACTIVE assignment, it will be reactivated with the new roles. ```graphql theme={null} mutation { addAuthorizedUser( email: String phone: String roles: [UACRoleType!]! status: UACRoleStatusType sendInvite: Boolean! ): AddAuthorizedUserResponse } ``` ## Arguments **email** `String` The email address of the user to add. At least one of email or phone is required. **phone** `String` The phone number of the user to add. At least one of email or phone is required. **roles** `[UACRoleType!]!` *required* The UAC roles to assign. OWNER is not allowed. **status** `UACRoleStatusType` The initial status for the role assignment. Defaults to PENDING. **sendInvite** `Boolean!` *required* Whether auth-service should send the role assignment invite email. Defaults to true. Set to false to create the assignment without sending an invite. ## Returns [`AddAuthorizedUserResponse`](/api-reference/types/add-authorized-user-response) — Response returned from the addAuthorizedUser mutation. # addBankCard Source: https://docs.fluz.app/api-reference/mutations/add-bank-card addBankCard adds a bank card to the user's wallet. addBankCard adds a bank card to the user's wallet. Requires PCI\_COMPLIANCE from the developer and MANAGE\_PAYMENT scope. PERSONAL/Private applications are exempt from PCI\_COMPLIANCE requirement. ```graphql theme={null} mutation { addBankCard( input: AddBankCardInput! ): BankCard } ``` ## Arguments **input** `AddBankCardInput!` *required* *No description provided in the schema yet.* ## Returns [`BankCard`](/api-reference/types/bank-card) — BankCard is a record of a bank card added by a user. # addVirtualCardAddress Source: https://docs.fluz.app/api-reference/mutations/add-virtual-card-address Saves a billing address for virtual card issuance. Saves a billing address for virtual card issuance. Requires CREATE\_VIRTUALCARD scope. When authUserId is provided, the address is saved for that authorized user's underlying user\_id but remains attached to the caller's account. ```graphql theme={null} mutation { addVirtualCardAddress( input: AddVirtualCardAddressInput! ): UserAddress } ``` ## Arguments **input** `AddVirtualCardAddressInput!` *required* *No description provided in the schema yet.* ## Returns [`UserAddress`](/api-reference/types/user-address) — UserAddress represents a User Address record. # approveApprovalRequest Source: https://docs.fluz.app/api-reference/mutations/approve-approval-request Approves an open approval request through the existing approval link pipeline. Approves an open approval request through the existing approval link pipeline. ```graphql theme={null} mutation { approveApprovalRequest( approvalId: UUID! ): ApprovalRequestActionResponse! } ``` ## Arguments **approvalId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`ApprovalRequestActionResponse!`](/api-reference/types/approval-request-action-response) — Response returned from approve/decline approval request mutations. # assignObjectOwner Source: https://docs.fluz.app/api-reference/mutations/assign-object-owner Assign an owner to an object that does not have one yet. Assign an owner to an object that does not have one yet. The target account is resolved from the caller's credentials (Bearer accountId or Basic operator account). Requires MANAGE\_SUBUSERS. Forwards to auth-service POST /api/v1/object-owners. ```graphql theme={null} mutation { assignObjectOwner( objectType: ObjectOwnerObjectType! objectId: UUID! userId: UUID! ): AssignObjectOwnerResponse } ``` ## Arguments **objectType** `ObjectOwnerObjectType!` *required* *No description provided in the schema yet.* **objectId** `UUID!` *required* *No description provided in the schema yet.* **userId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`AssignObjectOwnerResponse`](/api-reference/types/assign-object-owner-response) # assignSupervisor Source: https://docs.fluz.app/api-reference/mutations/assign-supervisor *No description provided in the schema yet.* ```graphql theme={null} mutation { assignSupervisor( superviseeUserId: UUID! supervisorUserId: UUID! ): SupervisionAssignmentResponse! } ``` ## Arguments **superviseeUserId** `UUID!` *required* *No description provided in the schema yet.* **supervisorUserId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`SupervisionAssignmentResponse!`](/api-reference/types/supervision-assignment-response) # bulkUpdateErpTransactionMetadata Source: https://docs.fluz.app/api-reference/mutations/bulk-update-erp-transaction-metadata Applies ERP-aware categorization to multiple transactions in one call (max 100 items). Applies ERP-aware categorization to multiple transactions in one call (max 100 items). ```graphql theme={null} mutation { bulkUpdateErpTransactionMetadata( items: [BulkUpdateErpTransactionMetadataItem!]! ): BulkUpdateErpTransactionMetadataResult! } ``` ## Arguments **items** `[BulkUpdateErpTransactionMetadataItem!]!` *required* *No description provided in the schema yet.* ## Returns [`BulkUpdateErpTransactionMetadataResult!`](/api-reference/types/bulk-update-erp-transaction-metadata-result) # cancelBulkOperation Source: https://docs.fluz.app/api-reference/mutations/cancel-bulk-operation Cancel one of your bulk jobs, using 'Basic '. Cancel one of your bulk jobs, using 'Basic \'. Best-effort and irreversible: items not yet started are cancelled, while items already running are allowed to finish and record their results. Safe to call more than once — an already-finished or already-cancelled job is returned unchanged. Returns not-found for a job id that is unknown or belongs to another application. ```graphql theme={null} mutation { cancelBulkOperation( bulkJobId: UUID! ): BulkJob } ``` ## Arguments **bulkJobId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`BulkJob`](/api-reference/types/bulk-job) — A submitted bulk job. # closeUserCashBalance Source: https://docs.fluz.app/api-reference/mutations/close-user-cash-balance Mutation for closing a user cash balance account. Mutation for closing a user cash balance account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { closeUserCashBalance( input: CloseUserCashBalanceInput! ): CloseUserCashBalanceResponse! } ``` ## Arguments **input** `CloseUserCashBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`CloseUserCashBalanceResponse!`](/api-reference/types/close-user-cash-balance-response) — Response type for the closeUserCashBalance mutation, returning a closed user cash balance and affected virtual card records # completePlaidLink Source: https://docs.fluz.app/api-reference/mutations/complete-plaid-link Completes Plaid Link after the frontend receives publicToken from Plaid Link onSuccess. Completes Plaid Link after the frontend receives publicToken from Plaid Link onSuccess. Requires MANAGE\_PAYMENT scope. BYOI app relinks should use completePlaidRelink instead. ```graphql theme={null} mutation { completePlaidLink( input: CompletePlaidLinkInput! ): PlaidLinkCompletionResponse! } ``` ## Arguments **input** `CompletePlaidLinkInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidLinkCompletionResponse!`](/api-reference/types/plaid-link-completion-response) — Safe public result of completing a Plaid Link flow. # completePlaidRelink Source: https://docs.fluz.app/api-reference/mutations/complete-plaid-relink Confirm and repopulate a repaired Item using its unchanged access token. Confirm and repopulate a repaired Item using its unchanged access token. Requires BYOI Plaid on the app. Requires the `MANAGE_PAYMENT` scope. See [BYOI Plaid](/features/byoi-plaid#relink-a-disconnected-bank-link). ```graphql theme={null} mutation { completePlaidRelink( input: CompletePlaidRelinkInput! ): PlaidLinkCompletionResponse! } ``` ## Arguments **input** `CompletePlaidRelinkInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidLinkCompletionResponse!`](/api-reference/types/plaid-link-completion-response) - Safe public result of completing a Plaid Link flow. # createPlaidLinkAddress Source: https://docs.fluz.app/api-reference/mutations/create-plaid-link-address Creates the billing/legal address required after Plaid Link when completePlaidLink returns requiresAddress. Creates the billing/legal address required after Plaid Link when completePlaidLink returns requiresAddress. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { createPlaidLinkAddress( input: CreatePlaidLinkAddressInput! ): CreatePlaidLinkAddressResponse! } ``` ## Arguments **input** `CreatePlaidLinkAddressInput!` *required* *No description provided in the schema yet.* ## Returns [`CreatePlaidLinkAddressResponse!`](/api-reference/types/create-plaid-link-address-response) # createPlaidLinkToken Source: https://docs.fluz.app/api-reference/mutations/create-plaid-link-token Creates a Plaid Link token for new bank linking or relinking an existing Plaid connection. Creates a Plaid Link token for new bank linking or relinking an existing Plaid connection. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { createPlaidLinkToken( input: CreatePlaidLinkTokenInput! ): PlaidLinkTokenResponse! } ``` ## Arguments **input** `CreatePlaidLinkTokenInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidLinkTokenResponse!`](/api-reference/types/plaid-link-token-response) — Response containing the token required to initialize Plaid Link. # createPlaidRelinkToken Source: https://docs.fluz.app/api-reference/mutations/create-plaid-relink-token Create an update-mode Link token using the existing Item and its original Plaid credentials. Create an update-mode Link token using the existing Item and its original Plaid credentials. Requires BYOI Plaid on the app; other apps relink with createPlaidLinkToken and platformItemId. Requires the `MANAGE_PAYMENT` scope. See [BYOI Plaid](/features/byoi-plaid#relink-a-disconnected-bank-link). ```graphql theme={null} mutation { createPlaidRelinkToken( input: CreatePlaidRelinkTokenInput! ): PlaidLinkTokenResponse! } ``` ## Arguments **input** `CreatePlaidRelinkTokenInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidLinkTokenResponse!`](/api-reference/types/plaid-link-token-response) - Response containing the token required to initialize Plaid Link. # createTransfer Source: https://docs.fluz.app/api-reference/mutations/create-transfer Create a transfer between accounts within an application. Create a transfer between accounts within an application. The sender is determined from the authentication credentials. Pass `useRewardsBalance` on [`CreateTransferInput`](/api-reference/types/create-transfer-input) to fund from rewards. See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). ```graphql theme={null} mutation { createTransfer( input: CreateTransferInput! ): CreateTransferResponse } ``` ## Arguments **input** `CreateTransferInput!` *required* *No description provided in the schema yet.* ## Returns [`CreateTransferResponse`](/api-reference/types/create-transfer-response) — Represents the response returned from the createTransfer mutation. # createUserCashBalance Source: https://docs.fluz.app/api-reference/mutations/create-user-cash-balance Mutation for creating a new user cash balance account. Mutation for creating a new user cash balance account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { createUserCashBalance( input: CreateUserCashBalanceInput! ): UserCashBalance! } ``` ## Arguments **input** `CreateUserCashBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`UserCashBalance!`](/api-reference/types/user-cash-balance) — UserCashBalance represents a user's cash balance account. # createVirtualCard Source: https://docs.fluz.app/api-reference/mutations/create-virtual-card Initiates virtual card creation. Initiates virtual card creation. Requires CREATE\_VIRTUALCARD scope. @throws INVALID\_INPUT if: * idempotency key is not provided * spendLimit less than \$1 * lockDate is in the past * spendLimit is not within card program limit * offerId is defined and is in uuid format When billingAddress or userAddressId is provided, the address is registered with the card program before card issuance. If the issuer has not approved the address within the server-side wait window (typically \~60s), this mutation returns an error with code VC-0020 (PENDING\_APPROVAL) and extensions.addressId. Callers should retry the same request using the returned addressId until the card is issued successfully. ```graphql theme={null} mutation { createVirtualCard( input: CreateVirtualCardInput! ): VirtualCard } ``` ## Arguments **input** `CreateVirtualCardInput!` *required* *No description provided in the schema yet.* ## Returns [`VirtualCard`](/api-reference/types/virtual-card) — VirtualCard represents a record of a virtual card generated by a user. # createVirtualCardBulkOrder Source: https://docs.fluz.app/api-reference/mutations/create-virtual-card-bulk-order Initiates bulk virtual card creation. Initiates bulk virtual card creation. Requires CREATE\_VIRTUALCARD scope. As with createVirtualCard, per-item billingAddress or userAddressId triggers card-program registration before job enqueue. A PENDING\_APPROVAL error with extensions.addressId may be returned on first call for a brand-new address; retry the same request to complete. ```graphql theme={null} mutation { createVirtualCardBulkOrder( input: CreateVirtualCardBulkOrderInput! ): VirtualCardBulkOrder } ``` ## Arguments **input** `CreateVirtualCardBulkOrderInput!` *required* *No description provided in the schema yet.* ## Returns [`VirtualCardBulkOrder`](/api-reference/types/virtual-card-bulk-order) — VirtualCardBulkOrder provides information about bulk creation order. # deactivateVCShareLinks Source: https://docs.fluz.app/api-reference/mutations/deactivate-vcshare-links Deactivates share links. Deactivates share links. Requires CREATE\_SHARE\_LINK scope. ```graphql theme={null} mutation { deactivateVCShareLinks( input: DeactivateVCShareLinksInput! ): String } ``` ## Arguments **input** `DeactivateVCShareLinksInput!` *required* *No description provided in the schema yet.* ## Returns `String` — The `String` scalar type represents textual data, represented as UTF-8 character sequences. # declineApprovalRequest Source: https://docs.fluz.app/api-reference/mutations/decline-approval-request Declines an open approval request through the existing approval link pipeline. Declines an open approval request through the existing approval link pipeline. ```graphql theme={null} mutation { declineApprovalRequest( approvalId: UUID! ): ApprovalRequestActionResponse! } ``` ## Arguments **approvalId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`ApprovalRequestActionResponse!`](/api-reference/types/approval-request-action-response) — Response returned from approve/decline approval request mutations. # deleteBankCard Source: https://docs.fluz.app/api-reference/mutations/delete-bank-card deleteBankCard set a bank card's status to INACTIVE. deleteBankCard set a bank card's status to INACTIVE. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { deleteBankCard( input: DeleteBankCardInput! ): BankCard } ``` ## Arguments **input** `DeleteBankCardInput!` *required* *No description provided in the schema yet.* ## Returns [`BankCard`](/api-reference/types/bank-card) — BankCard is a record of a bank card added by a user. # depositCashBalance Source: https://docs.fluz.app/api-reference/mutations/deposit-cash-balance Mutation for depositing into a cash balance. Mutation for depositing into a cash balance. Requires MAKE\_DEPOSIT scope. ```graphql theme={null} mutation { depositCashBalance( input: DepositCashBalanceInput! ): DepositCashBalanceResponse! } ``` ## Arguments **input** `DepositCashBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`DepositCashBalanceResponse!`](/api-reference/types/deposit-cash-balance-response) — Response type for the depositCashBalance mutation, returning the resulting deposits and current user balances. # disablePlaidIntegration Source: https://docs.fluz.app/api-reference/mutations/disable-plaid-integration Disables the application's Plaid integration. Disables the application's Plaid integration. Existing links made with these credentials stop working until the same credentials are registered again. Requires the `BYOI_PLAID` scope. See [BYOI Plaid](/features/byoi-plaid). ```graphql theme={null} mutation { disablePlaidIntegration: PlaidIntegrationResponse! } ``` ## Returns [`PlaidIntegrationResponse!`](/api-reference/types/plaid-integration-response) # editVirtualCard Source: https://docs.fluz.app/api-reference/mutations/edit-virtual-card Mutation to edit virtual cards. Mutation to edit virtual cards. Requires EDIT\_VIRTUALCARD scope. ```graphql theme={null} mutation { editVirtualCard( input: EditVirtualCardInput! ): VirtualCard } ``` ## Arguments **input** `EditVirtualCardInput!` *required* *No description provided in the schema yet.* ## Returns [`VirtualCard`](/api-reference/types/virtual-card) — VirtualCard represents a record of a virtual card generated by a user. # generateUserAccessToken Source: https://docs.fluz.app/api-reference/mutations/generate-user-access-token Generate a user token using 'Basic '. Generate a user token using 'Basic \'. The token will be associated with the user\_id and will have the specified scopes. ```graphql theme={null} mutation { generateUserAccessToken( userId: UUID accountId: UUID scopes: [ScopeType!]! seatId: UUID externalReferenceId: String ): GenerateUserAccessTokenResponse } ``` ## Arguments **userId** `UUID` The userId that will be associated with the token. Required if externalReferenceId is not provided. **accountId** `UUID` The accountId that will be associated with the token. Required if externalReferenceId is not provided. **scopes** `[ScopeType!]!` *required* The scopes that the token will have access to. **seatId** `UUID` The seatId will be used to make transactions, default to the most recently created if not provided. **externalReferenceId** `String` Your unique identifier for this user. This is the same value passed as external\_id during the OAuth authorization flow. Only applicable to OAuth applications. If provided, userId and accountId are optional. ## Returns [`GenerateUserAccessTokenResponse`](/api-reference/types/generate-user-access-token-response) — Represents the response returned from the generateUserAccessToken mutation. # generateVCShareLinks Source: https://docs.fluz.app/api-reference/mutations/generate-vcshare-links Generates share links. Generates share links. Requires `CREATE_SHARE_LINK` scope. Creates `quantity` share requests and returns one single load open loop card link per request. Each link maps to one share request (`PENDING`) and is funded from `userCashBalanceId` at claim time, not at generation. ```graphql theme={null} mutation GenerateVCShareLinks($input: GenerateVCShareLinksInput!) { generateVCShareLinks(input: $input) { shareLinks recipientRegistrationResults { index status userId error } } } ``` ## Arguments **input** `GenerateVCShareLinksInput!` *required* See [`GenerateVCShareLinksInput`](/api-reference/types/generate-vcshare-links-input) for the full field list. Key fields: | Field | Type | Required | Description | | - | - | - | - | | `cardLimit` | `Int!` | Yes | Spend limit (and load amount) for each card, in whole currency units. Must be a whole number ≥ the program minimum. | | `offerId` | `String!` | Yes | UUID v4 of the merchant offer the card is tied to. The offer must be active and its merchant must be shareable. | | `quantity` | `Int!` | Yes | Number of links to generate. One distinct hosted URL is created per unit. | | `shareMethod` | [`ShareMethodType!`](/api-reference/types/share-method-type) | Yes | How recipients are identified and links are delivered: `GENERATE_URL`, `EMAIL`, `PHONE_NUMBER`, `EXISTING_USER`, or `REGISTER_USER`. | | `userCashBalanceId` | `UUID` | Yes\* | The spend account used to fund the cards. *Marked optional in the schema but required in practice — omitting it fails validation.* | | `daysUntilExpiration` | `Int` | No | Days until the link expires. Minimum 1. Defaults to the program default (30 days) if omitted. This date also becomes the card's lock/freeze date. | | `recipientListEmail` | `[String]` | Conditional | Required and non-empty when `shareMethod = EMAIL`. Length must equal `quantity`. Must be empty otherwise. | | `recipientListPhone` | `[String]` | Conditional | Required and non-empty when `shareMethod = PHONE_NUMBER`. Length must equal `quantity`. Must be empty otherwise. | | `recipientUserIds` | `[UUID]` | Conditional | Known Fluz user IDs to bind as recipients when `shareMethod = EXISTING_USER`. Length must equal `quantity`. Mutually exclusive with `recipientRegistrations`. | | `recipientRegistrations` | [`[ShareLinkRecipientRegistrationInput]`](/api-reference/types/share-link-recipient-registration-input) | Conditional | Inline pre-register payloads when `shareMethod = REGISTER_USER` — Fluz creates or reuses a placeholder user (no seat) per entry before generating links. Length must equal `quantity`. Mutually exclusive with `recipientUserIds`. | | `virtualCardId` | `UUID` | No | Existing open loop card to bind to the hosted link. When provided, claim grants access to that card instead of issuing a new one. | | `usePrepaymentBalance` | `Boolean` | No | Whether to use your Prepayment Balance as an additional funding source if `userCashBalanceId` doesn't cover the full amount at claim time. Default `false`. | | `useRewardsBalance` | `Boolean` | No | Whether to use your Fluz Rewards Balance as an additional funding source if `userCashBalanceId` doesn't cover the full amount at claim time. Default `false`. | ### Recipient identification & delivery (`shareMethod`) | Value | Behavior | Recipient field | Card created | | - | - | - | - | | `GENERATE_URL` | Fluz returns the hosted URLs in the response. You distribute them yourself. | Both lists must be empty/omitted. | At claim | | `EMAIL` | Fluz emails a link to each recipient. | `recipientListEmail` required; length must equal `quantity`. | At claim | | `PHONE_NUMBER` | Fluz texts a link to each recipient. | `recipientListPhone` required; length must equal `quantity`. | At claim | | `EXISTING_USER` | Link is bound to a known Fluz user from the start; only that user can claim it. | `recipientUserIds` required; length must equal `quantity`. | **Immediately**, at generation time | | `REGISTER_USER` | Fluz creates or reuses a placeholder user per entry, then binds the link to that user, same as `EXISTING_USER`. | `recipientRegistrations` required; length must equal `quantity`. | **Immediately**, at generation time | ### Example ```json Generate URLs theme={null} { "input": { "cardLimit": 25, "offerId": "11111111-2222-3333-4444-555555555555", "daysUntilExpiration": 30, "quantity": 3, "shareMethod": "GENERATE_URL", "userCashBalanceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" } } ``` ## Returns [`ShareRequest`](/api-reference/types/share-request) — ShareRequest represents a response from creating a share link. | Field | Type | Description | | - | - | - | | `shareLinks` | `[String]` | The hosted URLs generated by this request, one per `quantity`, each of the form `https://fluz.app/virtual-prepaid-card/{share_request_id}`. | | `recipientRegistrationResults` | [`[ShareLinkRecipientRegistrationResult]`](/api-reference/types/share-link-recipient-registration-result) | Per-recipient registration outcomes, populated only when `shareMethod` is `REGISTER_USER`. | ```json theme={null} { "data": { "generateVCShareLinks": { "shareLinks": [ "https://fluz.app/virtual-prepaid-card/3f8a...c1", "https://fluz.app/virtual-prepaid-card/9b2d...77", "https://fluz.app/virtual-prepaid-card/0c41...e3" ] } } } ``` ## Errors | Cause | Result | | - | - | | Missing/invalid Bearer token or missing `CREATE_SHARE_LINK` scope | Request rejected (unauthorized). | | Recipient field (`recipientListEmail`, `recipientListPhone`, `recipientUserIds`, or `recipientRegistrations`) length ≠ `quantity` | Validation error; no records created. | | `recipientUserIds` references a non-existent Fluz user | Validation error; no records created. | | Inactive offer, non-shareable merchant, or invalid `offerId` | Validation error; no records created. | | Missing/invalid `userCashBalanceId` | Validation error; no records created. | See [Send Open Loop Cards](/features/open-loop-cards/send-open-loop-cards) for the full guide, including delivery-method walkthroughs and expiration/freeze behavior. # lockVirtualCard Source: https://docs.fluz.app/api-reference/mutations/lock-virtual-card Mutation to lock virtual cards. Mutation to lock virtual cards. Requires EDIT\_VIRTUALCARD scope. ```graphql theme={null} mutation { lockVirtualCard( input: LockVirtualCardInput! ): LockVirtualCardResponse } ``` ## Arguments **input** `LockVirtualCardInput!` *required* *No description provided in the schema yet.* ## Returns [`LockVirtualCardResponse`](/api-reference/types/lock-virtual-card-response) # postKycVerification Source: https://docs.fluz.app/api-reference/mutations/post-kyc-verification Post a decisioned KYC verification result performed by an external identity provider. Post a decisioned KYC verification result performed by an external identity provider. Fluz validates the payload, creates a verification record and updates the user's KYC status according to the result. Requires the `VERIFY_KYC` scope on a user access token. Use this mutation when your platform runs its own KYC and pushes the outcome to Fluz. To have Fluz run the verification instead, use [verifyUserInformation](/api-reference/mutations/verify-user-information) or [verifyUserPrefillInformation](/api-reference/mutations/verify-user-prefill-information). > [!WARNING] > > Send this mutation to the dedicated secure ingestion endpoint, not the standard API host. This endpoint securely tokenizes PII data like `person.ssn` in transit. A request whose SSN or other PII data arrive untokenized is rejected. Always pass the arguments as GraphQL **variables**; values inlined into the query document cannot be tokenized. > > * Staging: `https://secure.transactional-graph.staging.fluzapp.com/api/v1/graphql` > * Production: provided during onboarding ```graphql theme={null} mutation { postKycVerification( schemaVersion: String! externalVerificationProvider: String! externalVerificationId: String! decision: String! decisionReason: String decisionedAt: String person: JSON! verifications: JSON! externalProviderData: JSON ): PostKycVerificationResponse } ``` ## Key behaviors * **Idempotency.** Submissions are idempotent on `externalVerificationId`: replaying the same identifier returns the original result and writes nothing. A re-decisioned verification must be submitted with a new identifier. * **Only decisioned results.** `decision` must be `PASSED` or `FAILED` — do not send pending or undecisioned verifications. * **KYC status transitions.** A `PASSED` result moves an unverified user to `PASS` (US address) or `PASS_INTERNATIONAL` (non-US address). If the user is already verified, the verification is still recorded for audit but the status is never changed — the response message notes that the existing status was preserved. A `FAILED` result is recorded and leaves the status untouched. * **Photo URLs** must be HTTPS and remain fetchable for at least 24 hours after delivery — Fluz fetches and stores the images. * **Status update webhook.** When the submission changes the user's KYC status, Fluz publishes a `KYC_STATUS_UPDATE` event to your app's webhook. Your webhook configuration must include the `KYC_STATUS_UPDATE` event (it requires the `VERIFY_KYC` scope) to receive it, and no event fires when the status is preserved — see [Webhooks](/fluz-dashboard/webhooks). ## Arguments **schemaVersion** `String!` *required* Version of the payload contract. Currently `"1.0"`. **externalVerificationProvider** `String!` *required* The external KYC provider that performed the verification: `IDOLOGY`, `OSCILAR`, `PERSONA`, or `CUSTOM` for a provider not listed. **externalVerificationId** `String!` *required* The provider's globally unique identifier for this verification attempt (for example a Persona inquiry id). This is the idempotency key — one attempt, one id, forever. **decision** `String!` *required* The final decision for this verification attempt: `PASSED` or `FAILED`. **decisionReason** `String` Human-readable reason or rule that produced the decision; recorded on the KYC status log when present. **decisionedAt** `String` When the decision was made, as an ISO 8601 / RFC 3339 timestamp (UTC preferred). Defaults to the time of ingestion. **person** `JSON!` *required* The verified identity, exactly as established by the provider (not raw user input). **person fields** **firstName** `String` *required* The person's first name. **middleName** `String` The person's middle name. **lastName** `String` *required* The person's last name. **dateOfBirth** `String` *required* RFC 3339 full date (`YYYY-MM-DD`). **ssn** `String` Full US SSN, digits with optional dashes. Required together with `ssnLast4` when `verifications.ssn` is present. Sent in clear over TLS to the secure endpoint, where it is tokenized in transit — it never reaches Fluz systems and is never stored in clear. Never send a masked or partial value. **ssnLast4** `String` The last four digits of the SSN. Required whenever `ssn` is provided and must equal its last four digits — a mismatch is rejected. **phoneNumber** `String` E.164 format. **emailAddress** `String` The person's email address. **address** `Object` *required* The person's verified legal address: `streetLine1` (no PO Boxes), optional `streetLine2`, `city`, `subdivision` (state/province code, required for US addresses), `postalCode` (for US, the 5-digit ZIP), and `countryCode` (ISO 3166-1 alpha-2 — drives `PASS` vs `PASS_INTERNATIONAL`). **personData** `JSON` Open object for any additional provider data about the person. **verifications** `JSON!` *required* The provider verifications backing the decision. At least one of `document`, `ssn`, or `database` is required. Presence is semantic: a `ssn` section means the SSN was validated against an authoritative source; `person.ssn` without a `ssn` section means the SSN was merely collected. **verifications fields** **document** `Object` Government-id documentary verification: `verificationId` (provider id of this resource), `status` (`PASSED` or `FAILED`), `documentClass` (`DRIVER_LICENSE`, `PASSPORT`, or `GOVERNMENT_ID`), `documentNumber` (stored only as a hash), `issuingCountryCode` (ISO 3166-1 alpha-2), optional `issuingSubdivision`, `issueDate`, `expirationDate`, `confidenceScore` (0–100), `address` (as extracted from the document), `photos` (`front` required when present, optional `back` and `selfie` — HTTPS URLs), `checks`, and `documentData`. **ssn** `Object` SSN verification against an authoritative source: `verificationId`, `status`, `source` (for example `TIN_DATABASE`, `ECBSV`, `CREDIT_BUREAU`), optional `checks` and `ssnData`. Requires `person.ssn` and `person.ssnLast4` to carry the verified value. **database** `Object` Database verification of personal data against public or private sources, without a document or SSN: `verificationId`, `status`, optional `checks` and `databaseData`. **checks** `Array` Each section accepts a `checks` array of individual check outcomes: `name` (a stable snake\_case identifier, consistent across submissions — for example `id_expired_detection`), `status` (`PASSED`, `FAILED`, or `NOT_APPLICABLE`), and optional `reasons`. Fluz records all outcomes for audit and records `FAILED` check names as decline codes. **externalProviderData** `JSON` Freeform provider context for audit (template ids, partner reference ids, original provider identifiers). Must not contain clear PII beyond what the typed fields already carry. ## Returns [`PostKycVerificationResponse`](/api-reference/types/post-kyc-verification-response) — Response type for the postKycVerification mutation. ## Example ```json Variables theme={null} { "schemaVersion": "1.0", "externalVerificationProvider": "PERSONA", "externalVerificationId": "inq_gCf28LrXY9wrxDoZbnbEqTrn", "decision": "PASSED", "decisionReason": "All checks passed", "person": { "firstName": "JANE Q", "lastName": "SAMPLE", "dateOfBirth": "1990-01-01", "ssn": "900-98-7654", "ssnLast4": "7654", "address": { "streetLine1": "123 EXAMPLE STREET", "city": "SAMPLETOWN", "subdivision": "CA", "postalCode": "90001", "countryCode": "US" } }, "verifications": { "document": { "verificationId": "ver_p8qmKydzkQwyLKbaAJRvADi9", "status": "PASSED", "documentClass": "DRIVER_LICENSE", "documentNumber": "X90000000000001", "issuingCountryCode": "US", "photos": { "front": "https://files.provider.example/front.jpg" }, "checks": [{ "name": "id_expired_detection", "status": "PASSED" }] }, "ssn": { "verificationId": "ver_tin_snW2CvL2x4kTdZjbmGBjqcuV", "status": "PASSED", "source": "TIN_DATABASE" } } } ``` ```json Response theme={null} { "data": { "postKycVerification": { "status": "APPROVED", "verificationId": "4b99e8c5-4201-45fd-a5dc-1c3b88e4f6c7", "message": "User verification successful" } } } ``` # purchaseGiftCard Source: https://docs.fluz.app/api-reference/mutations/purchase-gift-card Initiates a gift card purchase transaction. Initiates a gift card purchase transaction. Requires PURCHASE\_GIFTCARD scope. The offerId or merchantSlug fields cannot both be empty. If both are provided, offerId will be used. ```graphql theme={null} mutation { purchaseGiftCard( input: PurchaseGiftCardInput! ): UserPurchase } ``` ## Arguments **input** `PurchaseGiftCardInput!` *required* *No description provided in the schema yet.* ## Returns [`UserPurchase`](/api-reference/types/user-purchase) — UserPurchase represents a record of a purchase made by a user, detailing the payment methods and other relevant information. # reassignSupervisor Source: https://docs.fluz.app/api-reference/mutations/reassign-supervisor *No description provided in the schema yet.* ```graphql theme={null} mutation { reassignSupervisor( superviseeUserId: UUID! supervisorUserId: UUID! ): SupervisionAssignmentResponse! } ``` ## Arguments **superviseeUserId** `UUID!` *required* *No description provided in the schema yet.* **supervisorUserId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`SupervisionAssignmentResponse!`](/api-reference/types/supervision-assignment-response) # redeemFluzGiftCard Source: https://docs.fluz.app/api-reference/mutations/redeem-fluz-gift-card Mutation for redeeming a Fluz Gift Card by code. Mutation for redeeming a Fluz Gift Card by code. Credits the authenticated user's gift card balance. Requires MAKE\_DEPOSIT scope. ```graphql theme={null} mutation { redeemFluzGiftCard( input: RedeemFluzGiftCardInput! ): DepositCashBalanceResponse! } ``` ## Arguments **input** `RedeemFluzGiftCardInput!` *required* *No description provided in the schema yet.* ## Returns [`DepositCashBalanceResponse!`](/api-reference/types/deposit-cash-balance-response) — Response type for the depositCashBalance mutation, returning the resulting deposits and current user balances. # refreshPlaidBankAccountBalance Source: https://docs.fluz.app/api-reference/mutations/refresh-plaid-bank-account-balance Requests a rate-limited realtime balance refresh for one owned Plaid bank account. Requests a rate-limited realtime balance refresh for one owned Plaid bank account. Requires MANAGE\_PAYMENT scope. Identity-service enforces 1 refresh per hour and 6 refreshes per day per Plaid institution. ```graphql theme={null} mutation { refreshPlaidBankAccountBalance( input: PlaidBankAccountInput! ): PlaidBankAccountBalanceRefreshResponse! } ``` ## Arguments **input** `PlaidBankAccountInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidBankAccountBalanceRefreshResponse!`](/api-reference/types/plaid-bank-account-balance-refresh-response) — Result of a rate-limited realtime refresh for one Plaid bank account's owning institution. # refreshPlaidBankConnections Source: https://docs.fluz.app/api-reference/mutations/refresh-plaid-bank-connections Requests a cached data refresh for all connected Plaid institutions owned by the authenticated account. Requests a cached data refresh for all connected Plaid institutions owned by the authenticated account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { refreshPlaidBankConnections: PlaidBankConnectionRefreshResponse! } ``` ## Returns [`PlaidBankConnectionRefreshResponse!`](/api-reference/types/plaid-bank-connection-refresh-response) — Result of a cached refresh for all Plaid connections owned by the authenticated account. # refreshUserAccessToken Source: https://docs.fluz.app/api-reference/mutations/refresh-user-access-token Refresh a user token using 'Basic '. Refresh a user token using 'Basic \'. The token will be associated with the user\_id and will have the specified scopes. ```graphql theme={null} mutation { refreshUserAccessToken( refreshToken: String! ): RefreshUserAccessTokenResponse } ``` ## Arguments **refreshToken** `String!` *required* The refreshToken issued to get a new access token. ## Returns [`RefreshUserAccessTokenResponse`](/api-reference/types/refresh-user-access-token-response) — Represents the response returned from the generateUserAccessToken mutation. # registerBusiness Source: https://docs.fluz.app/api-reference/mutations/register-business Register a new business. Register a new business. Creates business entity, signup form, and initiates KYB process. When the applicant is an authorized signer (less than 25% ownership and not the control person), upload the authorization document first via the REST endpoint POST /api/v1/file-upload/business-application-document and pass its URL as `authorizedSignerDocumentUrl`. The sole-proprietorship document is optional. ```graphql theme={null} mutation { registerBusiness( input: RegisterBusinessInput! ): RegisterBusinessResponse! } ``` ## Arguments **input** `RegisterBusinessInput!` *required* *No description provided in the schema yet.* ## Returns [`RegisterBusinessResponse!`](/api-reference/types/register-business-response) — Response from business registration. # registerPlaidIntegration Source: https://docs.fluz.app/api-reference/mutations/register-plaid-integration Registers the application's own Plaid credentials. Registers the application's own Plaid credentials. Only the developer of the application can call it. Requires the `BYOI_PLAID` scope. See [BYOI Plaid](/features/byoi-plaid). ```graphql theme={null} mutation { registerPlaidIntegration( input: RegisterPlaidIntegrationInput! ): PlaidIntegrationResponse! } ``` ## Arguments **input** `RegisterPlaidIntegrationInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidIntegrationResponse!`](/api-reference/types/plaid-integration-response) # registerUser Source: https://docs.fluz.app/api-reference/mutations/register-user Registration of a user. Registration of a user. Requires special permission from Fluz to enable. Please contact support to enable this feature if needed. Fluz sets one of two registration modes for your application: * **Direct registration**, the default, creates the account at once and returns `userId`, `accountId`, and `billingAddressId`. It grants your application nothing; the user still has to complete the OAuth grant. * **Consent mode**, which Fluz enables per application, creates no user, account, or grant when you call it. It returns an `authorizationUrl` where the person signs up or logs in with the phone number you submitted and approves your application's scopes. Fluz creates the grant when they approve. See [Consent mode](#consent-mode). Contact Fluz support to change your application's mode. ```graphql theme={null} mutation { registerUser( firstName: String! lastName: String! phoneNumber: String! regionCode: String! emailAddress: String! dateOfBirth: String billingAddress: VirtualCardBillingAddressInput acceptCardholderAgreement: Boolean externalReferenceId: String steps: [RegisterUserEnrollmentStep!] callbackUrl: String deferSeatAssignment: Boolean ): RegisterUserResponse } ``` ## Arguments | Argument | Type | Direct registration | Consent mode | Description | | - | - | - | - | - | | `firstName` | `String!` | Required | Required | The first name of user for registration. | | `lastName` | `String!` | Required | Required | The last name of user for registration. | | `phoneNumber` | `String!` | Required | Required | The phone number of user for registration. | | `regionCode` | `String!` | Required | Required | The region code of phone number for registration. | | `emailAddress` | `String!` | Required | Required | The email of user for registration. | | `dateOfBirth` | `String` | Required | Ignored | The date of birth of user for registration. | | `billingAddress` | `VirtualCardBillingAddressInput` | Required | Ignored | Billing address used for the user's virtual card. | | `acceptCardholderAgreement` | `Boolean` | Required, must be `true` | Ignored | Records the user's acceptance of the cardholder agreement. | | `externalReferenceId` | `String` | Ignored | Required | Your own ID for this person. Once the grant exists, pass it to [`generateUserAccessToken`](/api-reference/mutations/generate-user-access-token). | | `steps` | [`[RegisterUserEnrollmentStep!]`](/api-reference/types/register-user-enrollment-step) | Ignored | Optional | Setup steps the person completes after approving your application, in order: `KYC`, `FUNDING_SOURCE`, `PIN`. Defaults to none, so the flow ends at consent. | | `callbackUrl` | `String` | Ignored | Optional | An `https` URL. When the flow finishes, the person sees a **Back to app** button that opens it. Outside production, `http://localhost` is also accepted. | | `deferSeatAssignment` | `Boolean` | Optional | Ignored | When true, creates user without a rewards-network seat. Seat is assigned later at card redemption. | `dateOfBirth`, `billingAddress`, and `acceptCardholderAgreement` are nullable in the schema so that consent-mode calls can leave them out. Direct registration still requires them, but the error now comes back in `data` instead of failing GraphQL validation: a missing `dateOfBirth` or `billingAddress` returns `ARG-0002`, and a missing `acceptCardholderAgreement` returns `ARG-0001`. ## Returns [`RegisterUserResponse`](/api-reference/types/register-user-response) — represents the response returned from the registerUser mutation. | Field | Type | Description | | - | - | - | | `success` | `Boolean` | Whether registration completed. | | `userId` | `UUID` | The created user. Absent on failure. | | `accountId` | `UUID` | The created account. Absent on failure. | | `billingAddressId` | `UUID` | The billing address created for the user. Absent on failure and in consent mode. | | `status` | [`RegisterUserStatus`](/api-reference/types/register-user-status) | Consent mode only: `PENDING_CONSENT` or `LINKED`. Absent for direct registration. | | `authorizationUrl` | `String` | Consent mode only. The link to show the person. Returned with `PENDING_CONSENT`. | | `expiresAt` | `String` | Consent mode only. ISO 8601 time after which `authorizationUrl` stops working. | | `error` | [`RegisterUserError`](/api-reference/types/register-user-error) | `message` and `code` describing the failure. Absent on success. | In consent mode, `userId` and `accountId` are returned only with `LINKED`. ## Consent mode > [!NOTE] > > Fluz enables consent mode per application. Contact Fluz support to turn it on for yours. In consent mode, `registerUser` never creates a user and never checks whether the email address or phone number is already on Fluz. A person who is new to Fluz and a person who already has an account get the same response: ```json theme={null} { "data": { "registerUser": { "success": true, "status": "PENDING_CONSENT", "authorizationUrl": "https://fluz.app/embedded?token=eyJhbGciOiJIUzI1NiIs...", "expiresAt": "2026-10-06T14:03:11.000Z", "userId": null, "accountId": null, "error": null } } } ``` Show `authorizationUrl` to the person, either in the [enrollment widget](/developers/enrollment-widget) or as a link. They sign up or log in to their existing account with the phone number you submitted, which is pre-filled. Then they approve your application's scopes and complete any `steps` you listed. When they approve, Fluz creates the grant, and from then on you call [`generateUserAccessToken`](/api-reference/mutations/generate-user-access-token) with your `externalReferenceId`. If you subscribe to [`OAUTH_USER_LINKED`](/fluz-dashboard/webhooks), it usually tells you when the grant exists. It isn't sent when the person had already granted your application the same scopes, so don't rely on it alone: calling `registerUser` again returns `LINKED` once the grant exists. If the person's Fluz account is already linked to your application under a different `externalReferenceId`, approval fails unless your application allows multiple external links. Approval also fails if the person signs in with a different phone number from the one you submitted. Fluz shows them an error, creates no grant, and `registerUser` keeps returning `PENDING_CONSENT`. Submit the phone number the person uses with Fluz. > [!WARNING] > > Send `authorizationUrl` only to the person you registered, and treat it like a sign-in link. It carries the name, email address and phone number you submitted, and anyone who has it can open it. If your application already holds a grant for the `externalReferenceId`, the response is `LINKED`, with the linked user's `userId` and `accountId` and no link: ```json theme={null} { "data": { "registerUser": { "success": true, "status": "LINKED", "authorizationUrl": null, "expiresAt": null, "userId": "8f5b8a2e-1c3d-4e5f-9a7b-2c4d6e8f0a1b", "accountId": "3a1c5e7f-9b2d-4f6a-8c0e-1d3f5a7b9c2e", "error": null } } } ``` Each call returns a new link, and nothing is stored until the person approves. If a link expires or goes unused, call `registerUser` again for a new one. The consent flow doesn't collect `dateOfBirth`, `billingAddress`, or `acceptCardholderAgreement`. After the grant, add the billing address with [`addVirtualCardAddress`](/api-reference/mutations/add-virtual-card-address), and list `KYC` in `steps` if the person must verify their identity. ### Consent mode errors Nothing is created on any error. The [application and credential errors](#application-and-credential-errors) and the last `AUTH-0025` case apply here too. The conflict codes `AUTH-0026` and `AUTH-0027` are never returned in consent mode. | Code | Error name | When | Message | | - | - | - | - | | `ARG-0002` | MissingArguments | One or more of `firstName`, `lastName`, `phoneNumber`, `regionCode`, `emailAddress`, `externalReferenceId` is empty | `Missing required arguments - externalReferenceId is required.` | | `ARG-0001` | InvalidArguments | `steps` lists the same step twice | `steps must not contain duplicate values.` | | `ARG-0001` | InvalidArguments | `callbackUrl` is not an `https` URL (`http://localhost` is accepted outside production) | `callbackUrl must be an https URL.` | | `AUTH-0004` | InvalidPhoneNumber | `phoneNumber` is not a valid number for `regionCode` | `Please enter a valid phone number.` | | `WIDGET-003` | MisconfiguredApplication | Your application has no OAuth client | `This application needs an OAuth client to issue registration consent links.` | | `WIDGET-003` | MisconfiguredApplication | Your application has no type, API key, or secret for signing the link | `This application is not configured to issue registration consent links.` | `WIDGET-003` means your application's setup needs a change on Fluz's side. Contact Fluz support rather than retrying. A `steps` value other than `KYC`, `FUNDING_SOURCE`, or `PIN` fails GraphQL validation, so it comes back in the `errors` array, not in `data`. ## Errors > This mutation does not return GraphQL errors for business failures. Every failure comes back as HTTP `200` with `success: false` and a populated `error` object holding `message` and `code`. Two kinds of failure are exceptions. Authentication-middleware and rate-limit failures are rejected before the resolver runs and surface as HTTP errors. GraphQL validation failures, such as a wrong argument type or a `steps` value that is not a `RegisterUserEnrollmentStep`, come back in the GraphQL `errors` array with no `registerUser` result, so there is no `error` object to read. ```json theme={null} { "data": { "registerUser": { "success": false, "error": { "message": "The email address you chose is already in use.", "code": "AUTH-0027" } } } } ``` Branch on `error.code`, never on `error.message` — messages are prose and may be reworded, while codes are stable. The tables below list every code this mutation can return. `Error name` is the internal name for the code; it is not part of the response, and is shown only so support requests and log lines can be matched up. ### Application and credential errors Nothing was created. These apply in both modes. They indicate a problem with the calling application or its token, and will recur identically until the application setup changes. | Code | Error name | When | Message | | - | - | - | - | | `AUTH-0002` | InvalidCredentials | Called with Basic auth, or the bearer token carries no application | `User registration requires a valid user access (Bearer) token.` | | `AUTH-0030` | ApplicationInactive | No application matches the token's application id | `No application found for appId .` | | `AUTH-0030` | ApplicationInactive | The application exists but is not `ACTIVE` | `Application is not active (current status: ).` | | `AUTH-0008` | InvalidUserAccess | The bearer token belongs to a user other than the application's owning developer | `The provided bearer token does not belong to the developer that owns this application.` | | `AUTH-0022` | RegistrationNotAllowed | The application lacks the `can_register_user` capability | `This application is not permitted to register users. Please contact support to enable this capability.` | | `AUTH-0022` | RegistrationNotAllowed | The application's registration mode is not one Fluz supports | `Application has an unsupported register_user_mode. Please contact support.` | `AUTH-0022` means registration has not been enabled for your application. Contact Fluz support rather than retrying. ### Input errors Direct registration. For consent mode, see [Consent mode errors](#consent-mode-errors). Nothing was created. Fix the input and call again. | Code | Error name | When | Message | | - | - | - | - | | `ARG-0002` | MissingArguments | Direct registration: one or more of `firstName`, `lastName`, `phoneNumber`, `regionCode`, `emailAddress`, `dateOfBirth`, `billingAddress` is empty | `Missing required arguments - emailAddress, dateOfBirth are required.` | | `ARG-0001` | InvalidArguments | Direct registration: `acceptCardholderAgreement` is not `true` | `acceptCardholderAgreement must be true` | | `ARG-0001` | InvalidArguments | `billingAddress` fails schema validation | `Input validation error:
` | | `AUTH-0004` | InvalidPhoneNumber | `phoneNumber` is not a valid number for `regionCode` | `Please enter a valid phone number.` | | `AUTH-0004` | InvalidPhoneNumber | The number is valid but has no E.164 form | `Unable to format provided phone number.` | | `AUTH-0004` | InvalidPhoneNumber | `phoneNumber` could not be parsed at all | `Invalid phone number format.` | The `ARG-0002` message lists only the fields that were actually absent, and agrees in number (`is required` / `are required`). The `ARG-0001` validation message appends the specific field errors, so read `message` for the detail even though you branch on `code`. ### Conflicts Direct registration only. Nothing was created. The email address or phone number already belongs to a Fluz user. | Code | Error name | When | Message | | - | - | - | - | | `AUTH-0027` | RegistrationEmailTaken | `emailAddress` already belongs to a Fluz user | `The email address you chose is already in use.` | | `AUTH-0026` | RegistrationPhoneTaken | `phoneNumber` already belongs to a Fluz user | `The phone number you chose is already in use.` | Safe to retry with different values. Neither creates a partial user. ### Registration failures Registration could not be completed. Every case below returns the single code `AUTH-0025` (`UnsuccessfulRegistration`) — the `message` is what distinguishes them. | When | Message | | - | - | | The duplicate email/phone check could not be completed | `Unable to verify whether the email address or phone number is already in use.` | | Account creation failed | `Failed to create a new account for the user.` | | User creation failed | `Failed to create the user record.` | | Rewards-network seat setup failed | `Failed to create a seat for the new account.` | | Registration finished without producing a complete user, account, and seat | `User registration did not complete successfully. Please try again.` | | Any other unexpected failure | `Please try again again or contact support.` | `AUTH-0025` is retryable, but it does not tell you whether the user was created — some stages run after the user and account already exist. Retry the same call, and treat the outcome as the answer: * Success means registration completed and the returned `userId` is the user to use. * `AUTH-0027` or `AUTH-0026` means an earlier attempt already created the user; look it up by email address or phone number instead of registering again. * The same `AUTH-0025` message repeating means the failure is not transient. Contact Fluz support and quote the message text — it identifies the stage that failed. # removeAuthorizedUser Source: https://docs.fluz.app/api-reference/mutations/remove-authorized-user Remove an authorized user from the caller's account by setting their role assignment to INACTIVE. Remove an authorized user from the caller's account by setting their role assignment to INACTIVE. The target account is always resolved from the caller's credentials — Bearer tokens use the token's accountId; Basic (API key) callers use the application's configured operator account. Requires the MANAGE\_SUBUSERS scope. Supports both Bearer and Basic auth. The account owner (OWNER role) cannot be removed. ```graphql theme={null} mutation { removeAuthorizedUser( authUserId: UUID! ): RemoveAuthorizedUserResponse } ``` ## Arguments **authUserId** `UUID!` *required* The authorized user ID (UAC role assignment ID) to deactivate. Obtain this from the authorizedUsers query or the addAuthorizedUser mutation response. ## Returns [`RemoveAuthorizedUserResponse`](/api-reference/types/remove-authorized-user-response) — Response returned from the removeAuthorizedUser mutation. # API overview Source: https://docs.fluz.app/api-reference/overview Endpoints, request format, and how to read responses from the Fluz GraphQL API. The Fluz API is a single GraphQL endpoint per environment. This page is the reference companion to [How the GraphQL API works](/concepts/graphql). ## Browse the reference > **[Queries](/api-reference/queries/approval-requests)** > > Read operations — fetch wallets, transactions, merchants, card offers, and account data. > **[Mutations](/api-reference/mutations/add-authorized-user)** > > Write operations — issue cards, move money, register users and businesses, and manage approvals. ### Types > **[Objects](/api-reference/types/account)** > > Response shapes returned by queries and mutations. > **[Input objects](/api-reference/types/add-bank-card-input)** > > Arguments you pass to operations. > **[Enums](/api-reference/types/account-type)** > > Fixed sets of allowed values, like statuses and card networks. > **[Unions](/api-reference/types/stock-info-type)** > > Fields that resolve to one of several object types. > **[Interfaces](/api-reference/types/basic-user-cash-balance)** > > Shared field contracts implemented by multiple objects. > **[Scalars](/api-reference/types/date-time)** > > Primitive values like DateTime and UUID. ### Conventions > **[Authentication](/api-reference/authentication)** > > The Authorization header, token endpoints, and the OAuth authorization redirect. > **[Errors](/api-reference/errors)** > > The error envelope, domain code prefixes, and retry guidance. > **[Rate limits](/api-reference/rate-limits)** > > Per-surface request limits and what a 429 looks like. > **[Pagination](/api-reference/pagination)** > > Offset inputs, page-size caps, and connection responses. > **[REST endpoints](/api-reference/rest/upload-sole-proprietorship-document)** > > One operation lives outside GraphQL — the file upload used for sole proprietorship registration. ## Endpoints | Environment | GraphQL endpoint | | - | - | | Staging | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | | Live | `https://transactional-graph.fluzapp.com/api/v1/graphql` | Access tokens are minted at the same endpoint via the `generateUserAccessToken` mutation, authorized with your API Key — see [Authentication](/api-reference/authentication). ## Headers | Header | Required | Notes | | - | - | - | | `Authorization` | Yes | `Bearer ` (`Basic ` when calling `generateUserAccessToken`) | | `Content-Type` | Yes | `application/json` | ## Response envelope ```json theme={null} { "data": { ... }, "errors": [ ... ] } ``` * `data` — the successful payload (may be partial on error). * `errors` — array of failures, each with `code`, `message`, and `path`. ## Schema Staging exposes introspection so you can point tooling (Apollo Studio, GraphiQL, codegen) at it directly. In live, request the current SDL from your account team. # approvalRequests Source: https://docs.fluz.app/api-reference/queries/approval-requests Lists open approval requests for the caller's account. Lists open approval requests for the caller's account. ```graphql theme={null} query { approvalRequests: [ApprovalRequest!]! } ``` ## Returns [`[ApprovalRequest!]!`](/api-reference/types/approval-request) — An open approval request for the caller's account. # authorizedUsers Source: https://docs.fluz.app/api-reference/queries/authorized-users List authorized users on the caller's account. List authorized users on the caller's account. All filters are optional and narrow the results by email and/or phone. The target account is always resolved from the caller's credentials - Bearer tokens use the token's accountId; Basic (API key) callers use the application's configured operator account. OWNER role assignments are excluded from results. Requires the VIEW\_SUBUSERS scope. Supports both Bearer and Basic auth. ```graphql theme={null} query { authorizedUsers( email: String phone: String ): [AuthorizedUser] } ``` ## Arguments **email** `String` Filter by the email address of the authorized user. **phone** `String` Filter by the phone number of the authorized user. ## Returns [`[AuthorizedUser]`](/api-reference/types/authorized-user) — Represents an authorized user on an account. # erpChartOfAccounts Source: https://docs.fluz.app/api-reference/queries/erp-chart-of-accounts The imported chart of accounts for the account's connected ERP provider. The imported chart of accounts for the account's connected ERP provider. ```graphql theme={null} query { erpChartOfAccounts( filter: ErpReferenceItemFilter paginate: OffsetInput ): ErpReferenceItemConnection! } ``` ## Arguments **filter** `ErpReferenceItemFilter` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`ErpReferenceItemConnection!`](/api-reference/types/erp-reference-item-connection) # erpCustomers Source: https://docs.fluz.app/api-reference/queries/erp-customers The imported customers for the account's connected ERP provider. The imported customers for the account's connected ERP provider. ```graphql theme={null} query { erpCustomers( filter: ErpReferenceItemFilter paginate: OffsetInput ): ErpReferenceItemConnection! } ``` ## Arguments **filter** `ErpReferenceItemFilter` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`ErpReferenceItemConnection!`](/api-reference/types/erp-reference-item-connection) # erpTransactionMetadata Source: https://docs.fluz.app/api-reference/queries/erp-transaction-metadata The ERP metadata for a single transaction. The ERP metadata for a single transaction. Null when the account has no active ERP connection or when this transaction has no ERP metadata yet. ```graphql theme={null} query { erpTransactionMetadata( transactionRecordId: UUID! ): ErpTransactionMetadata } ``` ## Arguments **transactionRecordId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`ErpTransactionMetadata`](/api-reference/types/erp-transaction-metadata) — The current ERP-side categorization state of a transaction. # erpTransactionMetadataList Source: https://docs.fluz.app/api-reference/queries/erp-transaction-metadata-list ERP metadata across transactions, filterable by sync status / last-updated time. ERP metadata across transactions, filterable by sync status / last-updated time. ```graphql theme={null} query { erpTransactionMetadataList( filter: ErpTransactionMetadataFilter paginate: OffsetInput ): ErpTransactionMetadataConnection! } ``` ## Arguments **filter** `ErpTransactionMetadataFilter` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`ErpTransactionMetadataConnection!`](/api-reference/types/erp-transaction-metadata-connection) # erpVendors Source: https://docs.fluz.app/api-reference/queries/erp-vendors The imported vendors for the account's connected ERP provider. The imported vendors for the account's connected ERP provider. ```graphql theme={null} query { erpVendors( filter: ErpReferenceItemFilter paginate: OffsetInput ): ErpReferenceItemConnection! } ``` ## Arguments **filter** `ErpReferenceItemFilter` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`ErpReferenceItemConnection!`](/api-reference/types/erp-reference-item-connection) # getAccountsByUserId Source: https://docs.fluz.app/api-reference/queries/get-accounts-by-user-id Get all the accounts information for the User using 'Basic '. Get all the accounts information for the User using 'Basic \'. ```graphql theme={null} query { getAccountsByUserId( userId: UUID! ): [Account] } ``` ## Arguments **userId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`[Account]`](/api-reference/types/account) — Represents the account operable by the application. # getApplicationScopes Source: https://docs.fluz.app/api-reference/queries/get-application-scopes Get the allowed scopes of the application using 'Basic '. Get the allowed scopes of the application using 'Basic \'. ```graphql theme={null} query { getApplicationScopes: [ScopeType] } ``` ## Returns [`[ScopeType]`](/api-reference/types/scope-type) — Enum describing the various types of access scopes available within the system. # getApplicationUsers Source: https://docs.fluz.app/api-reference/queries/get-application-users Get the users who have granted scopes to the application using 'Basic '. Get the users who have granted scopes to the application using 'Basic \'. ```graphql theme={null} query { getApplicationUsers( paginate: OffsetInput ): [ApplicationUser] } ``` ## Arguments **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`[ApplicationUser]`](/api-reference/types/application-user) — Represents the User who grants access to the application with scopes. # getBulkBalances Source: https://docs.fluz.app/api-reference/queries/get-bulk-balances Get cash balances across up to 100 connected users in one call, using 'Basic '. Get cash balances across up to 100 connected users in one call, using 'Basic \'. Requires the bulk API capability on your application, and the LIST\_PAYMENT scope on each target user's grant. Failures are reported per target and never fail the whole request. ```graphql theme={null} query { getBulkBalances( targetSpec: BulkTargetSpecInput! ): BulkBalances } ``` ## Arguments **targetSpec** `BulkTargetSpecInput!` *required* *No description provided in the schema yet.* ## Returns [`BulkBalances`](/api-reference/types/bulk-balances) — Balances across your connected users, one result per target. # getBulkConnectedOAuthUsers Source: https://docs.fluz.app/api-reference/queries/get-bulk-connected-oauth-users List the users connected to your application through active OAuth grants, using 'Basic '. List the users connected to your application through active OAuth grants, using 'Basic \'. Requires the bulk API capability on your application. Returns identifiers and granted scopes only — never tokens. ```graphql theme={null} query { getBulkConnectedOAuthUsers( paginate: BulkPaginationInput ): BulkConnectedOAuthUsers } ``` ## Arguments **paginate** `BulkPaginationInput` *No description provided in the schema yet.* ## Returns [`BulkConnectedOAuthUsers`](/api-reference/types/bulk-connected-oauth-users) — A page of users connected to your application. # getBulkOperationItems Source: https://docs.fluz.app/api-reference/queries/get-bulk-operation-items Get the per-target results of one of your bulk jobs, using 'Basic '. Get the per-target results of one of your bulk jobs, using 'Basic \'. Cursor-paginated and filterable by item status — filter to FAILED to reconcile just the failures, or read resultResourceId to map a succeeded write back to its Fluz resource. totalCount is the job's unfiltered item total, not a count of the filtered page. Returns not-found for a job id that is unknown or belongs to another application. ```graphql theme={null} query { getBulkOperationItems( bulkJobId: UUID! status: BulkItemStatus limit: Int after: String ): BulkOperationItems } ``` ## Arguments **bulkJobId** `UUID!` *required* *No description provided in the schema yet.* **status** `BulkItemStatus` *No description provided in the schema yet.* **limit** `Int` Maximum items per page. Maximum and default are 100. **after** `String` Opaque cursor from a previous page's nextCursor. Omit for the first page. ## Returns [`BulkOperationItems`](/api-reference/types/bulk-operation-items) — A page of per-target results for a bulk job. # getBulkOperationJob Source: https://docs.fluz.app/api-reference/queries/get-bulk-operation-job Get the status and counters of one of your bulk jobs, using 'Basic '. Get the status and counters of one of your bulk jobs, using 'Basic \'. Reads the job row only (no per-item scan) — poll this to follow progress. Returns not-found for a job id that is unknown or belongs to another application. ```graphql theme={null} query { getBulkOperationJob( bulkJobId: UUID! ): BulkJob } ``` ## Arguments **bulkJobId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`BulkJob`](/api-reference/types/bulk-job) — A submitted bulk job. # getBulkTransactions Source: https://docs.fluz.app/api-reference/queries/get-bulk-transactions Get transactions across up to 1,000 connected users in one call, using 'Basic '. Get transactions across up to 1,000 connected users in one call, using 'Basic \'. Requires the bulk API capability on your application, and the LIST\_PAYMENT and LIST\_PURCHASES scopes on each target user's grant. Returns a flat, keyset-paginated list (newest first) with no per-user cap — page with nextCursor/after to retrieve every transaction in the window across every target user. The window defaults to the last 90 days and may span at most 365 days. Failures are reported per target (first page) and never fail the whole request. For more than 1,000 users, or a window wider than a year, use the asynchronous transactions export. ```graphql theme={null} query { getBulkTransactions( targetSpec: BulkTargetSpecInput! createdGte: DateTime createdLte: DateTime includeMetadata: Boolean limit: Int after: String includeTotalCount: Boolean ): BulkTransactions } ``` ## Arguments **targetSpec** `BulkTargetSpecInput!` *required* *No description provided in the schema yet.* **createdGte** `DateTime` Only include transactions created at or after this time. Defaults to 90 days before createdLte. The window may span at most 365 days. **createdLte** `DateTime` Only include transactions created at or before this time. Defaults to now. **includeMetadata** `Boolean` Also return memo and category metadata for each transaction. Slower; defaults to false. **limit** `Int` Maximum transactions to return in this page. Default and maximum are both 100; larger values are clamped rather than rejected. **after** `String` Opaque cursor from a previous response's nextCursor, to fetch the next page. Omit for the first page. **includeTotalCount** `Boolean` Also return totalCount, the number of transactions matching the request across all pages. Costs a second full scan of the window, so it is off by default; it is only ever computed on the first page. Prefer hasMore/nextCursor to drive paging. ## Returns [`BulkTransactions`](/api-reference/types/bulk-transactions) — Transactions across your connected users as a single flat, keyset-paginated list, newest first. # getBusiness Source: https://docs.fluz.app/api-reference/queries/get-business Returns KYB status and owner verification snapshots for the business linked to the token's accountId. Returns KYB status and owner verification snapshots for the business linked to the token's accountId. ```graphql theme={null} query { getBusiness: GetBusinessResponse! } ``` ## Returns [`GetBusinessResponse!`](/api-reference/types/get-business-response) — KYB summary for the business tied to the bearer token's accountId. # getBusinessCategories Source: https://docs.fluz.app/api-reference/queries/get-business-categories Query for listing all active business categories with their sub-categories. Query for listing all active business categories with their sub-categories. ```graphql theme={null} query { getBusinessCategories: [BusinessCategory!]! } ``` ## Returns [`[BusinessCategory!]!`](/api-reference/types/business-category) — BusinessCategory represents a business category with its associated sub-categories. # getCardProvisioningUrl Source: https://docs.fluz.app/api-reference/queries/get-card-provisioning-url Returns a short URL that, when opened on the end-user's mobile device, launches the Fluz App Clip (iOS) or Fluz app (Android) and adds the specified virtual card to Apple Pay / Google Pay. Returns a short URL that, when opened on the end-user's mobile device, launches the Fluz App Clip (iOS) or Fluz app (Android) and adds the specified virtual card to Apple Pay / Google Pay. Typical flow: 1. Developer creates a virtual card for a user via `createVirtualCard` and keeps the returned `virtualCardId`. 2. Developer calls `getCardProvisioningUrl` with that `virtualCardId`. 3. Developer surfaces the returned URL to the user (QR code, SMS, email, in-app button — anything that ends up on the user's mobile device). 4. User opens the URL within \`expiresAt\`. The App Clip / app handles the wallet provisioning. Each URL targets exactly one card. To provision several cards, call this once per card with each card's `virtualCardId`. `offerId` remains supported for the single-card case. It resolves only while the account holds at most one active card on that offer; beyond that it cannot identify a card and the query fails rather than picking one. Each call returns a fresh URL. The URL is opaque — it carries no credentials in plaintext; user identity is exchanged server-side via a short-lived lookup record (5-minute TTL). Treat the URL as a bearer secret: claiming it does not immediately invalidate it, so it stays redeemable for a short grace window after first use. Deliver it over a private channel and don't log or cache it. Requires the \`CREATE\_VIRTUALCARD\` scope. Possible errors: * `Arguments.INVALID` — id is not a valid UUID, or both/neither selector was supplied. * `VirtualCard.CARD_NOT_FOUND` — no such card on the caller's account. * `VirtualCard.CARD_NOT_ACTIVE` — the card exists but is not ACTIVE. * `VirtualCard.AMBIGUOUS_CARD_SELECTION` — offerId matches several active cards; pass virtualCardId. * `VirtualCard.OFFER_NOT_FOUND` — the card's offer doesn't exist or is inactive. * `VirtualCard.OFFER_NOT_ACCESSIBLE` — account lacks campaign access for the card's offer. * \`VirtualCard.OFFER\_NOT\_TOKENIZATION\_ELIGIBLE\` — offer's card program does not support wallet provisioning. * \`Auth.USER\_REVOKED\_DEVELOPER\_ACCESS\` — user has revoked OAuth access for this developer application. * \`Auth.INVALID\_SCOPE\` — caller's token does not have CREATE\_VIRTUALCARD. * \`Generic.EXTERNAL\_SERVICE\_ERROR\` — transient downstream failure; retry is safe. ```graphql theme={null} query { getCardProvisioningUrl( input: GetCardProvisioningUrlInput! ): CardProvisioningUrl! } ``` ## Arguments **input** `GetCardProvisioningUrlInput!` *required* *No description provided in the schema yet.* ## Returns [`CardProvisioningUrl!`](/api-reference/types/card-provisioning-url) # getDeclinedTransactions Source: https://docs.fluz.app/api-reference/queries/get-declined-transactions Retrieves paginated declined transaction history for the authenticated user's account. Retrieves paginated declined transaction history for the authenticated user's account. Supports comprehensive filtering and pagination. Requires LIST\_PAYMENT and LIST\_PURCHASES scope. ```graphql theme={null} query { getDeclinedTransactions( filter: DeclinedTransactionFilterInput paginate: OffsetInput ): DeclinedTransactionConnection! } ``` ## Arguments **filter** `DeclinedTransactionFilterInput` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`DeclinedTransactionConnection!`](/api-reference/types/declined-transaction-connection) — Paginated response for getDeclinedTransactions query. # getDefaultFundingSource Source: https://docs.fluz.app/api-reference/queries/get-default-funding-source getDefaultFundingSource returns the account's current primary (bank account) and backup (bank card) funding sources. getDefaultFundingSource returns the account's current primary (bank account) and backup (bank card) funding sources. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getDefaultFundingSource: DefaultFundingSource } ``` ## Returns [`DefaultFundingSource`](/api-reference/types/default-funding-source) — DefaultFundingSource represents an account's default funding source: the primary payment method (a bank account, charged first) and the backup payment method (a bank card, charged if the primary can't be charged). # getGiftCards Source: https://docs.fluz.app/api-reference/queries/get-gift-cards Retrieves the user's gift cards. Retrieves the user's gift cards. Requires LIST\_PURCHASES scope. ```graphql theme={null} query { getGiftCards( status: [GiftCardStatus] userCashBalanceId: UUID paginate: OffsetInput ): [GiftCard] } ``` ## Arguments **status** `[GiftCardStatus]` *No description provided in the schema yet.* **userCashBalanceId** `UUID` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`[GiftCard]`](/api-reference/types/gift-card) — GiftCard represents a record of a gift card purchased by a user. # getMccList Source: https://docs.fluz.app/api-reference/queries/get-mcc-list Query for listing merchant category codes. Query for listing merchant category codes. Requires MAKE\_DEPOSIT scope. ```graphql theme={null} query { getMccList( paginate: OffsetInput ): [MerchantCategoryCode] } ``` ## Arguments **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`[MerchantCategoryCode]`](/api-reference/types/merchant-category-code) — MerchantCategoryCode represents the classifier for a business by the types of goods or services it provides. # getMerchantCategories Source: https://docs.fluz.app/api-reference/queries/get-merchant-categories Get the merchant categories. Get the merchant categories (e.g. Dining, Travel), ordered by display priority. Use a category's id with `getMerchants(categoryId:)` to list its merchants. Requires LIST\_OFFERS scope. ```graphql theme={null} query { getMerchantCategories: [MerchantCategory!]! } ``` ## Returns [`[MerchantCategory!]!`](/api-reference/types/merchant-category) — MerchantCategory is a curated shopping category (e.g. Dining, Travel) that groups merchants. ## Example ```graphql theme={null} query GetMerchantCategories { getMerchantCategories { categoryId name } } ``` ```json theme={null} { "data": { "getMerchantCategories": [ { "categoryId": "223e4567-e89b-12d3-a456-426614174001", "name": "Dining" }, { "categoryId": "323e4567-e89b-12d3-a456-426614174002", "name": "Travel" } ] } } ``` The list is the same taxonomy shown in the Fluz catalog, ordered by display priority. Category ids are stable, so store the id rather than the name if you cache the mapping — a category can be renamed. # getMerchants Source: https://docs.fluz.app/api-reference/queries/get-merchants Get the active merchant catalog. Get the active merchant catalog. Requires LIST\_OFFERS scope. ```graphql theme={null} query { getMerchants( name: String paginate: OffsetInput offerTypes: OfferTypesInput filterBy: FilterByInput categoryId: UUID ): [Merchant]! } ``` ## Arguments **name** `String` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* **offerTypes** `OfferTypesInput` *No description provided in the schema yet.* **filterBy** `FilterByInput` Apply fine-grained filters to the offers. Example: \{ deliveryFormat: URL } will only return merchants that have gift card offers with the URL delivery format. **categoryId** `UUID` Only return merchants in this category. Category ids come from [`getMerchantCategories`](/api-reference/queries/get-merchant-categories). An unknown or inactive category returns an empty list. A non-UUID value returns `APPLICATIONS-0012` (`InvalidArgument`), except a blank string, which removes only the category filter and leaves the other arguments in effect. ## Returns [`[Merchant]!`](/api-reference/types/merchant) — Merchant is place that a Gift Card can be use, or can charge a user's VirtualCard. # getOfferQuote Source: https://docs.fluz.app/api-reference/queries/get-offer-quote Get the best offer for merchant based on matching arguments. Get the best offer for merchant based on matching arguments. Requires LIST\_OFFERS scope. This requires Fluz to confirm the inventory, response time varies by vendors. ```graphql theme={null} query { getOfferQuote( input: GetOfferQuoteInput! ): Offer } ``` ## Arguments **input** `GetOfferQuoteInput!` *required* *No description provided in the schema yet.* ## Returns [`Offer`](/api-reference/types/offer) — Offer is a discounted offer for a merchant. # getPlaidBankAccountBalance Source: https://docs.fluz.app/api-reference/queries/get-plaid-bank-account-balance Gets the latest stored safe Plaid balance for one owned bank account. Gets the latest stored safe Plaid balance for one owned bank account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} query { getPlaidBankAccountBalance( input: PlaidBankAccountInput! ): PlaidBankBalance } ``` ## Arguments **input** `PlaidBankAccountInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidBankBalance`](/api-reference/types/plaid-bank-balance) — Latest safe Plaid bank balance for a persisted bank account. # getPlaidBankAccountSpendPower Source: https://docs.fluz.app/api-reference/queries/get-plaid-bank-account-spend-power Gets the latest safe spend power for one owned Plaid bank account. Gets the latest safe spend power for one owned Plaid bank account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} query { getPlaidBankAccountSpendPower( input: PlaidBankAccountInput! ): PlaidBankAccountSpendPower } ``` ## Arguments **input** `PlaidBankAccountInput!` *required* *No description provided in the schema yet.* ## Returns [`PlaidBankAccountSpendPower`](/api-reference/types/plaid-bank-account-spend-power) — Safe spend power calculated for one Plaid-linked bank account. # getPlaidBankAccounts Source: https://docs.fluz.app/api-reference/queries/get-plaid-bank-accounts Lists safe Plaid bank accounts owned by the authenticated account. Lists safe Plaid bank accounts owned by the authenticated account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} query { getPlaidBankAccounts( input: PlaidBankAccountFilterInput ): [PlaidBankAccount!]! } ``` ## Arguments **input** `PlaidBankAccountFilterInput` *No description provided in the schema yet.* ## Returns [`[PlaidBankAccount!]!`](/api-reference/types/plaid-bank-account) — Safe persisted Plaid bank account metadata. # getPlaidBankBalances Source: https://docs.fluz.app/api-reference/queries/get-plaid-bank-balances Lists latest stored safe Plaid balances owned by the authenticated account. Lists latest stored safe Plaid balances owned by the authenticated account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} query { getPlaidBankBalances( input: PlaidBankBalanceFilterInput ): [PlaidBankBalance!]! } ``` ## Arguments **input** `PlaidBankBalanceFilterInput` *No description provided in the schema yet.* ## Returns [`[PlaidBankBalance!]!`](/api-reference/types/plaid-bank-balance) — Latest safe Plaid bank balance for a persisted bank account. # getPlaidBankTransactions Source: https://docs.fluz.app/api-reference/queries/get-plaid-bank-transactions Lists historical bank transactions populated by identity-service from Plaid. Lists historical bank transactions populated by identity-service from Plaid. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} query { getPlaidBankTransactions( input: PlaidBankTransactionFilterInput ): PlaidBankTransactionPage! } ``` ## Arguments **input** `PlaidBankTransactionFilterInput` *No description provided in the schema yet.* ## Returns [`PlaidBankTransactionPage!`](/api-reference/types/plaid-bank-transaction-page) # getPlaidIntegration Source: https://docs.fluz.app/api-reference/queries/get-plaid-integration Returns the application's Plaid integration, or null when none was registered. Returns the application's Plaid integration, or null when none was registered. Requires the `BYOI_PLAID` scope. See [BYOI Plaid](/features/byoi-plaid). ```graphql theme={null} query { getPlaidIntegration: PlaidIntegration } ``` ## Returns [`PlaidIntegration`](/api-reference/types/plaid-integration) - The application's own Plaid credentials. # getReferralUrl Source: https://docs.fluz.app/api-reference/queries/get-referral-url Get the referral url for a merchant. Get the referral url for a merchant. ```graphql theme={null} query { getReferralUrl( merchant: MerchantInput ): String } ``` ## Arguments **merchant** `MerchantInput` *No description provided in the schema yet.* ## Returns `String` — The `String` scalar type represents textual data, represented as UTF-8 character sequences. # getSpendAccountPaycheckDepositForm Source: https://docs.fluz.app/api-reference/queries/get-spend-account-paycheck-deposit-form Retrieves a PDF paycheck direct-deposit form for a virtual account number belonging to a spend account (cash balance) of the authenticated user's account. Retrieves a PDF paycheck direct-deposit form for a virtual account number belonging to a spend account (cash balance) of the authenticated user's account. Defaults to the spend account's primary virtual account number when virtualAccountNumberId is omitted. depositAmount is required when depositType is FIXED. depositPercentage (1-100) is required when depositType is PERCENTAGE. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getSpendAccountPaycheckDepositForm( userCashBalanceId: UUID! virtualAccountNumberId: UUID depositType: PaycheckDepositType! depositAmount: Float depositPercentage: Float employerName: String employeeName: String eSignForm: Boolean ): SpendAccountPdfDocument! } ``` ## Arguments **userCashBalanceId** `UUID!` *required* The spend account (cash balance) the virtual account number belongs to. **virtualAccountNumberId** `UUID` Which of the spend account's virtual account numbers to document. Defaults to the primary one. **depositType** `PaycheckDepositType!` *required* How much of each paycheck to deposit: the entire check, a fixed amount, or a percentage. **depositAmount** `Float` Dollar amount to deposit each paycheck. Required when depositType is FIXED, ignored otherwise. **depositPercentage** `Float` Percentage (1-100) of each paycheck to deposit. Required when depositType is PERCENTAGE, ignored otherwise. **employerName** `String` Employer name to print on the form's employer line, 1-150 characters. Left blank when omitted. **employeeName** `String` Name to print as the account holder on the form, 1-150 characters. Defaults to the account's legal name, with any DBA name on a second line. **eSignForm** `Boolean` When true, pre-fills the signature line with the printed account-holder name in italics. Defaults to false, which leaves the signature line blank to be signed by hand. ## Returns [`SpendAccountPdfDocument!`](/api-reference/types/spend-account-pdf-document) — A PDF artifact containing ACH routing and account instructions for funding a spend account's virtual account number, returned as a base64-encoded string for the client to decode and render or download. # getSpendAccountPaymentInstructions Source: https://docs.fluz.app/api-reference/queries/get-spend-account-payment-instructions Retrieves a PDF payment instructions artifact (ACH routing + account details) for funding a virtual account number belonging to a spend account (cash balance) of the authenticated user's account. Retrieves a PDF payment instructions artifact (ACH routing + account details) for funding a virtual account number belonging to a spend account (cash balance) of the authenticated user's account. Defaults to the spend account's primary virtual account number when virtualAccountNumberId is omitted. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getSpendAccountPaymentInstructions( userCashBalanceId: UUID! virtualAccountNumberId: UUID ): SpendAccountPdfDocument! } ``` ## Arguments **userCashBalanceId** `UUID!` *required* The spend account (cash balance) the virtual account number belongs to. **virtualAccountNumberId** `UUID` Which of the spend account's virtual account numbers to document. Defaults to the primary one. ## Returns [`SpendAccountPdfDocument!`](/api-reference/types/spend-account-pdf-document) — A PDF artifact containing ACH routing and account instructions for funding a spend account's virtual account number, returned as a base64-encoded string for the client to decode and render or download. # getSpendAccountStatusLetter Source: https://docs.fluz.app/api-reference/queries/get-spend-account-status-letter Retrieves a PDF account status letter for a virtual account number belonging to a spend account (cash balance) of the authenticated user's account. Retrieves a PDF account status letter for a virtual account number belonging to a spend account (cash balance) of the authenticated user's account. Defaults to the spend account's primary virtual account number when virtualAccountNumberId is omitted. Set displayBalance to include the balance on the letter, optionally as of a past date via balanceCheckDate. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getSpendAccountStatusLetter( userCashBalanceId: UUID! virtualAccountNumberId: UUID displayBalance: Boolean balanceCheckDate: DateTime ): SpendAccountPdfDocument! } ``` ## Arguments **userCashBalanceId** `UUID!` *required* The spend account (cash balance) the virtual account number belongs to. **virtualAccountNumberId** `UUID` Which of the spend account's virtual account numbers to document. Defaults to the primary one. **displayBalance** `Boolean` Include the account balance on the letter. Omitted by default, since the balance is sensitive. **balanceCheckDate** `DateTime` Report the available balance as of the end of this day (US Eastern) rather than the current balance, and label the letter's as-of line with that date. Cannot be in the future. Applies only when displayBalance is true; ignored otherwise. Requires a full RFC-3339 timestamp — a date alone ("2026-07-01") is rejected. Only the day the instant falls on in US Eastern is used, so send an Eastern-anchored timestamp: UTC midnight ("2026-07-01T00:00:00Z") is 8pm Eastern the day before and reports June 30. ## Returns [`SpendAccountPdfDocument!`](/api-reference/types/spend-account-pdf-document) — A PDF artifact containing ACH routing and account instructions for funding a spend account's virtual account number, returned as a base64-encoded string for the client to decode and render or download. # getSpendAccountVirtualAccountNumbers Source: https://docs.fluz.app/api-reference/queries/get-spend-account-virtual-account-numbers Retrieves the active virtual account number(s) for a spend account (cash balance) belonging to the authenticated user's account. Retrieves the active virtual account number(s) for a spend account (cash balance) belonging to the authenticated user's account. A spend account may have multiple active virtual account numbers, one of which is marked primary. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getSpendAccountVirtualAccountNumbers( userCashBalanceId: UUID! ): [SpendAccountVirtualAccountNumber!]! } ``` ## Arguments **userCashBalanceId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`[SpendAccountVirtualAccountNumber!]!`](/api-reference/types/spend-account-virtual-account-number) — A virtual account number (ACH routing + account number) tied to a spend account. # getTransactions Source: https://docs.fluz.app/api-reference/queries/get-transactions Retrieves paginated transaction history for the authenticated user's account. Retrieves paginated transaction history for the authenticated user's account. Supports comprehensive filtering and pagination. Requires LIST\_PAYMENT and LIST\_PURCHASES scope. ```graphql theme={null} query { getTransactions( filter: TransactionFilterInput paginate: OffsetInput ): TransactionConnection! } ``` ## Arguments **filter** `TransactionFilterInput` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`TransactionConnection!`](/api-reference/types/transaction-connection) — Paginated response for transactions query. # getUserAddresses Source: https://docs.fluz.app/api-reference/queries/get-user-addresses getUserAddresses returns the user's addresses. getUserAddresses returns the user's addresses. ```graphql theme={null} query { getUserAddresses( paginate: OffsetInput ): [UserAddress] } ``` ## Arguments **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`[UserAddress]`](/api-reference/types/user-address) — UserAddress represents a User Address record. # getUserCashBalanceById Source: https://docs.fluz.app/api-reference/queries/get-user-cash-balance-by-id Retrieves a single cash balance by ID for the authenticated user's account. Retrieves a single cash balance by ID for the authenticated user's account. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getUserCashBalanceById( userCashBalanceId: UUID! ): UserCashBalanceDetail! } ``` ## Arguments **userCashBalanceId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`UserCashBalanceDetail!`](/api-reference/types/user-cash-balance-detail) — UserCashBalanceDetail represents a user's cash balance account with additional details. # getUserCashBalances Source: https://docs.fluz.app/api-reference/queries/get-user-cash-balances Retrieves paginated list of cash balances for the authenticated user's account. Retrieves paginated list of cash balances for the authenticated user's account. Supports comprehensive filtering and pagination. Requires LIST\_PAYMENT scope. ```graphql theme={null} query { getUserCashBalances( filter: UserCashBalanceFilterInput paginate: OffsetInput ): UserCashBalanceConnection! } ``` ## Arguments **filter** `UserCashBalanceFilterInput` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`UserCashBalanceConnection!`](/api-reference/types/user-cash-balance-connection) — Paginated response for cash balances query. # getUserPurchases Source: https://docs.fluz.app/api-reference/queries/get-user-purchases Retrieves the user's purchase history. Retrieves the user's purchase history. Requires LIST\_PURCHASES scope. ```graphql theme={null} query { getUserPurchases( filter: UserPurchaseFilterInput paginate: OffsetInput ): [UserPurchase] } ``` ## Example Look up one purchase by the reference ID you stored at purchase time: ```graphql theme={null} query { getUserPurchases(filter: { purchaseDisplayId: "11117209" }) { purchaseDisplayId purchaseAmount seatRewardValue giftCard { giftCardId status } } } ``` ## Arguments **filter** `UserPurchaseFilterInput` Narrows the results. Pass `scope` to choose between the token's own purchases (`TOKEN_USER`, the default) and every purchase on the account (`TOKEN_ACCOUNT`) — the two combine with AND, so passing both returns only purchases the token's user made on the token's account. Pass `purchaseDisplayId` to fetch a single order by the reference ID returned at purchase time. See [`UserPurchaseFilterInput`](/api-reference/types/user-purchase-filter-input). **paginate** `OffsetInput` *No description provided in the schema yet.* ## Returns [`[UserPurchase]`](/api-reference/types/user-purchase) — UserPurchase represents a record of a purchase made by a user, detailing the payment methods and other relevant information. # getVCShareLinks Source: https://docs.fluz.app/api-reference/queries/get-vcshare-links Get list of generated share links. Get list of generated share links. Requires CREATE\_SHARE\_LINK scope. ```graphql theme={null} query { getVCShareLinks( input: GetVCShareLinksInput! ): [GeneratedShareLink] } ``` ## Arguments **input** `GetVCShareLinksInput!` *required* *No description provided in the schema yet.* ## Returns [`[GeneratedShareLink]`](/api-reference/types/generated-share-link) — GeneratedShareLink represents a single generated share link. # getVirtualCardBalance Source: https://docs.fluz.app/api-reference/queries/get-virtual-card-balance Get balance, status and lock state for multiple virtual cards. Get balance, status and lock state for multiple virtual cards. Requires PCI\_COMPLIANCE and REVEAL\_VIRTUALCARD scope. ```graphql theme={null} query { getVirtualCardBalance( input: GetVirtualCardBalanceInput! ): [VirtualCardBalance!]! } ``` ## Arguments **input** `GetVirtualCardBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`[VirtualCardBalance!]!`](/api-reference/types/virtual-card-balance) — VirtualCardBalance provides the balance, status and lock state of a virtual card. # getVirtualCardBulkOrderStatus Source: https://docs.fluz.app/api-reference/queries/get-virtual-card-bulk-order-status Checks bulk virtual card order status. Checks bulk virtual card order status. Requires PCI\_COMPLIANCE and REVEAL\_VIRTUALCARD scope. ```graphql theme={null} query { getVirtualCardBulkOrderStatus( input: GetVirtualCardBulkOrderStatusInput! ): VirtualCardBulkOrder } ``` ## Arguments **input** `GetVirtualCardBulkOrderStatusInput!` *required* *No description provided in the schema yet.* ## Returns [`VirtualCardBulkOrder`](/api-reference/types/virtual-card-bulk-order) — VirtualCardBulkOrder provides information about bulk creation order. # getVirtualCardOffers Source: https://docs.fluz.app/api-reference/queries/get-virtual-card-offers Get virtual card offers. Get virtual card offers. Requires CREATE\_VIRTUALCARD scope. ```graphql theme={null} query { getVirtualCardOffers( input: GetVirtualCardOffersInput ): [VirtualCardOffer] } ``` ## Arguments **input** `GetVirtualCardOffersInput` *No description provided in the schema yet.* ## Returns [`[VirtualCardOffer]`](/api-reference/types/virtual-card-offer) — Represents the Bank identification numbers (BINs). # getVirtualCardTransactions Source: https://docs.fluz.app/api-reference/queries/get-virtual-card-transactions Get transactions for multiple virtual cards with filters. Get transactions for multiple virtual cards with filters. Requires PCI\_COMPLIANCE and REVEAL\_VIRTUALCARD scope. ```graphql theme={null} query { getVirtualCardTransactions( input: GetVirtualCardTransactionsInput! ): [VirtualCardTransactions!]! } ``` ## Arguments **input** `GetVirtualCardTransactionsInput!` *required* *No description provided in the schema yet.* ## Returns [`[VirtualCardTransactions!]!`](/api-reference/types/virtual-card-transactions) — Virtual card transactions, grouped by virtual card ID. # getWallet Source: https://docs.fluz.app/api-reference/queries/get-wallet getWallet returns the user's wallet balance and payment methods. getWallet returns the user's wallet balance and payment methods. ```graphql theme={null} query { getWallet: GetWalletResponse } ``` ## Returns [`GetWalletResponse`](/api-reference/types/get-wallet-response) — GetWalletResponse represents the response to the get wallet request. # getWithdrawFeeEstimate Source: https://docs.fluz.app/api-reference/queries/get-withdraw-fee-estimate Estimate the fees for a withdrawal before submitting. Estimate the fees for a withdrawal before submitting. Returns the fee, net amount, and settlement timing. Requires MAKE\_WITHDRAWAL scope. ```graphql theme={null} query { getWithdrawFeeEstimate( input: GetWithdrawFeeEstimateInput! ): WithdrawFeeEstimate! } ``` ## Arguments **input** `GetWithdrawFeeEstimateInput!` *required* *No description provided in the schema yet.* ## Returns [`WithdrawFeeEstimate!`](/api-reference/types/withdraw-fee-estimate) — Estimated fees and net amount for a withdrawal. # lookupBusiness Source: https://docs.fluz.app/api-reference/queries/lookup-business Look up business recipients by company name. Look up business recipients by company name. Searches both business name and DBA name, returns all matches. Throws an error if no matches found. ```graphql theme={null} query { lookupBusiness( input: BusinessLookupInput! ): [BusinessLookupResult!]! } ``` ## Arguments **input** `BusinessLookupInput!` *required* *No description provided in the schema yet.* ## Returns [`[BusinessLookupResult!]!`](/api-reference/types/business-lookup-result) — Represents a business recipient. # lookupUser Source: https://docs.fluz.app/api-reference/queries/lookup-user Look up a user recipient by phone number or email. Look up a user recipient by phone number or email. Returns a single user or throws an error if not found. ```graphql theme={null} query { lookupUser( input: UserLookupInput! ): UserLookupResult! } ``` ## Arguments **input** `UserLookupInput!` *required* *No description provided in the schema yet.* ## Returns [`UserLookupResult!`](/api-reference/types/user-lookup-result) — Represents a user recipient. # objectChangeHistory Source: https://docs.fluz.app/api-reference/queries/object-change-history Change history for a virtual card, gift card, spend account, or funding source on the caller's account. Change history for a virtual card, gift card, spend account, or funding source on the caller's account: who requested, approved, created or changed it, and through which connected application. Returns the newest events first (max 50). Requires VIEW\_OBJECT\_CHANGE\_HISTORY scope. See [Activity history overview](/features/activity-history) for the events recorded on each object. ```graphql theme={null} query { objectChangeHistory( objectType: ObjectOwnerObjectType! objectId: UUID! ): [ObjectChangeHistoryEvent!]! } ``` ## Arguments **objectType** `ObjectOwnerObjectType!` *required* The type of object: `VIRTUAL_CARDS`, `GIFT_CARDS`, `SPEND_ACCOUNTS`, or `FUNDING_SOURCES`. **objectId** `UUID!` *required* The ID of the object on the caller's account. ## Returns [`[ObjectChangeHistoryEvent!]!`](/api-reference/types/object-change-history-event) — One append-only history event for an object. # Create a virtual card bulk order Source: https://docs.fluz.app/recipes/create-virtual-card-bulk-order Issue many virtual cards in a single asynchronous request. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const CREATE_VIRTUAL_CARD_BULK_ORDER = gql` mutation CreateVirtualCardBulkOrder { createVirtualCardBulkOrder( input: { offerId: "592c394e-26cc-44ac-a145-a5f81301fe77" orderItems: [ { quantity: 3 spendLimit: 100 spendLimitDuration: DAILY lockCardNextUse: true cardNickname: "Team card" primaryFundingSource: FLUZ_BALANCE } ] } ) { orderId orderStatus } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(CREATE_VIRTUAL_CARD_BULK_ORDER); console.log(response); ``` Poll `getVirtualCardBulkOrderStatus` with the returned `orderId` to fetch the created cards. # Edit a virtual card Source: https://docs.fluz.app/recipes/edit-virtual-card Update the spend limit, nickname, or lock date of an existing card. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const EDIT_VIRTUAL_CARD = gql` mutation { editVirtualCard( input: { virtualCardId: "59c7e092-7d5b-4ae7-946c-f119c93a830c" spendLimit: 200.00 spendLimitDuration: LIFETIME lockDate: "2032-10-10" lockCardNextUse: false cardNickname: "Updated nickname" primaryFundingSource: FLUZ_BALANCE } ) { virtualCardId cardholderName virtualCardLast4 status initialAmount } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(EDIT_VIRTUAL_CARD); console.log(response); ``` Requires the `EDIT_VIRTUALCARD` scope. # Get merchants Source: https://docs.fluz.app/recipes/get-merchants Fetch the merchant catalog with offers and stock information. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const GET_MERCHANTS = gql` query getMerchants($name: String, $paginate: OffsetInput) { getMerchants(name: $name, paginate: $paginate) { merchantId name slug offers { offerId type offerRates { maxUserRewardValue denominations } stockInfo { ... on StockInfoVariableType { __typename description maxDenomination minDenomination } ... on StockInfoFixedType { __typename denomination availableStock } } } } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer `, 'Content-Type': 'application/json', }, }); const res = await client.request(GET_MERCHANTS, { name: null, paginate: { limit: 20, offset: 0 }, }); console.log(JSON.stringify(res, null, 2)); ``` Paginate by increasing `offset` until you receive an empty array. Related: [Rewards / Gift cards](/gift-card-overview). # Get virtual card balance Source: https://docs.fluz.app/recipes/get-virtual-card-balance Retrieve remaining balance, spend limit, status and lock state for one or more cards. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const GET_VIRTUAL_CARD_BALANCE = gql` query GetVirtualCardBalance { getVirtualCardBalance( input: { virtualCardIds: [ "bd3be748-a0fb-4193-80a7-88419bc72dab" "9e42fdab-3c23-4eae-bd69-1fa746886505" ] } ) { virtualCardId spentAmount remainingBalance spendLimit spendLimitDuration status locked authorizationSetting { lockedByUser lockDate lockCardNextUse tokenized cardNickname } } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(GET_VIRTUAL_CARD_BALANCE); console.log(response); ``` Requires the `REVEAL_VIRTUALCARD` scope. This query reads from Fluz, so it is not subject to the card issuer's rate limits. Use it instead of `revealVirtualCardByVirtualCardId` when you need card state in bulk and not the card number. A locked card keeps `status: ACTIVE`, so check `locked` rather than `status` to tell whether a card can spend. `locked` covers locks set by the cardholder, by Fluz automatically once the lock date passes, and by Fluz support. `authorizationSetting` is the same object `revealVirtualCardByVirtualCardId` returns, including `lockDate` as a Unix timestamp in milliseconds. # Get virtual card bulk order status Source: https://docs.fluz.app/recipes/get-virtual-card-bulk-order-status Check the status of a bulk order and retrieve the issued cards. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const GET_STATUS = gql` query GetVirtualCardBulkOrderStatus { getVirtualCardBulkOrderStatus( input: { orderId: "<>" } ) { orderStatus orderId virtualCards { cardNumber expiryMMYY cvv cardHolderName virtualCardId } successfulCardCreations failedCardCreations totalCards } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(GET_STATUS); console.log(response); ``` # Get virtual card transactions Source: https://docs.fluz.app/recipes/get-virtual-card-transactions Query virtual card activity by card, transaction type, or date range. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const GET_VIRTUAL_CARD_TRANSACTIONS = gql` query GetVirtualCardTransactions { getVirtualCardTransactions( input: { virtualCardIds: ["2ed71ba0-d457-47ed-8ceb-d3fe6ce5c900"] paginate: { limit: 20, offset: 0 } filters: { transactionTypes: [PURCHASE] } } ) { virtualCardId transactions { transactionDate transactionType transactionStatus merchantName transactionAmount } } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(GET_VIRTUAL_CARD_TRANSACTIONS); console.log(response); ``` Requires the `REVEAL_VIRTUALCARD` scope. # Lock a virtual card Source: https://docs.fluz.app/recipes/lock-virtual-card Temporarily block a virtual card from further transactions. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const LOCK_VIRTUAL_CARD = gql` mutation LockVirtualCard { lockVirtualCard( input: { virtualCardId: "cc7eaf27-1560-425c-92c4-bdc88d45d261" } ) { virtualCardId locked } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(LOCK_VIRTUAL_CARD); console.log(response); ``` Requires the `EDIT_VIRTUALCARD` scope. # Purchase a gift card Source: https://docs.fluz.app/recipes/purchase-gift-card End-to-end example: generate a token, purchase a gift card, and reveal its details. Complete flow that generates a user access token, purchases a gift card by merchant slug, and reveals the redemption details. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; import crypto from 'crypto'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const API_KEY = 'your-api-key-here'; const USER_ID = 'user-id-uuid-here'; const ACCOUNT_ID = 'account-id-uuid-here'; const generateUserToken = async (userId, accountId) => { const client = new GraphQLClient(API_URL, { headers: { Authorization: `Basic ${API_KEY}`, 'Content-Type': 'application/json', }, }); const GENERATE_USER_ACCESS_TOKEN = gql` mutation generateUserAccessToken( $userId: UUID! $accountId: UUID! $scopes: [ScopeType!]! $seatId: UUID ) { generateUserAccessToken( userId: $userId accountId: $accountId scopes: $scopes seatId: $seatId ) { token scopes } } `; const variables = { userId, accountId, scopes: [ 'LIST_PURCHASES', 'REVEAL_GIFTCARD', 'LIST_OFFERS', 'PURCHASE_GIFTCARD', 'LIST_PAYMENT', 'MANAGE_PAYMENT', 'MAKE_DEPOSIT', ], seatId: null, }; const res = await client.request(GENERATE_USER_ACCESS_TOKEN, variables); return res.generateUserAccessToken.token; }; const purchaseGiftCard = async (userToken, slug, amount) => { const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer ${userToken}`, 'Content-Type': 'application/json', }, }); const PURCHASE_GIFT_CARD = gql` mutation purchaseGiftCard($input: PurchaseGiftCardInput!) { purchaseGiftCard(input: $input) { purchaseId purchaseDisplayId purchaseAmount giftCard { giftCardId status createdAt } } } `; const variables = { input: { idempotencyKey: crypto.randomUUID(), amount, merchantSlug: slug, minRewardRate: 2.0, }, }; return client.request(PURCHASE_GIFT_CARD, variables); }; const revealGiftCard = async (userToken, giftCardId) => { const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer ${userToken}`, 'Content-Type': 'application/json', }, }); const REVEAL_GIFT_CARD = gql` mutation revealGiftCardByGiftCardId($giftCardId: UUID!) { revealGiftCardByGiftCardId(giftCardId: $giftCardId) { code pin url } } `; return client.request(REVEAL_GIFT_CARD, { giftCardId }); }; (async () => { const userToken = await generateUserToken(USER_ID, ACCOUNT_ID); const giftCard = await purchaseGiftCard(userToken, 'aero', 10); const giftCardId = giftCard.purchaseGiftCard.giftCard.giftCardId; const details = await revealGiftCard(userToken, giftCardId); console.log('Gift Card Details:', JSON.stringify(details, null, 2)); })(); ``` Related: [Purchase Gift Cards](/gift-card-overview), [Authentication](/get-started/api-credentials). # Reveal a virtual card Source: https://docs.fluz.app/recipes/reveal-virtual-card Retrieve the PAN, CVV, and expiry for an issued virtual card. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const REVEAL_VIRTUAL_CARD = gql` mutation RevealVirtualCardByVirtualCardId { revealVirtualCardByVirtualCardId( virtualCardId: "c107e50b-10f3-449c-92c0-609d9a8cfa2a" ) { cardNumber expiryMMYY cvv cardHolderName billingAddress { streetAddress postalCode city state } } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(REVEAL_VIRTUAL_CARD); console.log(response); ``` Requires the `REVEAL_VIRTUALCARD` scope, plus `PCI_COMPLIANCE` on your application. > [!NOTE] > > This mutation returns the raw PAN and CVV to your server, which brings your infrastructure into PCI scope. `PCI_COMPLIANCE` is granted on request for applications operating on their own account; on accounts belonging to other people it requires a PCI DSS certification on file — see [Due diligence](/build-a-platform#due-diligence). > > If you only need to **show a user their own card**, use [Secure Elements Card Reveal](/build-a-platform/card-reveal) instead. It renders the details in a Fluz-hosted frame inline in your page, needs no `PCI_COMPLIANCE` grant, and keeps your systems out of scope entirely. # Set a virtual card PIN Source: https://docs.fluz.app/recipes/set-virtual-card-pin Enqueue PIN updates on all eligible virtual cards. ```javascript theme={null} import { GraphQLClient, gql } from 'graphql-request'; const API_URL = 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql'; const SET_VIRTUAL_CARD_PIN = gql` mutation SetVirtualCardPIN { setVirtualCardPIN(input: { pin: "1234" }) { success pinError } } `; const client = new GraphQLClient(API_URL, { headers: { Authorization: `Bearer <>`, 'Content-Type': 'application/json', }, }); const response = await client.request(SET_VIRTUAL_CARD_PIN); console.log(response); ``` Requires the `CREATE_VIRTUALCARD` scope. # removePlaidBankInstitution Source: https://docs.fluz.app/api-reference/mutations/remove-plaid-bank-institution Removes a Plaid bank institution and disables its local bank accounts. Removes a Plaid bank institution and disables its local bank accounts. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { removePlaidBankInstitution( input: RemovePlaidBankInstitutionInput! ): RemovePlaidBankInstitutionResponse! } ``` ## Arguments **input** `RemovePlaidBankInstitutionInput!` *required* *No description provided in the schema yet.* ## Returns [`RemovePlaidBankInstitutionResponse!`](/api-reference/types/remove-plaid-bank-institution-response) # removeSupervisor Source: https://docs.fluz.app/api-reference/mutations/remove-supervisor *No description provided in the schema yet.* ```graphql theme={null} mutation { removeSupervisor( superviseeUserId: UUID! ): SupervisionAssignmentResponse! } ``` ## Arguments **superviseeUserId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`SupervisionAssignmentResponse!`](/api-reference/types/supervision-assignment-response) # requestAccountTransfer Source: https://docs.fluz.app/api-reference/mutations/request-account-transfer Requests manager approval for an account-to-account transfer. Requests manager approval for an account-to-account transfer. Pass `useRewardsBalance` on [`RequestAccountTransferInput`](/api-reference/types/request-account-transfer-input) to fund from rewards after approval. See [Fund account transfers with rewards](/features/fund-account-transfers-with-rewards). ```graphql theme={null} mutation { requestAccountTransfer( input: RequestAccountTransferInput! ): RequestApprovalResponse! } ``` ## Arguments **input** `RequestAccountTransferInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestApprovalResponse!`](/api-reference/types/request-approval-response) — Response returned from request approval mutations. # requestDocumentVerificationLink Source: https://docs.fluz.app/api-reference/mutations/request-document-verification-link Request verification for a user with provided information Request verification for a user with provided information ```graphql theme={null} mutation { requestDocumentVerificationLink( input: RequestDocumentVerificationLinkInput! ): RequestDocumentVerificationLinkResponse! } ``` ## Arguments **input** `RequestDocumentVerificationLinkInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestDocumentVerificationLinkResponse!`](/api-reference/types/request-document-verification-link-response) — Response type for user verification request # requestGiftCardPurchase Source: https://docs.fluz.app/api-reference/mutations/request-gift-card-purchase Requests manager approval to purchase a gift card. Requests manager approval to purchase a gift card. ```graphql theme={null} mutation { requestGiftCardPurchase( input: RequestGiftCardPurchaseInput! ): RequestApprovalResponse! } ``` ## Arguments **input** `RequestGiftCardPurchaseInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestApprovalResponse!`](/api-reference/types/request-approval-response) — Response returned from request approval mutations. # requestInternalTransfer Source: https://docs.fluz.app/api-reference/mutations/request-internal-transfer Requests manager approval for an internal transfer. Requests manager approval for an internal transfer. ```graphql theme={null} mutation { requestInternalTransfer( input: RequestInternalTransferInput! ): RequestApprovalResponse! } ``` ## Arguments **input** `RequestInternalTransferInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestApprovalResponse!`](/api-reference/types/request-approval-response) — Response returned from request approval mutations. # requestOwnerDocumentVerificationLink Source: https://docs.fluz.app/api-reference/mutations/request-owner-document-verification-link Request a document verification link for a business owner on the document path. Request a document verification link for a business owner on the document path. Returns a shareable URL that the owner can use to complete document verification. `businessOwnerId` must be a non-null `BusinessOwnerStatus.id` from `getBusiness` for an owner with `verificationType: DOCUMENTS`. Invited owners that have not accepted the invite are not eligible: `getBusiness` omits `id` for those rows (`status: PENDING_INVITE`). They complete verification after accepting their invite, not through this mutation. ```graphql theme={null} mutation { requestOwnerDocumentVerificationLink( businessOwnerId: UUID! ): RequestOwnerDocumentVerificationLinkResponse! } ``` ## Arguments **businessOwnerId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`RequestOwnerDocumentVerificationLinkResponse!`](/api-reference/types/request-owner-document-verification-link-response) — Response from requesting an owner document verification link. # requestReimbursement Source: https://docs.fluz.app/api-reference/mutations/request-reimbursement Requests manager approval for a reimbursement. Requests manager approval for a reimbursement. ```graphql theme={null} mutation { requestReimbursement( input: RequestReimbursementInput! ): RequestApprovalResponse! } ``` ## Arguments **input** `RequestReimbursementInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestApprovalResponse!`](/api-reference/types/request-approval-response) — Response returned from request approval mutations. # requestVirtualCard Source: https://docs.fluz.app/api-reference/mutations/request-virtual-card Requests manager approval to create a virtual card. Requests manager approval to create a virtual card. ```graphql theme={null} mutation { requestVirtualCard( input: RequestVirtualCardInput! ): RequestApprovalResponse! } ``` ## Arguments **input** `RequestVirtualCardInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestApprovalResponse!`](/api-reference/types/request-approval-response) — Response returned from request approval mutations. # requestVirtualCardLimitChange Source: https://docs.fluz.app/api-reference/mutations/request-virtual-card-limit-change Requests manager approval to change a virtual card limit. Requests manager approval to change a virtual card limit. ```graphql theme={null} mutation { requestVirtualCardLimitChange( input: RequestVirtualCardLimitChangeInput! ): RequestApprovalResponse! } ``` ## Arguments **input** `RequestVirtualCardLimitChangeInput!` *required* *No description provided in the schema yet.* ## Returns [`RequestApprovalResponse!`](/api-reference/types/request-approval-response) — Response returned from request approval mutations. # revealGiftCardByGiftCardId Source: https://docs.fluz.app/api-reference/mutations/reveal-gift-card-by-gift-card-id Reveals the Gift Card Code. Reveals the Gift Card Code. It has an irreversible effect on the Gift Card status. Requires REVEAL\_GIFTCARD scope. ```graphql theme={null} mutation { revealGiftCardByGiftCardId( giftCardId: UUID! ): GiftCardCode } ``` ## Arguments **giftCardId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`GiftCardCode`](/api-reference/types/gift-card-code) — GiftCardCode contains the redemption credentials for a Merchant. # revealVirtualCardByVirtualCardId Source: https://docs.fluz.app/api-reference/mutations/reveal-virtual-card-by-virtual-card-id Reveals the full card number and CVV of a virtual card, along with its billing details. Reveals the full card number and CVV of a virtual card, along with its billing details. Requires PCI\_COMPLIANCE from the developer and REVEAL\_VIRTUALCARD scope. PERSONAL/Private applications are exempt from PCI\_COMPLIANCE requirement. ```graphql theme={null} mutation { revealVirtualCardByVirtualCardId( virtualCardId: UUID! ): VirtualCardDetails } ``` ## Arguments **virtualCardId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`VirtualCardDetails`](/api-reference/types/virtual-card-details) — The VirtualCardDetails type represents the details of a virtual card. # setBackupFundingSource Source: https://docs.fluz.app/api-reference/mutations/set-backup-funding-source setBackupFundingSource sets a bank card as the account's backup funding source, replacing any previous backup. setBackupFundingSource sets a bank card as the account's backup funding source, replacing any previous backup. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { setBackupFundingSource( input: SetBackupFundingSourceInput! ): DefaultFundingSource } ``` ## Arguments **input** `SetBackupFundingSourceInput!` *required* *No description provided in the schema yet.* ## Returns [`DefaultFundingSource`](/api-reference/types/default-funding-source) — DefaultFundingSource represents an account's default funding source: the primary payment method (a bank account, charged first) and the backup payment method (a bank card, charged if the primary can't be charged). # setPrimaryFundingSource Source: https://docs.fluz.app/api-reference/mutations/set-primary-funding-source setPrimaryFundingSource sets a bank account as the account's primary funding source, replacing any previous primary. setPrimaryFundingSource sets a bank account as the account's primary funding source, replacing any previous primary. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { setPrimaryFundingSource( input: SetPrimaryFundingSourceInput! ): DefaultFundingSource } ``` ## Arguments **input** `SetPrimaryFundingSourceInput!` *required* *No description provided in the schema yet.* ## Returns [`DefaultFundingSource`](/api-reference/types/default-funding-source) — DefaultFundingSource represents an account's default funding source: the primary payment method (a bank account, charged first) and the backup payment method (a bank card, charged if the primary can't be charged). # setVirtualCardPIN Source: https://docs.fluz.app/api-reference/mutations/set-virtual-card-pin Mutation to set the PIN on eligible virtual cards that have not yet had a PIN set. Mutation to set the PIN on eligible virtual cards that have not yet had a PIN set. The request adds the cards to a queue and the response indicates whether it was successfully enqueued. The actual PIN updating can take a few minutes to process. Requires CREATE\_VIRTUALCARD scope. ```graphql theme={null} mutation { setVirtualCardPIN( input: SetVirtualCardPINInput! ): SetVirtualCardPINResponse } ``` ## Arguments **input** `SetVirtualCardPINInput!` *required* *No description provided in the schema yet.* ## Returns [`SetVirtualCardPINResponse`](/api-reference/types/set-virtual-card-pinresponse) — Shows whether setting the PIN on eligible virtual cards was successful. # submitBulkOperation Source: https://docs.fluz.app/api-reference/mutations/submit-bulk-operation Submit an asynchronous bulk operation, using 'Basic '. Submit an asynchronous bulk operation, using 'Basic \'. Requires the bulk API capability on your application, and the scopes the operation needs on each target user's grant. GET\_TRANSACTIONS\_EXPORT, GET\_BALANCES\_EXPORT, CREATE\_TRANSFER, and DEPOSIT\_CASH\_BALANCE accept up to 10,000 targets; UPDATE\_TRANSACTION\_METADATA executes synchronously and accepts at most 100 edits in total. Creates one job with one item per resolved target and returns immediately; poll the job to follow progress. Idempotent on your idempotency key: an identical resubmit returns the existing job, and reusing a key with a different request is rejected. A submission with no eligible targets is rejected. ```graphql theme={null} mutation { submitBulkOperation( input: SubmitBulkOperationInput! ): BulkJob } ``` ## Arguments **input** `SubmitBulkOperationInput!` *required* *No description provided in the schema yet.* ## Returns [`BulkJob`](/api-reference/types/bulk-job) — A submitted bulk job. # transferInternalBalance Source: https://docs.fluz.app/api-reference/mutations/transfer-internal-balance Mutation for transferring between user cash balances. Mutation for transferring between user cash balances. Requires MAKE\_INTERNAL\_TRANSFER scope. ```graphql theme={null} mutation { transferInternalBalance( input: TransferInternalBalanceInput! ): TransferInternalBalanceResponse! } ``` ## Arguments **input** `TransferInternalBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`TransferInternalBalanceResponse!`](/api-reference/types/transfer-internal-balance-response) — Response type for the transferInternalBalance mutation, returning the deposit, withdrawal records and updated balances. # transferObjectOwner Source: https://docs.fluz.app/api-reference/mutations/transfer-object-owner Transfer ownership of an existing object to another user on the same account. Transfer ownership of an existing object to another user on the same account. Requires MANAGE\_SUBUSERS. Forwards to auth-service PATCH /api/v1/object-owners/:objectOwnerId/owner. ```graphql theme={null} mutation { transferObjectOwner( objectOwnerId: UUID! userId: UUID! ): TransferObjectOwnerResponse } ``` ## Arguments **objectOwnerId** `UUID!` *required* *No description provided in the schema yet.* **userId** `UUID!` *required* *No description provided in the schema yet.* ## Returns [`TransferObjectOwnerResponse`](/api-reference/types/transfer-object-owner-response) # unlockVirtualCard Source: https://docs.fluz.app/api-reference/mutations/unlock-virtual-card Mutation to unlock a previously locked virtual card. Mutation to unlock a previously locked virtual card. Requires EDIT\_VIRTUALCARD scope. ```graphql theme={null} mutation { unlockVirtualCard( input: UnlockVirtualCardInput! ): UnlockVirtualCardResponse } ``` ## Arguments **input** `UnlockVirtualCardInput!` *required* *No description provided in the schema yet.* ## Returns [`UnlockVirtualCardResponse`](/api-reference/types/unlock-virtual-card-response) # updateBankCardNickname Source: https://docs.fluz.app/api-reference/mutations/update-bank-card-nickname updateBankCardNickname updates the nickname of a bank card. updateBankCardNickname updates the nickname of a bank card. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { updateBankCardNickname( input: UpdateBankCardNicknameInput! ): BankCard } ``` ## Arguments **input** `UpdateBankCardNicknameInput!` *required* *No description provided in the schema yet.* ## Returns [`BankCard`](/api-reference/types/bank-card) — BankCard is a record of a bank card added by a user. # updateBankCardPreferredMerchantCategoryCode Source: https://docs.fluz.app/api-reference/mutations/update-bank-card-preferred-merchant-category-code updateBankCardPreferredMerchantCategoryCode updates the preferred merchant category code (MCC) for a bank card. updateBankCardPreferredMerchantCategoryCode updates the preferred merchant category code (MCC) for a bank card. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { updateBankCardPreferredMerchantCategoryCode( input: UpdateBankCardPreferredMerchantCategoryCodeInput! ): BankCard } ``` ## Arguments **input** `UpdateBankCardPreferredMerchantCategoryCodeInput!` *required* *No description provided in the schema yet.* ## Returns [`BankCard`](/api-reference/types/bank-card) — BankCard is a record of a bank card added by a user. # updateTransactionMetadata Source: https://docs.fluz.app/api-reference/mutations/update-transaction-metadata Updates the memo, category, and/or attachment on an existing transaction. Updates the memo, category, and/or attachment on an existing transaction. Requires LIST\_PAYMENT and LIST\_PURCHASES scope. ```graphql theme={null} mutation { updateTransactionMetadata( input: UpdateTransactionMetadataInput! ): UpdatedTransaction } ``` ## Arguments **input** `UpdateTransactionMetadataInput!` *required* *No description provided in the schema yet.* ## Returns [`UpdatedTransaction`](/api-reference/types/updated-transaction) — UpdatedTransaction is the transaction record returned by the updateTransactionMetadata mutation: the persisted transaction row plus the memo/category/attachment just written. # updateUserCashBalance Source: https://docs.fluz.app/api-reference/mutations/update-user-cash-balance Mutation for updating a user cash balance account. Mutation for updating a user cash balance account. Requires MANAGE\_PAYMENT scope. ```graphql theme={null} mutation { updateUserCashBalance( input: UpdateUserCashBalanceInput! ): UserCashBalance! } ``` ## Arguments **input** `UpdateUserCashBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`UserCashBalance!`](/api-reference/types/user-cash-balance) — UserCashBalance represents a user's cash balance account. # verifyUserInformation Source: https://docs.fluz.app/api-reference/mutations/verify-user-information Verify and record a user's personal information. Verify and record a user's personal information. ```graphql theme={null} mutation { verifyUserInformation( firstName: String! lastName: String! streetLine1: String! streetLine2: String city: String! state: String! postalCode: String! country: String! dateOfBirth: String! ssnLast4: String! forceRetry: Boolean ): VerifyUserInformationResponse } ``` ## Arguments **firstName** `String!` *required* The Fluz user's first name. **lastName** `String!` *required* The Fluz user's last name. **streetLine1** `String!` *required* The street address line 1. **streetLine2** `String` The street address line 2. **city** `String!` *required* The address city. **state** `String!` *required* The address state. **postalCode** `String!` *required* The postal code. **country** `String!` *required* The address country. **dateOfBirth** `String!` *required* The user's date of birth (format MM/DD/YYYY). **ssnLast4** `String!` *required* The user's SSN last 4 digits. **forceRetry** `Boolean` Whether to bypass duplicate-verification checks and force a new verification attempt. ## Returns [`VerifyUserInformationResponse`](/api-reference/types/verify-user-information-response) — Represents the response returned from the verifyUserInformation mutation. # verifyUserPrefillInformation Source: https://docs.fluz.app/api-reference/mutations/verify-user-prefill-information Verify and record a user's personal information using prefilled data already on file. Verify and record a user's personal information using prefilled data already on file. ```graphql theme={null} mutation { verifyUserPrefillInformation( forceRetry: Boolean ): VerifyUserInformationResponse } ``` ## Arguments **forceRetry** `Boolean` Whether to bypass duplicate-verification checks and force a new verification attempt. ## Returns [`VerifyUserInformationResponse`](/api-reference/types/verify-user-information-response) — Represents the response returned from the verifyUserInformation mutation. # withdrawCashBalance Source: https://docs.fluz.app/api-reference/mutations/withdraw-cash-balance Mutation for withdrawing from a cash balance. Mutation for withdrawing from a cash balance. Supports ACH, PayPal, Venmo, and push-to-card methods. Use getWithdrawFeeEstimate to preview fees before submitting. Requires MAKE\_WITHDRAWAL scope. ```graphql theme={null} mutation { withdrawCashBalance( input: WithdrawCashBalanceInput! ): WithdrawCashBalanceResponse! } ``` ## Arguments **input** `WithdrawCashBalanceInput!` *required* *No description provided in the schema yet.* ## Returns [`WithdrawCashBalanceResponse!`](/api-reference/types/withdraw-cash-balance-response) — Response type for the withdrawCashBalance mutation, returning the withdrawal record and updated balances. # Account Source: https://docs.fluz.app/api-reference/types/account Represents the account operable by the application. **Object** Represents the account operable by the application. ## Fields **accountId** `UUID` The id of the account. **type** `AccountType` The type of account. **accountName** `String` The name of the business or first and last name of the user. **seats** `[Seat]` The seats owned with the account. **user** `User` The user associated with the account. **business** `Business` The business associated with the account. # AddAuthorizedUserResponse Source: https://docs.fluz.app/api-reference/types/add-authorized-user-response Response returned from the addAuthorizedUser mutation. **Object** Response returned from the addAuthorizedUser mutation. ## Fields **success** `Boolean!` Whether the operation succeeded. **authUserId** `UUID` The authorized user ID (UAC role assignment ID) created for the user on the account. Use this ID when calling removeAuthorizedUser. **roles** `[UACRoleType]` The roles assigned to the user. **status** `UACRoleStatusType` The status of the role assignment. **pendingActionId** `UUID` The pending action ID for the invite, if created. **error** `AuthorizedUserError` Error details when the operation fails. # ApplicationUser Source: https://docs.fluz.app/api-reference/types/application-user Represents the User who grants access to the application with scopes. **Object** Represents the User who grants access to the application with scopes. ## Fields **userId** `UUID` The userId that has granted the scopes. **scopes** `[ScopeType]` The user scopes available the application. **accounts** `[Account]` The accounts that the user has access to. # ApprovalRequest Source: https://docs.fluz.app/api-reference/types/approval-request An open approval request for the caller's account. **Object** An open approval request for the caller's account. ## Fields **approvalId** `UUID!` *No description provided in the schema yet.* **accountId** `UUID!` *No description provided in the schema yet.* **requesterUserId** `UUID` *No description provided in the schema yet.* **approvalType** `String!` *No description provided in the schema yet.* **approvalCode** `String!` *No description provided in the schema yet.* **approvalStatus** `ApprovalRequestStatus!` *No description provided in the schema yet.* **approversLogic** `String` *No description provided in the schema yet.* **expireDate** `DateTime` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* **approvers** `[ApprovalRequestApprover!]!` *No description provided in the schema yet.* # ApprovalRequestActionError Source: https://docs.fluz.app/api-reference/types/approval-request-action-error Error details returned when an approval action cannot be submitted. **Object** Error details returned when an approval action cannot be submitted. ## Fields **code** `String!` *No description provided in the schema yet.* **message** `String!` *No description provided in the schema yet.* # ApprovalRequestActionResponse Source: https://docs.fluz.app/api-reference/types/approval-request-action-response Response returned from approve/decline approval request mutations. **Object** Response returned from approve/decline approval request mutations. ## Fields **success** `Boolean!` *No description provided in the schema yet.* **approvalId** `UUID!` *No description provided in the schema yet.* **action** `ApprovalRequestAction!` *No description provided in the schema yet.* **error** `ApprovalRequestActionError` *No description provided in the schema yet.* # ApprovalRequestApprover Source: https://docs.fluz.app/api-reference/types/approval-request-approver An approver assigned to an approval request. **Object** An approver assigned to an approval request. ## Fields **approvalApproverId** `UUID!` *No description provided in the schema yet.* **accountId** `UUID!` *No description provided in the schema yet.* **approverUserId** `UUID` *No description provided in the schema yet.* **role** `String` *No description provided in the schema yet.* **status** `ApprovalRequestApproverStatus` *No description provided in the schema yet.* # AssignObjectOwnerResponse Source: https://docs.fluz.app/api-reference/types/assign-object-owner-response **Object** *No description provided in the schema yet.* ## Fields **success** `Boolean!` *No description provided in the schema yet.* **objectOwnerId** `UUID` *No description provided in the schema yet.* **accountId** `UUID` *No description provided in the schema yet.* **objectType** `String` *No description provided in the schema yet.* **objectId** `UUID` *No description provided in the schema yet.* **userId** `UUID` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* **error** `ObjectOwnerError` *No description provided in the schema yet.* # AuthorizedUser Source: https://docs.fluz.app/api-reference/types/authorized-user Represents an authorized user on an account. **Object** Represents an authorized user on an account. ## Fields **authUserId** `UUID!` The authorized user ID (UAC role assignment ID). **roles** `[UACRoleType]` The roles assigned to the user on this account. **status** `UACRoleStatusType` The status of the role assignment. **email** `String` The email address of the authorized user. **phone** `String` The phone number of the authorized user. **firstName** `String` The first name of the authorized user. **lastName** `String` The last name of the authorized user. **supervisorUserId** `UUID` *No description provided in the schema yet.* **supervisorAuthUserId** `UUID` *No description provided in the schema yet.* **supervisionAssignmentId** `UUID` *No description provided in the schema yet.* # AuthorizedUserError Source: https://docs.fluz.app/api-reference/types/authorized-user-error Error details for authorized user mutation failures. **Object** Error details for authorized user mutation failures. ## Fields **message** `String` Brief error message describing the failure. **code** `String` Error code identifying the specific error type. # BankAccount Source: https://docs.fluz.app/api-reference/types/bank-account BankAccount is a record of a bank account added by a user. **Object** BankAccount is a record of a bank account added by a user. ## Fields **bankAccountId** `UUID!` The ID of the bank account. **accountId** `UUID!` *No description provided in the schema yet.* **type** `BankAccountType!` *No description provided in the schema yet.* **accountName** `String!` *No description provided in the schema yet.* **status** `BankAccountStatus!` *No description provided in the schema yet.* **achRouting** `String!` *No description provided in the schema yet.* **lastFour** `String!` *No description provided in the schema yet.* **authChargeBackupPaymentMethod** `Boolean!` *No description provided in the schema yet.* **billingAddressId** `UUID!` *No description provided in the schema yet.* **nickname** `String` *No description provided in the schema yet.* # BankCard Source: https://docs.fluz.app/api-reference/types/bank-card BankCard is a record of a bank card added by a user. **Object** BankCard is a record of a bank card added by a user. ## Fields **bankCardId** `UUID!` The ID of the bank card. **ownerAccountId** `UUID!` *No description provided in the schema yet.* **addedUserId** `UUID!` *No description provided in the schema yet.* **cardType** `BankCardType` *No description provided in the schema yet.* **cardProcessor** `String` *No description provided in the schema yet.* **cardholderName** `String` *No description provided in the schema yet.* **lastFourDigits** `String` *No description provided in the schema yet.* **expirationMonth** `String` *No description provided in the schema yet.* **expirationYear** `String` *No description provided in the schema yet.* **cardStatus** `BankCardStatus` *No description provided in the schema yet.* **billingAddressId** `UUID` *No description provided in the schema yet.* **nickname** `String` *No description provided in the schema yet.* # BulkBalances Source: https://docs.fluz.app/api-reference/types/bulk-balances Balances across your connected users, one result per target. **Object** Balances across your connected users, one result per target. ## Fields **results** `[BulkBalancesTargetResult!]!` *No description provided in the schema yet.* **targetCount** `Int!` The number of targets addressed by the request. **successCount** `Int!` *No description provided in the schema yet.* **failureCount** `Int!` *No description provided in the schema yet.* # BulkBalancesTargetResult Source: https://docs.fluz.app/api-reference/types/bulk-balances-target-result The result of a bulk balances request for one target user. **Object** The result of a bulk balances request for one target user. ## Fields **externalReferenceId** `String` Your reference id for the target user, when available. **accountId** `UUID` The accountId of the target user, when the target was resolved. **success** `Boolean!` Whether balances could be returned for this target. **error** `BulkTargetError` Failure detail when success is false. **balances** `[UserCashBalance!]` The target user's cash balances. Restricted to the permitted accounts when the user's grant is scoped to specific spend accounts. # BulkConnectedOAuthUser Source: https://docs.fluz.app/api-reference/types/bulk-connected-oauth-user A user connected to your application through an active OAuth grant. **Object** A user connected to your application through an active OAuth grant. ## Fields **externalReferenceId** `String` Your reference id for this user, if one was provided when the user connected. Users without an externalReferenceId cannot be selected individually by bulk operations and are only reachable in ALL\_CONNECTED mode. **accountId** `UUID` The accountId of the connected user. **userId** `UUID` The userId of the connected user. **scopes** `[ScopeType!]!` The scopes the user has granted to your application. **connectedAt** `DateTime` When the user connected to your application. # BulkConnectedOAuthUsers Source: https://docs.fluz.app/api-reference/types/bulk-connected-oauth-users A page of users connected to your application. **Object** A page of users connected to your application. ## Fields **users** `[BulkConnectedOAuthUser!]!` *No description provided in the schema yet.* **totalCount** `Int!` The total number of connected users across all pages. **hasNextPage** `Boolean!` Whether more connected users exist beyond this page. # BulkJob Source: https://docs.fluz.app/api-reference/types/bulk-job A submitted bulk job. **Object** A submitted bulk job. Poll its status to follow progress and retrieve results. ## Fields **jobId** `UUID!` The id of the job. Use it to poll status and, for exports, fetch results. **status** `BulkJobStatus!` *No description provided in the schema yet.* **operation** `BulkOperationType!` *No description provided in the schema yet.* **targetMode** `BulkTargetMode!` *No description provided in the schema yet.* **requestedTargetCount** `Int!` The number of resolved targets this job addresses (one item per target). **acceptedItemCount** `Int!` The number of items that will be processed. **skippedItemCount** `Int!` The number of resolved targets skipped at submission (e.g. missing the required scopes). **succeededItemCount** `Int!` *No description provided in the schema yet.* **failedItemCount** `Int!` *No description provided in the schema yet.* **createdAt** `DateTime!` *No description provided in the schema yet.* **resultUrl** `String` Short-lived, pre-signed download URL for a completed job's result file — the NDJSON export for the read operations, or the per-target results ledger for the write operations (CREATE\_TRANSFER, DEPOSIT\_CASH\_BALANCE), one row per target with its status, resultResourceId, and any error. Minted on request, so it is only signed when you actually select this field — polling status without it stays cheap. Null until the job is terminal, and for jobs that produced no file (e.g. every target was denied). The URL expires shortly after issue (see resultUrlExpiresAt); request the field again for a fresh one. **resultUrlExpiresAt** `DateTime` When the currently-issued resultUrl stops working. Null whenever resultUrl is null. # BulkMetadataEditResult Source: https://docs.fluz.app/api-reference/types/bulk-metadata-edit-result Per-edit outcome for a single UPDATE_TRANSACTION_METADATA edit within a bulk operation item. **Object** Per-edit outcome for a single UPDATE\_TRANSACTION\_METADATA edit within a bulk operation item. ## Fields **recordId** `UUID!` *No description provided in the schema yet.* **success** `Boolean!` *No description provided in the schema yet.* **errorCode** `String` Machine-readable failure reason when success is false. **errorMessage** `String` *No description provided in the schema yet.* # BulkOperationItem Source: https://docs.fluz.app/api-reference/types/bulk-operation-item The result of one target within a bulk job. **Object** The result of one target within a bulk job. For write operations, resultResourceId reconciles the item back to the Fluz resource it produced; errorCode/errorMessage explain a failure. ## Fields **externalReferenceId** `String` Your reference id for the target user, when available. **accountId** `UUID` The accountId of the target user. **itemIndex** `Int!` The position of this item within the job, matching submission order. **status** `BulkItemStatus!` *No description provided in the schema yet.* **resultResourceId** `String` The id of the Fluz resource this item produced (e.g. a transfer), when it succeeded. **errorCode** `String` Machine-readable failure reason when status is FAILED or SKIPPED. **errorMessage** `String` *No description provided in the schema yet.* **metadataResults** `[BulkMetadataEditResult!]` Per-edit results for an UPDATE\_TRANSACTION\_METADATA item. Null for every other operation. # BulkOperationItems Source: https://docs.fluz.app/api-reference/types/bulk-operation-items A page of per-target results for a bulk job. **Object** A page of per-target results for a bulk job. ## Fields **items** `[BulkOperationItem!]!` *No description provided in the schema yet.* **totalCount** `Int!` The total number of items on the job across all statuses — not filtered by the status argument. Read from the job's own counters, not a live count of this page's filter. **hasNextPage** `Boolean!` *No description provided in the schema yet.* **nextCursor** `String` Cursor to fetch the next page when hasNextPage is true; null otherwise. Cursors are opaque — pass back exactly what was returned. # BulkTargetError Source: https://docs.fluz.app/api-reference/types/bulk-target-error The per-target failure detail for a bulk operation. **Object** The per-target failure detail for a bulk operation. ## Fields **code** `BulkTargetErrorCode!` *No description provided in the schema yet.* **message** `String!` *No description provided in the schema yet.* # BulkTargetFailure Source: https://docs.fluz.app/api-reference/types/bulk-target-failure A target user that could not be included in a bulk read result. **Object** A target user that could not be included in a bulk read result. A failed target never fails the whole request — it is reported here instead. Shared by the bulk balances and transactions reads. ## Fields **externalReferenceId** `String` Your reference id for the target user, when available. **accountId** `UUID` The accountId of the target user, when the target was resolved. **code** `BulkTargetErrorCode!` Machine-readable failure reason. Enum rather than String so the closed set is discoverable by introspection and codegen: the resolver can only emit an unresolvable-target reason (TARGET\_NOT\_CONNECTED, INVALID\_TARGET\_IDENTIFIER) or a scope-denial reason (INSUFFICIENT\_SCOPE, ACCOUNT\_NOT\_PERMITTED). Adding a fifth reason at either call site means adding it here too. **message** `String!` Human-readable failure detail. # BulkTransaction Source: https://docs.fluz.app/api-reference/types/bulk-transaction One transaction in a bulk transactions result, attributed to the connected user it belongs to. **Object** One transaction in a bulk transactions result, attributed to the connected user it belongs to. ## Fields **externalReferenceId** `String` Your reference id for the user this transaction belongs to. **accountId** `UUID!` The accountId of the user this transaction belongs to. **recordId** `UUID` Unique identifier for the transaction record. **transactionType** `String` The type of transaction (e.g., GIFT\_CARD\_PURCHASE, DEPOSIT, WITHDRAWAL, TRANSFER). **amount** `Float!` The primary transaction amount. **status** `TransactionStatus` Current status of the transaction. **createdAt** `DateTime` When the transaction was created. **memo** `String` User-defined memo, populated only when includeMetadata is true. **transactionCategory** `String` Category name, populated only when includeMetadata is true. # BulkTransactions Source: https://docs.fluz.app/api-reference/types/bulk-transactions Transactions across your connected users as a single flat, keyset-paginated list, newest first. **Object** Transactions across your connected users as a single flat, keyset-paginated list, newest first. Each transaction carries the externalReferenceId/accountId of the user it belongs to; targets that were skipped or failed appear in errors (first page only). No per-user cap — page with nextCursor/after to retrieve every transaction in the requested window. ## Fields **transactions** `[BulkTransaction!]!` Every returned transaction across all target users, attributed per row. **errors** `[BulkTargetFailure!]!` A bounded sample of skipped/failed targets (at most 100), returned on the first page only. A failed target never fails the request. Use failedTargetCount for the true total — errors is capped so a large sweep cannot return a huge payload. **totalCount** `Int` Total transactions matching the request across ALL pages (every target, whole window). Costs a second full scan of the window, so it is opt-in: null unless includeTotalCount is true, and only ever returned on the first page (no cursor), since it does not change as you page. **targetCount** `Int!` The number of target users addressed by the request. **succeededTargetCount** `Int!` The number of target users this request was authorized to read: resolved, and holding the scopes the operation requires. Independent of what this page happens to contain — an authorized user with no transactions in the window still counts here. **failedTargetCount** `Int!` The number of target users that could not be read — unresolvable, or missing the required scopes. Returned on every page, unlike errors, which carries a bounded sample of those same failures on the first page only. **nextCursor** `String` Opaque cursor for the next page of transactions; null when this is the last page. Pass it back as the "after" argument to continue. Keyset-based, so it is stable across concurrent writes. **hasMore** `Boolean!` Whether more transactions remain beyond this page. Call again with after = nextCursor. # BulkUpdateErpTransactionMetadataResult Source: https://docs.fluz.app/api-reference/types/bulk-update-erp-transaction-metadata-result **Object** *No description provided in the schema yet.* ## Fields **succeeded** `[UUID!]!` *No description provided in the schema yet.* **failed** `[ErpBulkFailure!]!` *No description provided in the schema yet.* # Business Source: https://docs.fluz.app/api-reference/types/business Represents the Business who has access to the account. **Object** Represents the Business who has access to the account. ## Fields **businessId** `UUID` The businessId associated with the account. **businessName** `String` The name of the business. # BusinessCategory Source: https://docs.fluz.app/api-reference/types/business-category BusinessCategory represents a business category with its associated sub-categories. **Object** BusinessCategory represents a business category with its associated sub-categories. ## Fields **businessCategoryId** `UUID!` Unique identifier for the business category. **categoryName** `String!` Name of the category. **categoryDescription** `String` Description of the category. **businessSubCategories** `[BusinessSubCategory!]!` List of sub-categories associated with this category. # BusinessLookupResult Source: https://docs.fluz.app/api-reference/types/business-lookup-result Represents a business recipient. **Object** Represents a business recipient. ## Fields **recipientId** `UUID!` The account ID of the recipient. **companyName** `String` Formatted company name with DBA in parentheses. Format: "Business Name (DBA Name)" or just "Business Name" if no DBA. **state** `String` State of the business. Derived from the business's legal address. # BusinessOwnerStatus Source: https://docs.fluz.app/api-reference/types/business-owner-status Snapshot of one business owner row for KYB UI (registered user, business user, or NRU). **Object** Snapshot of one business owner row for KYB UI (registered user, business user, or NRU). ## Fields **id** `UUID` Owner identity id when available. Present for registered owners (`user_id`) and provided business-user owners (`business_user_id`). `null` for invited owners that have not accepted their invite yet. **name** `String!` Derived display name from the linked identity records. **verificationType** `BusinessOwnerVerificationType!` What identity verification is expected for this owner (SSN, documents, or not required). **status** `ExternalBusinessOwnerStatus!` Current verification progress for this owner. # BusinessSubCategory Source: https://docs.fluz.app/api-reference/types/business-sub-category BusinessSubCategory represents a sub-category within a business category. **Object** BusinessSubCategory represents a sub-category within a business category. ## Fields **businessSubCategoryId** `UUID!` Unique identifier for the business sub-category. **subCategoryName** `String!` Name of the sub-category. # CardProvisioningUrl Source: https://docs.fluz.app/api-reference/types/card-provisioning-url **Object** *No description provided in the schema yet.* ## Fields **url** `String!` Short URL that, when opened on the end-user's mobile device, launches the Fluz App Clip (iOS) or Fluz app (Android) and initiates the wallet-provisioning flow for the targeted virtual card. Treat it as a bearer secret: it stays redeemable for a short grace window after first use. Surfaceable as a QR code, SMS / email link, or in-app button. **expiresAt** `String!` ISO-8601 timestamp at which the URL stops working (approximately 5 minutes after creation). Plan your delivery timing accordingly. # CashBalance Source: https://docs.fluz.app/api-reference/types/cash-balance CashBalance represents the balance of a user's cash. **Object** CashBalance represents the balance of a user's cash. ## Fields **availableBalance** `String` The available balance of the user's cash. **totalBalance** `String` The total balance of the user's cash. **pendingBalance** `String` The pending balance of the user's cash. **lifetimeBalance** `String` The lifetime balance of the user's cash. # CashBalanceDeposit Source: https://docs.fluz.app/api-reference/types/cash-balance-deposit CashBalanceDeposit represents a cash balance deposit, including details like amount, fee, and status. **Object** CashBalanceDeposit represents a cash balance deposit, including details like amount, fee, and status. ## Fields **cashBalanceDepositId** `UUID!` Unique identifier for the cash balance deposit. **depositDisplayId** `String!` Display identifier for the deposit, used for user-facing purposes. **depositAmount** `String!` The amount of the deposit. **depositFee** `String` Fee associated with the deposit, if applicable. **bankCardId** `UUID` Identifier of the bank card used for the deposit. **bankAccountId** `UUID` Identifier of the bank account used for the deposit. **paypalVaultId** `UUID` Identifier of the PayPal vault used for the deposit. **transactionDate** `DateTime!` Date and time when the transaction was made. **clearedDate** `DateTime` Date and time when the deposit cleared, if applicable. **status** `CashBalanceDepositStatus!` Current status of the deposit. **expectedClearedDate** `DateTime!` Expected date and time for the deposit to clear. **cashBalanceDepositType** `CashBalanceDepositType!` Type of the cash balance deposit. **cashBalanceSettlements** `[CashBalanceSettlement]` Associated settlements for the deposit. **memo** `String` Memo for the deposit. **transactionCategory** `String` Category name for the deposit. **attachmentUrl** `String` URL of the attachment associated with this deposit. # CashBalanceSettlement Source: https://docs.fluz.app/api-reference/types/cash-balance-settlement CashBalanceSettlement representing a settlement record of a cash balance. **Object** CashBalanceSettlement representing a settlement record of a cash balance. ## Fields **cashBalanceSettlementId** `UUID!` Unique identifier for the cash balance settlement. **cashBalanceDepositId** `UUID!` Identifier of the associated cash balance deposit. **availabilityType** `CashBalanceAvailabilityType!` Type of availability for the settlement (instant or standard). **status** `CashBalanceDepositStatus!` Current status of the cash balance settlement. # CloDetails Source: https://docs.fluz.app/api-reference/types/clo-details [CLO Only] Contains specific details about rates and periods for a Card Linked Offer. **Object** \[CLO Only] Contains specific details about rates and periods for a Card Linked Offer. This object is null for non-CLO offer types. ## Fields **currentRateType** `CloRateTypeEnum!` The type of rate currently active (REGULAR or PROMO) based on evaluation of all periods against the current time. **regularRate** `Float` The standard reward rate (%) for the offer. **promoRate** `Float` The promotional reward rate (%) potentially active during a PROMO period. **promoBaseRate** `Float` The reward rate (%) applied during a PROMO period *after* the promoMaxCap is exceeded. **promoMaxCap** `Float` The maximum purchase amount up to which the promoRate applies during a PROMO period. **applyPromoBaseRateAfterCap** `Boolean` If true, the promoBaseRate applies to the purchase amount exceeding the promoMaxCap during a PROMO period. **minimumPurchaseAmount** `Float` The minimum purchase amount required to qualify for the CLO reward, if any. **activePeriodStartDate** `String` The start date (YYYY-MM-DD) of the *currently active* offer period, if any. **activePeriodEndDate** `String` The end date (YYYY-MM-DD) of the *currently active* offer period, if any. **periods** `[CloPeriod!]!` List of all defined periods for this offer's rates (past, present, future). # CloPeriod Source: https://docs.fluz.app/api-reference/types/clo-period [CLO Only] Represents a specific time period during which a Card Linked Offer rate applies. **Object** \[CLO Only] Represents a specific time period during which a Card Linked Offer rate applies. ## Fields **id** `UUID!` *No description provided in the schema yet.* **offerRateType** `CloRateTypeEnum!` *No description provided in the schema yet.* **periodType** `String` *No description provided in the schema yet.* **startDate** `String` *No description provided in the schema yet.* **endDate** `String` *No description provided in the schema yet.* **startTime** `String` *No description provided in the schema yet.* **endTime** `String` *No description provided in the schema yet.* **daysOfWeek** `[String]` *No description provided in the schema yet.* **daysOfMonth** `[String]` *No description provided in the schema yet.* # CloseUserCashBalanceResponse Source: https://docs.fluz.app/api-reference/types/close-user-cash-balance-response Response type for the closeUserCashBalance mutation, returning a closed user cash balance and affected virtual card records **Object** Response type for the closeUserCashBalance mutation, returning a closed user cash balance and affected virtual card records ## Fields **closedUserCashBalance** `ClosedUserCashBalance` *No description provided in the schema yet.* **affectedVirtualCards** `[CloseUserCashBalanceResponseVirtualCard]` Virtual cards affected by the user cash balance closure. # CloseUserCashBalanceResponseVirtualCard Source: https://docs.fluz.app/api-reference/types/close-user-cash-balance-response-virtual-card Response type for a virtual card affected by user cash balance closure. **Object** Response type for a virtual card affected by user cash balance closure. ## Fields **virtualCardId** `UUID!` The ID associated with the virtual card. **virtualCardLast4** `String!` The last 4 digits of the virtual card number. # ClosedUserCashBalance Source: https://docs.fluz.app/api-reference/types/closed-user-cash-balance Response type for closing a user cash balance account. **Object** Response type for closing a user cash balance account. ## Fields **userCashBalanceId** `UUID!` Unique identifier for the user cash balance. **lifetimeCashBalance** `String!` Lifetime cash balance (cumulative total ever deposited). **nickname** `String` Custom nickname for the balance. **status** `UserCashBalanceStatus!` Status of the cash balance account. **createdAt** `DateTime!` Date and time when the account was created. **closedAt** `DateTime!` Date and time when the account was closed. # CreatePlaidLinkAddressResponse Source: https://docs.fluz.app/api-reference/types/create-plaid-link-address-response **Object** *No description provided in the schema yet.* ## Fields **addressId** `UUID!` *No description provided in the schema yet.* # CreateTransferResponse Source: https://docs.fluz.app/api-reference/types/create-transfer-response Represents the response returned from the createTransfer mutation. **Object** Represents the response returned from the createTransfer mutation. ## Fields **success** `Boolean!` *No description provided in the schema yet.* **message** `String!` *No description provided in the schema yet.* **transferId** `UUID` *No description provided in the schema yet.* # DeclinedTransaction Source: https://docs.fluz.app/api-reference/types/declined-transaction DeclinedTransaction represents an attempted transaction that was declined or failed. **Object** DeclinedTransaction represents an attempted transaction that was declined or failed. ## Fields **transactionId** `UUID!` Unique identifier of the declined transaction. **transactionType** `String!` Type of transaction that was declined, e.g. "Gift Card Purchase", "Virtual Card Purchase", "Add Money", "Withdrawal", "Transfer - Out", "Bill Payment Initiation", "Issue Virtual Card". **amount** `Float!` Total transaction amount that was attempted, in the transaction's currency. **fluzBalanceActivity** `Float!` Portion of the attempted amount that would have been funded from Fluz internal balances (cash/rewards/reserve/prepayment activity). Defaults to 0 when not applicable. **fluzAmount** `Float!` Portion of the attempted amount that would have been funded from Fluz internal balances (cash/rewards/reserve/prepayment activity). Defaults to 0 when not applicable. **Deprecated:** Use `fluzBalanceActivity` instead. **externalFundingSourceActivity** `Float!` Portion of the attempted amount that would have been funded from an external funding source (e.g. linked bank/card). Defaults to 0 when not applicable. **externalFundingAmount** `Float!` Portion of the attempted amount that would have been funded from an external funding source (e.g. linked bank/card). Defaults to 0 when not applicable. **Deprecated:** Use `externalFundingSourceActivity` instead. **currency** `String!` ISO 4217 currency code for the transaction amount. Defaults to "USD" when not specified on the record. **sourceRecordType** `String` Type of the originating source record that produced this declined transaction (e.g. the originating domain/entity type). **sourceRecordId** `String` Identifier of the originating source record that produced this declined transaction. **accountId** `String!` Identifier of the Fluz account that owns the transaction. **userId** `String` Identifier of the user who attempted the transaction. May be absent for account-level/system transactions. **source** `String` Source of funds / origin label for the transaction (e.g. funding source or originating party). **destination** `String` Destination of the transaction (e.g. merchant or recipient label). For merchant transactions this is typically the merchant. **status** `DeclinedTransactionStatus!` Outcome status of the transaction. One of DeclinedTransactionStatus: "DECLINED" or "FAILED". **merchantId** `String` Identifier of the merchant associated with the transaction, when known. **descriptorId** `String` Identifier of the merchant descriptor used to enrich merchant metadata. **liabilityId** `String` Identifier of the liability record associated with the transaction, when applicable. **merchantName** `String` Display name of the merchant. Populated from the transaction destination when a merchant is identified; otherwise null. **merchantCountry** `String` Country of the merchant. **merchantCity** `String` City of the merchant. **merchantState** `String` State/region of the merchant. **logoUrl** `String` URL of the logo to display for the transaction (typically the merchant/brand logo). **category** `String` Category/classification of the transaction or merchant. **cardLastFour** `String` Last four digits of the card used for the transaction. **cardDisplayName** `String` Human-friendly display name of the card used for the transaction. **virtualCardId** `String` Identifier of the virtual card involved in the transaction, when applicable. **virtualCardProgram** `String` Virtual card program/issuer label for the virtual card, when applicable. **Deprecated:** Obsolete. **channel** `String` Channel through which the transaction was initiated (e.g. app, web, integration). **externalFundingSourceType** `String` Type of the external funding source used (e.g. bank account, debit card). **externalFundingSourceId** `String` Identifier of the external funding source used. **fundingSourceSubtype** `String` Subtype of the funding source (e.g. checking vs. savings, card network subtype). **isCashBalanceUsed** `Boolean!` True if the user's cash balance was applied/attempted for this transaction. **isPrepaymentBalanceUsed** `Boolean!` True if the gift-card prepayment balance was applied/attempted for this transaction. **isRewardsBalanceUsed** `Boolean!` True if the rewards balance was applied/attempted for this transaction. **isReserveBalanceUsed** `Boolean!` True if the reserve balance was applied/attempted for this transaction. **transactionDateTime** `String!` Timestamp of the transaction, as an ISO 8601 datetime String (derived from the record's creation time). **Deprecated:** Use `createdAt` instead. **declineTitle** `String` Short, user-facing title summarizing the decline. **declineReason** `String` User-facing reason explaining why the transaction was declined. **declineDescription** `String` Longer user-facing description providing additional detail about the decline. **declineCtaText** `String` Call-to-action text suggesting the next step the user can take in response to the decline. **declineCategory** `String` Categorization of the decline (used to group/route decline messaging). **approvalCode** `String` Approval action configured for the decline code, when one is mapped. **offerId** `UUID` Identifier of the offer associated with the transaction, when applicable. **issuer** `String` Card issuer associated with the card/transaction. **bin** `String` Bank Identification Number (first digits) of the card used. **limitAmount** `String` Spend-limit amount relevant to the decline (e.g. the limit that was exceeded), as a String. **limitDuration** `String` Duration/window the spend limit applies to (e.g. per-transaction, daily, monthly). **lockOnNextUse** `String` Indicates whether the card is set to lock on next use. **lockDate** `String` Date associated with a card lock, when applicable. **bankAccountNickname** `String` Nickname of the bank account associated with the funding source, when derivable. **bankAccountLastFour** `String` Last four digits of the bank account associated with the funding source. **spendAccountNickname** `String` Nickname of the spend account used. **createdAt** `DateTime!` Timestamp when the transaction was created. **updatedAt** `DateTime!` Timestamp when the transaction was last updated. # DeclinedTransactionConnection Source: https://docs.fluz.app/api-reference/types/declined-transaction-connection Paginated response for getDeclinedTransactions query. **Object** Paginated response for getDeclinedTransactions query. ## Fields **transactions** `[DeclinedTransaction]` List of transactions. **totalCount** `Int!` Total count of transactions matching the filter. **hasNextPage** `Boolean!` Whether there are more results available. # DefaultFundingSource Source: https://docs.fluz.app/api-reference/types/default-funding-source DefaultFundingSource represents an account's default funding source: the primary payment method (a bank account, charged first) and the backup payment method (a bank card, charged if the primary can't be charged). **Object** DefaultFundingSource represents an account's default funding source: the primary payment method (a bank account, charged first) and the backup payment method (a bank card, charged if the primary can't be charged). ## Fields **primary** `BankAccount` The primary funding source (bank account) charged first for transactions. Null if no primary is set (or the stored primary is not a bank account). **backup** `BankCard` The backup funding source (bank card) charged if the primary can't be charged. Null if no backup is set (or the stored backup is not a bank card). # DepositCashBalanceResponse Source: https://docs.fluz.app/api-reference/types/deposit-cash-balance-response Response type for the depositCashBalance mutation, returning the resulting deposits and current user balances. **Object** Response type for the depositCashBalance mutation, returning the resulting deposits and current user balances. ## Fields **cashBalanceDeposits** `[CashBalanceDeposit]` List of cash balance deposits made as part of the mutation. **balances** `UserBalances` User's current balances. # ErpBulkFailure Source: https://docs.fluz.app/api-reference/types/erp-bulk-failure **Object** *No description provided in the schema yet.* ## Fields **transactionRecordId** `UUID!` *No description provided in the schema yet.* **error** `ErpError!` *No description provided in the schema yet.* # ErpError Source: https://docs.fluz.app/api-reference/types/erp-error A single per-item failure in bulkUpdateErpTransactionMetadata. **Object** A single per-item failure in bulkUpdateErpTransactionMetadata. ## Fields **code** `String!` *No description provided in the schema yet.* **message** `String!` *No description provided in the schema yet.* # ErpReferenceItem Source: https://docs.fluz.app/api-reference/types/erp-reference-item A chart-of-accounts entry, vendor, or customer imported from the connected accounting provider (e.g. **Object** A chart-of-accounts entry, vendor, or customer imported from the connected accounting provider (e.g. QuickBooks). ## Fields **referenceItemId** `UUID!` *No description provided in the schema yet.* **externalId** `String!` *No description provided in the schema yet.* **name** `String!` *No description provided in the schema yet.* **fullyQualifiedName** `String` *No description provided in the schema yet.* **accountType** `String` Only populated for chart-of-accounts entries. **accountSubType** `String` Only populated for chart-of-accounts entries. **active** `Boolean!` *No description provided in the schema yet.* **lastChangedAt** `DateTime` *No description provided in the schema yet.* # ErpReferenceItemConnection Source: https://docs.fluz.app/api-reference/types/erp-reference-item-connection **Object** *No description provided in the schema yet.* ## Fields **items** `[ErpReferenceItem!]!` *No description provided in the schema yet.* **totalCount** `Int!` *No description provided in the schema yet.* **hasNextPage** `Boolean!` *No description provided in the schema yet.* # ErpReferenceItemRef Source: https://docs.fluz.app/api-reference/types/erp-reference-item-ref A lightweight reference to a category, vendor, or customer attached to a transaction's ERP metadata. **Object** A lightweight reference to a category, vendor, or customer attached to a transaction's ERP metadata. ## Fields **referenceItemId** `UUID!` *No description provided in the schema yet.* **name** `String!` *No description provided in the schema yet.* # ErpTransactionMetadata Source: https://docs.fluz.app/api-reference/types/erp-transaction-metadata The current ERP-side categorization state of a transaction. **Object** The current ERP-side categorization state of a transaction. ## Fields **transactionMetadataId** `UUID!` *No description provided in the schema yet.* **transactionRecordId** `UUID!` *No description provided in the schema yet.* **category** `ErpReferenceItemRef` *No description provided in the schema yet.* **vendor** `ErpReferenceItemRef` *No description provided in the schema yet.* **customer** `ErpReferenceItemRef` *No description provided in the schema yet.* **isBillable** `Boolean` *No description provided in the schema yet.* **memo** `String` *No description provided in the schema yet.* **syncStatus** `ErpTransactionSyncStatus!` *No description provided in the schema yet.* **isSyncing** `Boolean!` *No description provided in the schema yet.* **updatedAt** `DateTime!` *No description provided in the schema yet.* # ErpTransactionMetadataConnection Source: https://docs.fluz.app/api-reference/types/erp-transaction-metadata-connection **Object** *No description provided in the schema yet.* ## Fields **items** `[ErpTransactionMetadata!]!` *No description provided in the schema yet.* **totalCount** `Int!` *No description provided in the schema yet.* **hasNextPage** `Boolean!` *No description provided in the schema yet.* # ErrorInfo Source: https://docs.fluz.app/api-reference/types/error-info Error information. **Object** Error information. ## Fields **message** `String!` Error message. **code** `String!` Error code. # GenerateUserAccessTokenResponse Source: https://docs.fluz.app/api-reference/types/generate-user-access-token-response Represents the response returned from the generateUserAccessToken mutation. **Object** Represents the response returned from the generateUserAccessToken mutation. Includes the access token and the scopes that the token grants. ## Fields **token** `String` The User Access JWT for API requests. **refreshToken** `String` The User Access refresh token for API requests. **scopes** `[ScopeType]` The list of scopes granted to the token. # GeneratedShareLink Source: https://docs.fluz.app/api-reference/types/generated-share-link GeneratedShareLink represents a single generated share link. **Object** GeneratedShareLink represents a single generated share link. ## Fields **senderAppId** `String` *No description provided in the schema yet.* **shareRequestBatchId** `String` *No description provided in the schema yet.* **shareRequestDisplayId** `String` *No description provided in the schema yet.* **shareRequestDetails** `ShareRequestDetails` *No description provided in the schema yet.* **shareObjectStatus** `ShareObjectStatus` *No description provided in the schema yet.* **recipientPhone** `String` *No description provided in the schema yet.* **recipientEmail** `String` *No description provided in the schema yet.* **linkExpirationDate** `DateTime` *No description provided in the schema yet.* **virtualCardId** `String` *No description provided in the schema yet.* **linkUrl** `String` *No description provided in the schema yet.* # GetBusinessResponse Source: https://docs.fluz.app/api-reference/types/get-business-response KYB summary for the business tied to the bearer token's accountId. **Object** KYB summary for the business tied to the bearer token's accountId. ## Fields **accountId** `UUID!` *No description provided in the schema yet.* **kybStatus** `ExternalKybStatus!` *No description provided in the schema yet.* **owners** `[BusinessOwnerStatus!]!` *No description provided in the schema yet.* # GetWalletResponse Source: https://docs.fluz.app/api-reference/types/get-wallet-response GetWalletResponse represents the response to the get wallet request. **Object** GetWalletResponse represents the response to the get wallet request. ## Fields **bankCards** `[BankCard]` *No description provided in the schema yet.* **bankAccounts** `[BankAccount]` *No description provided in the schema yet.* **paypalAccounts** `[Paypal]` *No description provided in the schema yet.* **blockedPaymentTypes** `[PaymentMethodType]` *No description provided in the schema yet.* **balances** `UserBalances` *No description provided in the schema yet.* **defaultFundingSource** `DefaultFundingSource` *No description provided in the schema yet.* # GiftCard Source: https://docs.fluz.app/api-reference/types/gift-card GiftCard represents a record of a gift card purchased by a user. **Object** GiftCard represents a record of a gift card purchased by a user. ## Fields **giftCardId** `UUID!` The ID associated with the gift card. **purchaseId** `UUID` The ID of the purchase that created this gift card. **purchaseDisplayId** `String` The display ID of the purchase that created this gift card. **purchaserUserId** `UUID!` The ID of the user who made the purchase. **endDate** `DateTime!` The expiration date of the gift card. **status** `GiftCardStatus!` The status of the Gift Card. **purchaseValue** `Float` The value the gift card was purchased at (its denomination / face value). **currentValue** `Float` The remaining balance on the gift card. **currency** `String` The ISO currency code of the gift card's value. **createdAt** `DateTime!` The time the gift card was created. **merchant** `Merchant` A connection to the merchant that the gift card can be redeemed at. **termsAndConditions** `String` The terms and conditions for this gift card's offer. **deliveryFormat** `DeliveryFormatType` The delivery format of the offer this gift card was purchased from. The active offer on a merchant may change over time, so the delivery format of the merchant's current offer is not necessarily the same as the delivery format of the offer used to purchase this gift card. # GiftCardCode Source: https://docs.fluz.app/api-reference/types/gift-card-code GiftCardCode contains the redemption credentials for a Merchant. **Object** GiftCardCode contains the redemption credentials for a Merchant. ## Fields **code** `String` *No description provided in the schema yet.* **pin** `String` *No description provided in the schema yet.* **url** `String` *No description provided in the schema yet.* **termsAndConditions** `String` *No description provided in the schema yet.* # LockVirtualCardResponse Source: https://docs.fluz.app/api-reference/types/lock-virtual-card-response **Object** *No description provided in the schema yet.* ## Fields **virtualCardId** `UUID!` *No description provided in the schema yet.* **locked** `Boolean!` *No description provided in the schema yet.* # Merchant Source: https://docs.fluz.app/api-reference/types/merchant Merchant is place that a Gift Card can be use, or can charge a user's VirtualCard. **Object** Merchant is place that a Gift Card can be use, or can charge a user's VirtualCard. ## Fields **merchantId** `UUID` *No description provided in the schema yet.* **name** `String` *No description provided in the schema yet.* **slug** `String` *No description provided in the schema yet.* **logoUrl** `String` *No description provided in the schema yet.* **faceplateUrl** `String` *No description provided in the schema yet.* **offers** `[Offer]` *No description provided in the schema yet.* **shortDescription** `String` A short marketing description of the merchant (e.g. product details, category summary). **categories** `[MerchantCategory!]!` The shopping categories this merchant belongs to, ordered by display priority. A merchant can belong to more than one category, and the list is empty when the merchant is uncategorized. # MerchantCategory Source: https://docs.fluz.app/api-reference/types/merchant-category MerchantCategory is a curated shopping category (e.g. Dining, Travel) that groups merchants. **Object** MerchantCategory is a curated shopping category (e.g. Dining, Travel) that groups merchants. ## Fields **categoryId** `UUID!` The unique identifier for the category. Pass it to [`getMerchants`](/api-reference/queries/get-merchants) as `categoryId` to list the merchants in this category. **name** `String!` The display name of the category, for example `Dining` or `Travel`. # MerchantCategoryCode Source: https://docs.fluz.app/api-reference/types/merchant-category-code MerchantCategoryCode represents the classifier for a business by the types of goods or services it provides. **Object** MerchantCategoryCode represents the classifier for a business by the types of goods or services it provides. ## Fields **code** `String` Four digit merchant category code. **displayDescription** `String` Display description. **description** `String` Mcc description. # ObjectChangeHistoryActor Source: https://docs.fluz.app/api-reference/types/object-change-history-actor Who performed an object change. **Object** Who performed an object change: the user who actually made the request or, when no user did, the system that made it, and the connected application the request came through, if any. ## Fields **userId** `UUID` The user who made the change. Null when it was made by a system or by Fluz support. **system** `String` The system that made the change when no user did, e.g. "virtual-card-service", or "cms" for a change made by Fluz support. Null when a user made the change. **applicationId** `UUID` The connected application the change came through, if any. **applicationName** `String` Application name as it was when the change happened. # ObjectChangeHistoryApproval Source: https://docs.fluz.app/api-reference/types/object-change-history-approval The approval that authorized, or is tracked by, an object change. **Object** The approval that authorized, or is tracked by, an object change. ## Fields **approvalId** `UUID!` The approval ID. **approvalType** `String!` The approval type, e.g. `VIRTUAL_CARD`, `GIFT_CARD`, or `ACCOUNT_ACCESS`. **status** `String!` The approval's current status. **requesterUserId** `UUID` The user who requested the approval. **approvers** `[ObjectChangeHistoryApprover!]!` Everyone asked to decide. See [`ObjectChangeHistoryApprover`](/api-reference/types/object-change-history-approver). # ObjectChangeHistoryApprover Source: https://docs.fluz.app/api-reference/types/object-change-history-approver One approver on the approval that authorized an object change. **Object** One approver on the approval that authorized an object change. ## Fields **approverUserId** `UUID` The user who was asked to decide. **decidedByUserId** `UUID` The user who actually submitted the decision. Null when not recorded. **status** `String!` This approver's status. **decidedAt** `DateTime` When the approver decided. **decisionChannel** `String` How the decision was submitted: LINK, REST or API. # ObjectChangeHistoryEvent Source: https://docs.fluz.app/api-reference/types/object-change-history-event One append-only history event for an object. **Object** One append-only history event for an object. ## Fields **id** `UUID!` The event ID. **objectType** `ObjectOwnerObjectType!` The type of the object the event belongs to. **objectId** `UUID!` The object the event belongs to. **changeType** `String!` OBJECT\_CREATED, FIELD\_CHANGED, OWNER\_\* / ACCESS\_\* events, or APPROVAL\_REQUESTED / APPROVAL\_APPROVED / APPROVAL\_DECLINED / APPROVAL\_EXPIRED. **actor** `ObjectChangeHistoryActor!` Who performed the change. See [`ObjectChangeHistoryActor`](/api-reference/types/object-change-history-actor). **subjectUserId** `UUID` The user the change was made for, when different from the actor (e.g. the cardholder). **approvalId** `UUID` The approval linked to this event, when there is one. **approval** `ObjectChangeHistoryApproval` The approval linked to this event. See [`ObjectChangeHistoryApproval`](/api-reference/types/object-change-history-approval). **change** `JSON` Event details: `{ field, old, new }` for field, owner, and access changes, and `{ approval_type, approval_code, decision_channel }` for approval events. See [The change payload](/features/activity-history#the-change-payload). **createdAt** `DateTime!` When the change happened. # ObjectOwner Source: https://docs.fluz.app/api-reference/types/object-owner Object ownership metadata returned from assign/transfer mutations. **Object** Object ownership metadata returned from assign/transfer mutations. ## Fields **objectOwnerId** `UUID!` *No description provided in the schema yet.* **accountId** `UUID!` *No description provided in the schema yet.* **objectType** `String!` *No description provided in the schema yet.* **objectId** `UUID!` *No description provided in the schema yet.* **userId** `UUID!` *No description provided in the schema yet.* **createdAt** `DateTime!` *No description provided in the schema yet.* **updatedAt** `DateTime!` *No description provided in the schema yet.* # ObjectOwnerError Source: https://docs.fluz.app/api-reference/types/object-owner-error **Object** *No description provided in the schema yet.* ## Fields **message** `String` *No description provided in the schema yet.* **code** `String` *No description provided in the schema yet.* # Offer Source: https://docs.fluz.app/api-reference/types/offer Offer is a discounted offer for a merchant. **Object** Offer is a discounted offer for a merchant. ## Fields **offeringMerchantId** `UUID!` *No description provided in the schema yet.* **offerId** `UUID!` *No description provided in the schema yet.* **exclusiveRateId** `UUID` *No description provided in the schema yet.* **type** `OfferType!` *No description provided in the schema yet.* **deliveryFormat** `DeliveryFormatType` *No description provided in the schema yet.* **barcodeType** `BarcodeTypeEnum` *No description provided in the schema yet.* **hasStockInfo** `Boolean` *No description provided in the schema yet.* **offerRates** `[OfferRate]` *No description provided in the schema yet.* **denominationsType** `OfferDenominationType` *No description provided in the schema yet.* **stockInfo** `[StockInfoType]!` StockInfo is an object where the key:value pairs represent the amount in stock for each denomination (key = denomination; value = amount in stock). It's only available if the offer `hasStockInfo`. This requires Fluz to confirm the inventory, response time varies by vendors. **cloDetails** `CloDetails` \[CLO Only] Contains specific details about rates and periods for a Card Linked Offer. Returns null for non-CLO offer types. **termsAndConditions** `String` The terms and conditions text for this offer (e.g. legal language, restrictions, expiration policies). # OfferRate Source: https://docs.fluz.app/api-reference/types/offer-rate OfferRate is the rate of a specific offer. **Object** OfferRate is the rate of a specific offer. ## Fields **maxUserRewardValue** `Float` *No description provided in the schema yet.* **cashbackVoucherRewardValue** `Float` *No description provided in the schema yet.* **boostRewardValue** `Float` *No description provided in the schema yet.* **displayBoostReward** `Boolean` *No description provided in the schema yet.* **denominations** `[String]` *No description provided in the schema yet.* **allowedPaymentMethods** `[String]` *No description provided in the schema yet.* # Paypal Source: https://docs.fluz.app/api-reference/types/paypal Paypal represents a PayPal account added by a user. **Object** Paypal represents a PayPal account added by a user. ## Fields **paypalVaultId** `UUID!` The ID of the PayPal account. **userId** `UUID!` *No description provided in the schema yet.* **status** `PayPalAccountStatus!` *No description provided in the schema yet.* **email** `String!` *No description provided in the schema yet.* # PlaidBankAccount Source: https://docs.fluz.app/api-reference/types/plaid-bank-account Safe persisted Plaid bank account metadata. **Object** Safe persisted Plaid bank account metadata. Plaid access tokens, account numbers, and routing numbers are never returned. ## Fields **bankInstitutionAuthId** `UUID!` *No description provided in the schema yet.* **platformItemId** `String!` *No description provided in the schema yet.* **bankInstitutionName** `String` *No description provided in the schema yet.* **bankInstitutionLogo** `String` *No description provided in the schema yet.* **authStatus** `String` *No description provided in the schema yet.* **authAction** `String` *No description provided in the schema yet.* **authType** `String` *No description provided in the schema yet.* **bankAccountId** `UUID!` *No description provided in the schema yet.* **accountName** `String` *No description provided in the schema yet.* **status** `String` *No description provided in the schema yet.* **lastFour** `String` *No description provided in the schema yet.* **type** `String` *No description provided in the schema yet.* **subtype** `String` *No description provided in the schema yet.* **nickname** `String` *No description provided in the schema yet.* # PlaidBankAccountBalanceRefreshResponse Source: https://docs.fluz.app/api-reference/types/plaid-bank-account-balance-refresh-response Result of a rate-limited realtime refresh for one Plaid bank account's owning institution. **Object** Result of a rate-limited realtime refresh for one Plaid bank account's owning institution. ## Fields **status** `Int` *No description provided in the schema yet.* **balances** `[PlaidBankBalance!]!` *No description provided in the schema yet.* # AddBankCardInput Source: https://docs.fluz.app/api-reference/types/add-bank-card-input Input type for adding a bank card to the user's wallet. **Input object** Input type for adding a bank card to the user's wallet. ## Input fields **cardNumber** `String!` *required* The card number. **expirationMonth** `String!` *required* The expiration month in the format MM. **expirationYear** `String!` *required* The expiration year in the format YYYY. **cvv** `String!` *required* The three- or four-digit security numbers. **cardholderName** `String!` *required* The name of the cardholder. **billingAddress** `UserAddressInput` Create the card with a new address. **userAddressId** `UUID` Create the card with an existing user address. **preferredMerchantCategoryCode** `String` Create the card with preferred mcc category. **nickname** `String` Create the card with nickname. # AddVirtualCardAddressInput Source: https://docs.fluz.app/api-reference/types/add-virtual-card-address-input Input type for saving a billing address before issuing virtual cards. **Input object** Input type for saving a billing address before issuing virtual cards. ## Input fields **billingAddress** `VirtualCardBillingAddressInput!` *required* Billing address to save on the target account for the caller or authorized cardholder. **authUserId** `UUID` Optional authorized user ID (UAC role assignment ID). When provided, the address is saved for that user's underlying user\_id while remaining attached to the caller's account. # BulkDepositItemInput Source: https://docs.fluz.app/api-reference/types/bulk-deposit-item-input One deposit for a DEPOSIT_CASH_BALANCE bulk operation. **Input object** One deposit for a DEPOSIT\_CASH\_BALANCE bulk operation. The funding source must belong to the target user: exactly one of bankCardId or bankAccountId. ## Input fields **externalReferenceId** `String!` *required* The externalReferenceId of the target user to deposit to. The set of deposit targets is derived from these items; each target may appear at most once. **amount** `Float!` *required* The amount to deposit (must be positive, at most 2 decimal places). **bankCardId** `UUID` The target user's bank card to fund the deposit. Provide this or bankAccountId, not both. **bankAccountId** `UUID` The target user's bank account to fund the deposit. Provide this or bankCardId, not both. **userCashBalanceId** `UUID` The target user's spend account to deposit into. Required when the user's grant permits multiple spend accounts; defaults to the single permitted account or the user's default. **memo** `String` An optional memo recorded on this deposit. # BulkDepositOptionsInput Source: https://docs.fluz.app/api-reference/types/bulk-deposit-options-input Options for the DEPOSIT_CASH_BALANCE operation. **Input object** Options for the DEPOSIT\_CASH\_BALANCE operation. Each item names its own target user, so targetSpec is ignored — the deposit targets are derived from the items. Each target may appear at most once. ## Input fields **items** `[BulkDepositItemInput!]!` *required* *No description provided in the schema yet.* # BulkExportOptionsInput Source: https://docs.fluz.app/api-reference/types/bulk-export-options-input Options for the read-export operations (GET_TRANSACTIONS_EXPORT, GET_BALANCES_EXPORT). **Input object** Options for the read-export operations (GET\_TRANSACTIONS\_EXPORT, GET\_BALANCES\_EXPORT). ## Input fields **createdGte** `DateTime` Only include records created at or after this time. Defaults to 90 days before createdLte. Applies to GET\_TRANSACTIONS\_EXPORT; GET\_BALANCES\_EXPORT is a point-in-time snapshot and ignores the window. Must be strictly before createdLte. **createdLte** `DateTime` Only include records created at or before this time. Defaults to now. **includeMetadata** `Boolean` Also include memo and category metadata for each record. Slower; defaults to false. # BulkMetadataEditInput Source: https://docs.fluz.app/api-reference/types/bulk-metadata-edit-input One metadata change for a single transaction. **Input object** One metadata change for a single transaction. Only memo and transactionCategory can be edited. A field set to null clears it; an omitted field is left untouched. ## Input fields **recordId** `UUID!` *required* The record id of the transaction to update. Must belong to the item's target user. **memo** `String` User-defined memo, max 255 characters. Null clears the memo; omit to leave unchanged. **transactionCategory** `String` Category name. Null clears the category; omit to leave unchanged. # BulkMetadataItemInput Source: https://docs.fluz.app/api-reference/types/bulk-metadata-item-input All metadata edits for one target user in an UPDATE_TRANSACTION_METADATA bulk operation. **Input object** All metadata edits for one target user in an UPDATE\_TRANSACTION\_METADATA bulk operation. ## Input fields **externalReferenceId** `String!` *required* The externalReferenceId of the target user whose transactions to edit. The set of targets is derived from these items; each target may appear at most once. **edits** `[BulkMetadataEditInput!]!` *required* *No description provided in the schema yet.* # BulkMetadataOptionsInput Source: https://docs.fluz.app/api-reference/types/bulk-metadata-options-input Options for the UPDATE_TRANSACTION_METADATA operation. **Input object** Options for the UPDATE\_TRANSACTION\_METADATA operation. Each item names its own target user, so targetSpec is ignored — the targets are derived from the items (each at most once). At most 100 edits in total per submission; edits are applied synchronously and the returned job is already complete — read per-edit results with getBulkOperationItems. ## Input fields **items** `[BulkMetadataItemInput!]!` *required* *No description provided in the schema yet.* # BulkPaginationInput Source: https://docs.fluz.app/api-reference/types/bulk-pagination-input Pagination input for bulk API listings. **Input object** Pagination input for bulk API listings. The bulk API allows larger pages than the rest of the API: the maximum (and default) page size is 100. ## Input fields **limit** `Int` The number of items to return per page. Maximum and default are 100. **offset** `Int` The number of items to skip. # BulkTargetSpecInput Source: https://docs.fluz.app/api-reference/types/bulk-target-spec-input Selects the users a bulk operation applies to. **Input object** Selects the users a bulk operation applies to. The maximum number of targets depends on the operation — see the description of the query or mutation you are calling. ## Input fields **mode** `BulkTargetMode!` *required* *No description provided in the schema yet.* **targets** `[String!]` The externalReferenceIds of the users to target. Required for SELECTED mode; ignored for ALL\_CONNECTED. Duplicates are removed; result order follows this list. # BulkTransferEndpointInput Source: https://docs.fluz.app/api-reference/types/bulk-transfer-endpoint-input One endpoint (the source or destination) of a bulk transfer. **Input object** One endpoint (the source or destination) of a bulk transfer. Set EXACTLY ONE of operator or externalReferenceId: operator true means your application's configured payout account; externalReferenceId names a connected user. Both-set or neither-set is rejected. ## Input fields **operator** `Boolean` Set true to use your application's operator payout account as this endpoint. Mutually exclusive with externalReferenceId. **externalReferenceId** `String` The externalReferenceId of a connected user to use as this endpoint. Mutually exclusive with operator. **userCashBalanceId** `UUID` The user's spend account for this side of the transfer. Only valid alongside externalReferenceId (the operator endpoint's account is fixed by your payout configuration). Required when the item would otherwise be ambiguous — a source user whose grant permits several spend accounts, or a destination user with several accounts at the source's sponsor bank and no default. Omitted, it defaults to the single eligible account, else the user's default one. # BulkTransferItemInput Source: https://docs.fluz.app/api-reference/types/bulk-transfer-item-input One transfer for a CREATE_TRANSFER bulk operation: move the amount from the 'from' endpoint to the 'to' endpoint. **Input object** One transfer for a CREATE\_TRANSFER bulk operation: move the amount from the "from" endpoint to the "to" endpoint. Any combination of operator and connected users is allowed (operator to user, user to operator, user to user), except that from and to must differ. The source user's grant must include MAKE\_PAYOUT\_TRANSFER\_SEND; the destination user only needs to be connected. ## Input fields **from** `BulkTransferEndpointInput!` *required* The endpoint the money moves from. **to** `BulkTransferEndpointInput!` *required* The endpoint the money moves to. **amount** `Float!` *required* The amount to transfer (must be positive, at most 2 decimal places). **memo** `String` An optional memo recorded on this transfer (max 255 characters). # BulkTransferOptionsInput Source: https://docs.fluz.app/api-reference/types/bulk-transfer-options-input Options for the CREATE_TRANSFER operation. **Input object** Options for the CREATE\_TRANSFER operation. Each item names its own source and destination, so targetSpec is ignored for this operation — the participants are derived from the items. ## Input fields **items** `[BulkTransferItemInput!]!` *required* *No description provided in the schema yet.* # BulkUpdateErpTransactionMetadataItem Source: https://docs.fluz.app/api-reference/types/bulk-update-erp-transaction-metadata-item **Input object** *No description provided in the schema yet.* ## Input fields **transactionRecordId** `UUID!` *required* *No description provided in the schema yet.* **erpTransactionMetadata** `UpdateErpTransactionMetadataInput!` *required* *No description provided in the schema yet.* # BusinessLegalAddressInput Source: https://docs.fluz.app/api-reference/types/business-legal-address-input Business legal address input. **Input object** Business legal address input. ## Input fields **streetAddressLine1** `String!` *required* Street address line 1. **streetAddressLine2** `String` Street address line 2 (optional). **city** `String!` *required* City. **state** `String!` *required* State/province **postalCode** `String!` *required* Postal code. **country** `String!` *required* Country name. Must be a valid country name from ISO 3166 standard that is allowed for KYB (Know Your Business) registration. Certain countries are restricted and not allowed for business registration (e.g., North Korea, Iran, Russia, Cuba, Syria, and others). Examples of allowed countries: "Canada", "Germany", "France". # BusinessLookupInput Source: https://docs.fluz.app/api-reference/types/business-lookup-input Input for looking up business recipients by company name. **Input object** Input for looking up business recipients by company name. ## Input fields **companyName** `String!` *required* Lookup by company name (case-insensitive). Searches both business name and DBA name using OR logic. Returns all matching businesses. # BusinessOwnerInput Source: https://docs.fluz.app/api-reference/types/business-owner-input Business owner information for registration. **Input object** Business owner information for registration. At least one of `emailAddress` or `phoneNumber` must be provided. Exactly one owner in the roster must be the control person (`isControlPerson: true`). Exactly one owner must match the bearer token user by email or phone (the applicant). When the applicant's `isUsPerson` is `true`, they must already have successful SSN (CIP) verification on file. When `isUsPerson` is `false`, the applicant must have KYC status `PASS` or `PASS_INTERNATIONAL`. `isUsPerson` is required on EVERY owner in the roster, including the applicant, invited owners and non-beneficial owners. Full KYC fields (`dob`, `address`) are required for owners who are not the applicant, not invited, and either a beneficial owner (>= 25% ownership) or the control person. * US persons (`isUsPerson: true`): provide `lastFourSsnDigits` (exactly 4 digits) with `dob` and `address` (SSN path). * Non-US persons (`isUsPerson: false`): provide `dob` and `address` without `lastFourSsnDigits` (document path). Invited owners (`isInvited: true`) and non-beneficial owners (\< 25%, not control person) only require base contact fields. ## Input fields **firstName** `String!` *required* Owner's first name. **lastName** `String!` *required* Owner's last name. **emailAddress** `String` Owner's email address. Either `emailAddress` or `phoneNumber` must be provided. **phoneNumber** `String` Owner's phone number. Either `emailAddress` or `phoneNumber` must be provided. Must include a country calling code (e.g. `+1 555 123 4567` or `+44 20 7946 0958`); numbers without a country code (e.g. a bare 10-digit US number) are rejected. Formatting characters (spaces, dashes, parentheses) are stripped automatically. **title** `String!` *required* Owner's title in the company. **ownershipPercentage** `Int!` *required* Percentage of ownership (0-100). **isControlPerson** `Boolean!` *required* Whether this owner is the control person for the business. Exactly one owner must be `true`. **isInvited** `Boolean!` *required* Whether this owner will be invited to complete verification later. Invited owners require only base contact fields. **isUsPerson** `Boolean` Whether the owner is a US person. REQUIRED on every owner. For the applicant: `true` requires successful SSN (CIP) verification on file; `false` requires KYC `PASS` / `PASS_INTERNATIONAL`. For full-KYC co-owners: `true` requires `lastFourSsnDigits` (SSN path); `false` uses document verification. **lastFourSsnDigits** `String` Last 4 digits of SSN. Required when `isUsPerson` is `true` for full-KYC owners. Omit for non-US persons (document verification still requires `dob` and `address`). **dob** `String` Owner's date of birth (ISO 8601 date, e.g. `1980-01-15`). Required when full KYC is required. **address** `OwnerAddressInput` Owner's address. Required when full KYC is required (both SSN and document verification paths). # CloseUserCashBalanceFundingSource Source: https://docs.fluz.app/api-reference/types/close-user-cash-balance-funding-source CloseUserCashBalanceFundingSource represents new funding source for all user cash balance-associated virtual cards. **Input object** CloseUserCashBalanceFundingSource represents new funding source for all user cash balance-associated virtual cards. ## Input fields **bankAccountId** `UUID` The bank account ID. **userCashBalanceId** `UUID` The spend account ID. **primaryFundingSource** `VirtualCardFundingSource` Primary Funding Source for Virtual Card. # CloseUserCashBalanceInput Source: https://docs.fluz.app/api-reference/types/close-user-cash-balance-input Input type for closing a user cash balance account. **Input object** Input type for closing a user cash balance account. ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **userCashBalanceId** `UUID!` *required* Unique identifier for the user's *closing* cash balance. **newFundingSource** `CloseUserCashBalanceFundingSource` New funding source for all user cash balance-associated virtual cards **lockAllVirtualCards** `Boolean` Whether we should lock all user cash balance-associated virtual cards **transferUserCashBalanceId** `UUID` Unique identifier for the user's *transfer destination* cash balance. # CompletePlaidLinkInput Source: https://docs.fluz.app/api-reference/types/complete-plaid-link-input Input submitted by the frontend after Plaid Link onSuccess returns a public token. **Input object** Input submitted by the frontend after Plaid Link onSuccess returns a public token. For relink, include the same platformItemId used to create the Link token. BYOI app relinks should use completePlaidRelink instead; update mode keeps the original access token. ## Input fields **publicToken** `String!` *required* *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* # CompletePlaidRelinkInput Source: https://docs.fluz.app/api-reference/types/complete-plaid-relink-input Finish update mode after Link onSuccess. **Input object** Finish update mode after Link onSuccess. A public token is not required. ## Input fields **platformItemId** `String!` *required* *No description provided in the schema yet.* # CreatePlaidLinkAddressInput Source: https://docs.fluz.app/api-reference/types/create-plaid-link-address-input Input for creating the address required to complete a Plaid bank link. **Input object** Input for creating the address required to complete a Plaid bank link. ## Input fields **bankInstitutionAuthId** `UUID!` *required* *No description provided in the schema yet.* **address** `PlaidLinkAddressInput!` *required* *No description provided in the schema yet.* # CreatePlaidLinkTokenInput Source: https://docs.fluz.app/api-reference/types/create-plaid-link-token-input Input for creating a Plaid Link token. **Input object** Input for creating a Plaid Link token. Omit platformItemId for a new link; provide it to relink a disconnected connection. For web Link, omit deviceOs. For native Link, pass IOS or ANDROID to enable Plaid OAuth redirect handling. ## Input fields **deviceOs** `PlaidDeviceOs` *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* # CreatePlaidRelinkTokenInput Source: https://docs.fluz.app/api-reference/types/create-plaid-relink-token-input Start update mode for an existing Item, for an app with BYOI Plaid. **Input object** Start update mode for an existing Item, for an app with BYOI Plaid. The Item must belong to this app's Plaid integration or have been linked with Fluz Plaid. ## Input fields **platformItemId** `String!` *required* *No description provided in the schema yet.* **deviceOs** `PlaidDeviceOs` *No description provided in the schema yet.* # CreateTransferInput Source: https://docs.fluz.app/api-reference/types/create-transfer-input Represents the request payload for creating a transfer. **Input object** Represents the request payload for creating a transfer. The sender is always determined from the authentication credentials. ## Input fields **idempotencyKey** `UUID!` *required* A unique client-generated UUID to ensure a request is processed only once. **amount** `Float!` *required* The amount to transfer (must be positive). **destination** `TransferDestination` The destination for the transfer. For public applications, defaults to the application operator if omitted. **bankCardId** `UUID` Bank card ID to fund the transfer. At most one funding source allowed. **bankAccountId** `UUID` Bank account ID to fund the transfer via ACH. At most one funding source allowed. **paypalVaultId** `UUID` PayPal vault ID to fund the transfer via PayPal. At most one funding source allowed. **memo** `String` Optional memo to attach to this transfer transaction. **transactionCategory** `String` Optional category name to attach to this transfer transaction. **attachmentId** `String` ID of a previously uploaded attachment (PDF or PNG). Use the /api/v1/file-upload/transaction-memo-attachment endpoint to upload. **useRewardsBalance** `Boolean` When true, fund the transfer from the sender's rewards balance (in addition to the resolved spend/cash balance). Defaults to false (cash-only). # CreateUserCashBalanceInput Source: https://docs.fluz.app/api-reference/types/create-user-cash-balance-input Input type for creating a new user cash balance account. **Input object** Input type for creating a new user cash balance account. ## Input fields **nickname** `String!` *required* Nickname for the cash balance account. # CreateVirtualCardBulkOrderInput Source: https://docs.fluz.app/api-reference/types/create-virtual-card-bulk-order-input **Input object** *No description provided in the schema yet.* ## Input fields **offerId** `UUID!` *required* The Offer Id of Virtual Card Offer. This offer will be applied to all cards created in this order. **orderItems** `[VirtualCardOrderItemInput!]!` *required* A list of configurations for the virtual cards to be created. Each item in this list represents a group of one or more cards with identical settings. The total number of cards across all items cannot exceed 100. # CreateVirtualCardInput Source: https://docs.fluz.app/api-reference/types/create-virtual-card-input Input type for creating virtual cards. **Input object** Input type for creating virtual cards. ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **spendLimit** `Float!` *required* The maximum amount that you can charge to the card. You will only be charged for the amount you actually used. **spendLimitDuration** `VirtualCardSpendLimitDuration` Card limit duration type. Default Lifetime. **lockDate** `String` The date when the card will be locked. The default is 47 months. **lockCardNextUse** `Boolean` The setting to lock the card after next use. The default is false. **cardNickname** `String` The card's nickname. **primaryFundingSource** `VirtualCardFundingSource` Primary Funding Source for Virtual Card. **bankAccountId** `UUID` The unique identifier for the bank account. **offerId** `UUID` The Offer Id of Virtual Card Offer. Use getVirtualCardOffers to fetch a list of active offers. **userCashBalanceId** `UUID` The unique identifier of the user cash balance id **billingAddress** `VirtualCardBillingAddressInput` A new billing address to register with the card program before issuing the card. A UserAddress (type: BILLING) is created if one does not already exist with the same street / city / state / postal code. Ignored when userAddressId is also provided. **userAddressId** `UUID` An existing UserAddress ID belonging to the caller's account to use as the billing address. Takes precedence over billingAddress when both are provided. **usePrepaymentBalance** `Boolean` When false, the card will not draw from the prepaid (gift card) balance. Defaults to true to preserve the existing behavior where the card may be funded by prepaid funds in addition to the specified spend account. **useRewardsBalance** `Boolean` When false, the card will not draw from the rewards balance. Defaults to true to preserve the existing behavior where the card may be funded by rewards in addition to the specified spend account. **authUserId** `UUID` Optional authorized user ID (UAC role assignment ID) to create the virtual card on behalf of. The assignment must exist on the caller's account and be ACTIVE. When provided, the downstream access token is issued for the underlying user so the resulting card is created on their behalf. **memo** `String` An optional note to attach to the transaction. **transactionCategory** `String` An optional category name. A matching category will be found or created for the account. # DeactivateVCShareLinksInput Source: https://docs.fluz.app/api-reference/types/deactivate-vcshare-links-input Input type for deactivateVCShareLinks. **Input object** Input type for deactivateVCShareLinks. ## Input fields **shareRequestBatchIds** `[String]` The list of batch ID's to deactivate. Each share link included in these batches will be deactivated. **shareRequestDisplayIds** `[String]` The list of display ID's to deactivate. Only the share links with these display ID's will be deactivated. # DeclinedTransactionFilterInput Source: https://docs.fluz.app/api-reference/types/declined-transaction-filter-input Input filter for querying transactions. **Input object** Input filter for querying transactions. ## Input fields **transactionId** `[UUID]` Filter by specific transaction record IDs. **status** `[DeclinedTransactionStatus]` Filter by declined transaction status. **amount** `Float` Filter by exact amount. **amountGte** `Float` Filter by minimum amount (greater than or equal). **amountLte** `Float` Filter by maximum amount (less than or equal). **fluzAmount** `Float` Filter by exact fluz amount. **fluzAmountGte** `Float` Filter by minimum fluz amount (greater than or equal). **fluzAmountLte** `Float` Filter by maximum fluz amount (less than or equal). **createdGte** `DateTime` Filter by creation date (greater than or equal). **createdLte** `DateTime` Filter by creation date (less than or equal). **updatedGte** `DateTime` Filter by update date (greater than or equal). **updatedLte** `DateTime` Filter by update date (less than or equal). **merchantId** `[UUID]` Filter by merchant IDs. **merchant** `[String]` Filter by merchant names (destination field). **transactionType** `[String]` Filter by transaction types. **channel** `[String!]` Filter by channel. **category** `[String]` Filter by transaction category. **virtualCardProgram** `[String]` Filter by virtual card program. **Deprecated:** Obsolete. **virtualCardId** `[UUID]` Filter by specific virtual card IDs. **fundingSource** `[String]` Filter by funding source names. **userCashBalanceId** `[UUID]` Filter by spend account IDs. **liabilityId** `UUID` Filter by liability ID (for bill payments). # DeleteBankCardInput Source: https://docs.fluz.app/api-reference/types/delete-bank-card-input Input type for deleting a bank card. **Input object** Input type for deleting a bank card. ## Input fields **bankCardId** `UUID!` *required* The ID of the bank card to delete. # DepositCashBalanceInput Source: https://docs.fluz.app/api-reference/types/deposit-cash-balance-input Input type for depositing cash balance, specifying the amount and deposit type. **Input object** Input type for depositing cash balance, specifying the amount and deposit type. ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **amount** `Float!` *required* The amount deposited. **depositType** `CashBalanceDepositType` Type of the deposit being made. **merchantCategoryCode** `Int` A four-digit number that classifies a business by the type of products or services it offers **bankAccountId** `UUID` Identifier of the bank account for the deposit. **bankCardId** `UUID` Identifier of the bank card for the deposit. **paypalVaultId** `UUID` Identifier of the PayPal ID for the deposit. **userCashBalanceId** `UUID` Identifier of the UserCashBalance for the deposit. Ignored if depositType is not CASH\_BALANCE **memo** `String` Memo for the deposit. Limited to 255 characters. **transactionCategory** `String` Optional category name to attach to this deposit transaction. **attachmentId** `String` ID of a previously uploaded attachment (PDF or PNG). Use the /api/v1/file-upload/transaction-memo-attachment endpoint to upload. # EditVirtualCardInput Source: https://docs.fluz.app/api-reference/types/edit-virtual-card-input Input type for edit virtual card. **Input object** Input type for edit virtual card. ## Input fields **virtualCardId** `UUID!` *required* The virtual card to update. **spendLimit** `Float` The maximum amount that you can charge to the card. You will only be charged for the amount you actually used. **spendLimitDuration** `VirtualCardSpendLimitDuration` Card limit duration type. **lockDate** `String` The date when the card will be locked. **lockCardNextUse** `Boolean` The setting to lock the card after next use. **cardNickname** `String` The card's nickname. **bankAccountId** `UUID` The bank account ID. **userCashBalanceId** `UUID` The spend account ID. **primaryFundingSource** `VirtualCardFundingSource` Primary Funding Source for Virtual Card. # ErpReferenceItemFilter Source: https://docs.fluz.app/api-reference/types/erp-reference-item-filter **Input object** *No description provided in the schema yet.* ## Input fields **q** `String` Case-insensitive substring match on the item's name. **active** `Boolean` Defaults to true (active items only). **accountType** `[String!]` Chart-of-accounts entries only. **accountSubType** `[String!]` Chart-of-accounts entries only. **sortKey** `ErpReferenceItemSortKey` Defaults to NAME. **sortOrder** `SortOrder` Defaults to ASC. # ErpTransactionMetadataFilter Source: https://docs.fluz.app/api-reference/types/erp-transaction-metadata-filter **Input object** *No description provided in the schema yet.* ## Input fields **syncStatus** `ErpTransactionSyncStatus` *No description provided in the schema yet.* **updatedAfter** `DateTime` *No description provided in the schema yet.* **transactionRecordIds** `[UUID!]` *No description provided in the schema yet.* **sortKey** `ErpTransactionMetadataSortKey` Defaults to UPDATED\_AT. **sortOrder** `SortOrder` Defaults to DESC. # FilterByInput Source: https://docs.fluz.app/api-reference/types/filter-by-input Input for applying fine-grained filters on offer attributes. **Input object** Input for applying fine-grained filters on offer attributes. This will filter the offers *within* a merchant. If all of a merchant's offers are filtered out, the merchant itself will be excluded. ## Input fields **deliveryFormat** `DeliveryFormatType` Filters gift card offers by their delivery format. # PlaidBankAccountSpendPower Source: https://docs.fluz.app/api-reference/types/plaid-bank-account-spend-power Safe spend power calculated for one Plaid-linked bank account. **Object** Safe spend power calculated for one Plaid-linked bank account. ## Fields **spendPowerId** `UUID!` *No description provided in the schema yet.* **bankAccountId** `UUID!` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID!` *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* **spendPower** `Float` *No description provided in the schema yet.* **availableSpendPower** `Float` *No description provided in the schema yet.* **lastRecordedBalance** `Float` *No description provided in the schema yet.* **pendingTransactions** `Float` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* # PlaidBankBalance Source: https://docs.fluz.app/api-reference/types/plaid-bank-balance Latest safe Plaid bank balance for a persisted bank account. **Object** Latest safe Plaid bank balance for a persisted bank account. ## Fields **bankBalanceId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID!` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* **amount** `Float` *No description provided in the schema yet.* **current** `Float` *No description provided in the schema yet.* **source** `String` *No description provided in the schema yet.* **trigger** `String` *No description provided in the schema yet.* **status** `String` *No description provided in the schema yet.* **type** `String` *No description provided in the schema yet.* **startedAt** `DateTime` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* # PlaidBankConnectionRefreshResponse Source: https://docs.fluz.app/api-reference/types/plaid-bank-connection-refresh-response Result of a cached refresh for all Plaid connections owned by the authenticated account. **Object** Result of a cached refresh for all Plaid connections owned by the authenticated account. ## Fields **status** `Int` *No description provided in the schema yet.* **balances** `[PlaidBankBalance!]!` *No description provided in the schema yet.* **verifyMembers** `[PlaidVerifyMember!]!` *No description provided in the schema yet.* # PlaidBankTransaction Source: https://docs.fluz.app/api-reference/types/plaid-bank-transaction Safe persisted bank transaction populated by identity-service from Plaid. **Object** Safe persisted bank transaction populated by identity-service from Plaid. ## Fields **historicalBankTransactionId** `UUID!` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* **transactionId** `String` *No description provided in the schema yet.* **transactionStatus** `String` *No description provided in the schema yet.* **transactionType** `String` *No description provided in the schema yet.* **transactionDate** `DateTime` *No description provided in the schema yet.* **amount** `Float` *No description provided in the schema yet.* **currencyCode** `String` *No description provided in the schema yet.* **description** `String` *No description provided in the schema yet.* **merchantName** `String` *No description provided in the schema yet.* **category** `String` *No description provided in the schema yet.* **pending** `Boolean` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* # PlaidBankTransactionPage Source: https://docs.fluz.app/api-reference/types/plaid-bank-transaction-page **Object** *No description provided in the schema yet.* ## Fields **totalCount** `Int!` *No description provided in the schema yet.* **transactions** `[PlaidBankTransaction!]!` *No description provided in the schema yet.* # PlaidIntegration Source: https://docs.fluz.app/api-reference/types/plaid-integration The application's own Plaid credentials. **Object** The application's own Plaid credentials. Secrets are never returned. ## Fields **plaidIntegrationId** `ID!` *No description provided in the schema yet.* **name** `String!` *No description provided in the schema yet.* **environment** `PlaidIntegrationEnvironment!` *No description provided in the schema yet.* **status** `PlaidIntegrationStatus!` *No description provided in the schema yet.* **validatedAt** `DateTime` *No description provided in the schema yet.* # PlaidIntegrationResponse Source: https://docs.fluz.app/api-reference/types/plaid-integration-response **Object** *No description provided in the schema yet.* ## Fields **change** `PlaidIntegrationChange!` *No description provided in the schema yet.* **plaidIntegration** `PlaidIntegration` *No description provided in the schema yet.* # PlaidLinkCompletionResponse Source: https://docs.fluz.app/api-reference/types/plaid-link-completion-response Safe public result of completing a Plaid Link flow. **Object** Safe public result of completing a Plaid Link flow. Store platformItemId for future relinks. Access tokens, public tokens, and identity-match details are never returned. ## Fields **requiresAddress** `Boolean!` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **newlyLinkedBankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **bankInstitutionName** `String` *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* **bankAccounts** `[PlaidLinkedBankAccount!]!` *No description provided in the schema yet.* # PlaidLinkTokenResponse Source: https://docs.fluz.app/api-reference/types/plaid-link-token-response Response containing the token required to initialize Plaid Link. **Object** Response containing the token required to initialize Plaid Link. Use linkToken immediately; do not store it. ## Fields **linkToken** `String!` *No description provided in the schema yet.* **expiration** `DateTime` *No description provided in the schema yet.* **requestId** `String` *No description provided in the schema yet.* **mode** `PlaidLinkMode!` *No description provided in the schema yet.* # PlaidLinkedBankAccount Source: https://docs.fluz.app/api-reference/types/plaid-linked-bank-account Linked bank account returned after identity-service completes Plaid ingestion. **Object** Linked bank account returned after identity-service completes Plaid ingestion. ## Fields **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **bankName** `String` *No description provided in the schema yet.* **lastFour** `String` *No description provided in the schema yet.* **type** `String` *No description provided in the schema yet.* **subtype** `String` *No description provided in the schema yet.* # PlaidVerifyMember Source: https://docs.fluz.app/api-reference/types/plaid-verify-member Plaid member that requires relinking after a refresh attempt. **Object** Plaid member that requires relinking after a refresh attempt. ## Fields **platform** `String` *No description provided in the schema yet.* **platformItemId** `String` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* # PostKycVerificationResponse Source: https://docs.fluz.app/api-reference/types/post-kyc-verification-response Response type for the postKycVerification mutation. **Object** Response type for the postKycVerification mutation. ## Fields **status** `UserVerificationStatus!` The resulting verification status: `APPROVED`, `DECLINED`, `DUPLICATE` when duplicate detection matched another user's verification, or `ERROR`. See [UserVerificationStatus](/api-reference/types/user-verification-status). **verificationId** `String` The identifier of the verification record created — or previously created, when the submission is an idempotent replay. **message** `String` The response message, if any. When the user was already KYC verified, this notes that the verification was recorded without a status update. # ProgramLimits Source: https://docs.fluz.app/api-reference/types/program-limits Represents the bank program spent limits. **Object** Represents the bank program spent limits. ## Fields **dailyLimit** `String` The bank program daily spend limit. **weeklyLimit** `String` The bank program weekly spend limit. **monthlyLimit** `String` The bank program monthly spend limit. # RefreshUserAccessTokenResponse Source: https://docs.fluz.app/api-reference/types/refresh-user-access-token-response Represents the response returned from the generateUserAccessToken mutation. **Object** Represents the response returned from the generateUserAccessToken mutation. Includes the access token and the scopes that the token grants. ## Fields **token** `String` The User Access JWT for API requests. **scopes** `[ScopeType]` The list of scopes granted to the token. # RegisterBusinessResponse Source: https://docs.fluz.app/api-reference/types/register-business-response Response from business registration. **Object** Response from business registration. On success: returns accountId and kybStatus. On error: returns success: false and error information. ## Fields **accountId** `UUID` The account ID of the newly created business (present on success). **kybStatus** `String` The KYB status of the business (present on success). **success** `Boolean` Indicates if the registration was successful (present on error). **error** `ErrorInfo` Error information if registration failed (present on error). # RegisterUserError Source: https://docs.fluz.app/api-reference/types/register-user-error Error details for registerUser mutation failures. **Object** Error details for registerUser mutation failures. ## Fields **message** `String` Brief error message describing the failure. **code** `String` Error code identifying the specific error type. # RegisterUserResponse Source: https://docs.fluz.app/api-reference/types/register-user-response Represents the response returned from the registerUser mutation. **Object** Represents the response returned from the registerUser mutation. Includes the success flag. ## Fields **success** `Boolean` Flag representing the success of calling registerUser mutation. **userId** `UUID` *No description provided in the schema yet.* **accountId** `UUID` *No description provided in the schema yet.* **billingAddressId** `UUID` *No description provided in the schema yet.* **status** `RegisterUserStatus` Registration outcome for applications in consent mode, which Fluz enables per application. Absent for direct registration. **authorizationUrl** `String` Link the application shows to the person to sign up or log in with the submitted phone number and approve the application's scopes. Approval fails if they sign in with a different number. Send the link only to that person. Returned with status PENDING\_CONSENT. **expiresAt** `String` ISO 8601 time after which authorizationUrl no longer works. Call registerUser again for a new link. Consent mode only. **error** `RegisterUserError` Error object containing message and error code when registration fails. # RemoveAuthorizedUserResponse Source: https://docs.fluz.app/api-reference/types/remove-authorized-user-response Response returned from the removeAuthorizedUser mutation. **Object** Response returned from the removeAuthorizedUser mutation. ## Fields **success** `Boolean!` Whether the operation succeeded. **authUserId** `UUID` The authorized user ID (UAC role assignment ID) that was deactivated. **status** `UACRoleStatusType` The updated status of the role assignment. **error** `AuthorizedUserError` Error details when the operation fails. # RemovePlaidBankInstitutionResponse Source: https://docs.fluz.app/api-reference/types/remove-plaid-bank-institution-response **Object** *No description provided in the schema yet.* ## Fields **removed** `Boolean!` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID!` *No description provided in the schema yet.* # RequestApprovalError Source: https://docs.fluz.app/api-reference/types/request-approval-error Error details returned when an approval request cannot be submitted. **Object** Error details returned when an approval request cannot be submitted. ## Fields **code** `String!` *No description provided in the schema yet.* **message** `String!` *No description provided in the schema yet.* # RequestApprovalResponse Source: https://docs.fluz.app/api-reference/types/request-approval-response Response returned from request approval mutations. **Object** Response returned from request approval mutations. ## Fields **success** `Boolean!` *No description provided in the schema yet.* **messageId** `String` *No description provided in the schema yet.* **error** `RequestApprovalError` *No description provided in the schema yet.* # RequestDocumentVerificationLinkResponse Source: https://docs.fluz.app/api-reference/types/request-document-verification-link-response Response type for user verification request **Object** Response type for user verification request ## Fields **userId** `UUID` The user ID being verified **verificationType** `String` The type of verification being performed **verificationId** `String` The unique identifier for this verification request **verificationUrl** `String` The URL where the verification can be performed **status** `String` The verification status. **message** `String` The response message if any. # RequestOwnerDocumentVerificationLinkResponse Source: https://docs.fluz.app/api-reference/types/request-owner-document-verification-link-response Response from requesting an owner document verification link. **Object** Response from requesting an owner document verification link. ## Fields **success** `Boolean!` Indicates if the request was successful. **id** `UUID` Business owner ID. **verificationLink** `String` Shareable verification link for the business owner to complete identity verification. # RewardsBalance Source: https://docs.fluz.app/api-reference/types/rewards-balance RewardsBalance represents the balance of a user's rewards. **Object** RewardsBalance represents the balance of a user's rewards. ## Fields **availableBalance** `String` The available balance of the user's rewards. **totalBalance** `String` The total balance of the user's rewards. **lifetimeBalance** `String` The lifetime balance of the user's rewards. # Seat Source: https://docs.fluz.app/api-reference/types/seat Represents the network position held by the entity. **Object** Represents the network position held by the entity. ## Fields **seatId** `UUID` The seat id associated with the account. **seatNumber** `String` The string value of seat position. # SetVirtualCardPINResponse Source: https://docs.fluz.app/api-reference/types/set-virtual-card-pinresponse Shows whether setting the PIN on eligible virtual cards was successful. **Object** Shows whether setting the PIN on eligible virtual cards was successful. ## Fields **success** `Boolean` Returns whether the operation was successfully queued. **pinError** `String` Returns if an error occurred while trying to validate your PIN. # ShareLinkRecipientRegistrationResult Source: https://docs.fluz.app/api-reference/types/share-link-recipient-registration-result Per-recipient outcome for REGISTER_USER share link generation. **Object** Per-recipient outcome for REGISTER\_USER share link generation. ## Fields **index** `Int!` The position of this result within the request's `recipientRegistrations` array, so you can match a result back to the input entry it came from. **emailAddress** `String` The email address supplied for this recipient in `recipientRegistrations`. **phoneNumber** `String` The phone number supplied for this recipient in `recipientRegistrations`. **userId** `UUID` The Fluz user ID of the placeholder user that was created or reused for this recipient. Absent when `status` is `FAILED`. **status** `ShareLinkRecipientRegistrationStatus!` Whether Fluz created a new placeholder user, reused an existing one, or failed to register this recipient. **error** `String` The failure reason. Present only when `status` is `FAILED`. # ShareRequest Source: https://docs.fluz.app/api-reference/types/share-request ShareRequest represents a response from creating a share link. **Object** ShareRequest represents a response from creating a share link. ## Fields **shareLinks** `[String]` The hosted URLs generated by this request, one per `quantity`, each of the form `https://fluz.app/virtual-prepaid-card/{share_request_id}`. Distribute these exactly as returned — do not rewrite or re-shorten them. **recipientRegistrationResults** `[ShareLinkRecipientRegistrationResult]` Per-recipient registration outcomes, populated only when the request's `shareMethod` is `REGISTER_USER`. Contains one entry per `recipientRegistrations` input entry, in the same order. Absent for other `shareMethod` values. # ShareRequestDetails Source: https://docs.fluz.app/api-reference/types/share-request-details ShareRequestDetails represents the configuration settings of the share request. **Object** ShareRequestDetails represents the configuration settings of the share request. ## Fields **objectType** `ShareObjectType!` *No description provided in the schema yet.* **cardType** `ShareCardType!` *No description provided in the schema yet.* **cardLimit** `Int!` *No description provided in the schema yet.* **offerId** `String!` *No description provided in the schema yet.* **daysUntilExpiration** `Int` *No description provided in the schema yet.* **quantity** `Int!` *No description provided in the schema yet.* **shareMethod** `ShareMethodType!` *No description provided in the schema yet.* **recipientListPhone** `[String]` *No description provided in the schema yet.* **recipientListEmail** `[String]` *No description provided in the schema yet.* **recipientUserIds** `[UUID]` *No description provided in the schema yet.* **userCashBalanceId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **bankCardId** `UUID` *No description provided in the schema yet.* **usePrepaymentBalance** `Boolean` *No description provided in the schema yet.* **useRewardsBalance** `Boolean` *No description provided in the schema yet.* # SpendAccountPdfDocument Source: https://docs.fluz.app/api-reference/types/spend-account-pdf-document A PDF artifact containing ACH routing and account instructions for funding a spend account's virtual account number, returned as a base64-encoded string for the client to decode and render or download. **Object** A PDF artifact containing ACH routing and account instructions for funding a spend account's virtual account number, returned as a base64-encoded string for the client to decode and render or download. ## Fields **pdfBase64** `String!` Base64-encoded contents of the payment instructions PDF. **fileName** `String!` Suggested file name for the PDF, e.g. when saving or downloading it on the client. # SpendAccountVirtualAccountNumber Source: https://docs.fluz.app/api-reference/types/spend-account-virtual-account-number A virtual account number (ACH routing + account number) tied to a spend account. **Object** A virtual account number (ACH routing + account number) tied to a spend account. A spend account may have multiple active virtual account numbers, one of which is marked primary. ## Fields **virtualAccountNumberId** `UUID!` Unique identifier for the virtual account number. **userCashBalanceId** `UUID!` Identifier of the spend account (cash balance) this virtual account number belongs to. **achRoutingNumber** `String!` The ACH routing number for the virtual account number. **accountNumber** `String!` The full account number for the virtual account number. Every reveal of the full number is audit-logged. **last4** `String!` The last four digits of the account number. **nickname** `String` Optional custom nickname for the virtual account number. **status** `VirtualAccountNumberStatus!` Status of the virtual account number. **isPrimary** `Boolean!` Whether this is the primary virtual account number for the spend account. **debitControlMode** `VirtualAccountNumberDebitControlMode!` Which debits are permitted against this virtual account number. **createdAt** `DateTime!` Date and time when the virtual account number was created. # StockInfoFixedType Source: https://docs.fluz.app/api-reference/types/stock-info-fixed-type StockInfoFixedType describes the available denominations for fixed offers, and the amount in stock for each denomination. **Object** StockInfoFixedType describes the available denominations for fixed offers, and the amount in stock for each denomination. ## Fields **denomination** `String` *No description provided in the schema yet.* **availableStock** `String` *No description provided in the schema yet.* # StockInfoVariableType Source: https://docs.fluz.app/api-reference/types/stock-info-variable-type StockInfoVariableType describes the available denomination range for variable offers. **Object** StockInfoVariableType describes the available denomination range for variable offers. ## Fields **minDenomination** `String` *No description provided in the schema yet.* **maxDenomination** `String` *No description provided in the schema yet.* **description** `String` *No description provided in the schema yet.* # SupervisionAssignment Source: https://docs.fluz.app/api-reference/types/supervision-assignment **Object** *No description provided in the schema yet.* ## Fields **id** `UUID!` *No description provided in the schema yet.* **accountId** `UUID!` *No description provided in the schema yet.* **supervisorUserId** `UUID!` *No description provided in the schema yet.* **superviseeUserId** `UUID!` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* # SupervisionAssignmentError Source: https://docs.fluz.app/api-reference/types/supervision-assignment-error **Object** *No description provided in the schema yet.* ## Fields **message** `String` *No description provided in the schema yet.* **code** `String` *No description provided in the schema yet.* # SupervisionAssignmentResponse Source: https://docs.fluz.app/api-reference/types/supervision-assignment-response **Object** *No description provided in the schema yet.* ## Fields **success** `Boolean!` *No description provided in the schema yet.* **assignment** `SupervisionAssignment` *No description provided in the schema yet.* **error** `SupervisionAssignmentError` *No description provided in the schema yet.* # Transaction Source: https://docs.fluz.app/api-reference/types/transaction Transaction represents a comprehensive record of all account activity including purchases, deposits, withdrawals, transfers, and payouts. **Object** Transaction represents a comprehensive record of all account activity including purchases, deposits, withdrawals, transfers, and payouts. Fields mirror the standardized transaction payload one-for-one, in the same order. Some fields are aliases kept for backwards compatibility and are marked as deprecated — prefer the replacement named in each deprecation reason. ## Fields **transactionId** `UUID` Unique identifier for the transaction record. **recordId** `UUID` Unique identifier for the transaction record. **Deprecated:** Use `transactionId` instead. **user** `String!` The display name of the user associated with this transaction. **accountId** `UUID!` The ID of the account that owns this transaction. **userId** `UUID` The ID of the user that initiated this transaction. **transactionType** `String` The type of transaction (e.g., GIFT\_CARD\_PURCHASE, DEPOSIT, WITHDRAWAL, TRANSFER). **amount** `Float!` The primary transaction amount. **initialAmount** `Float` Initial authorized amount for a virtual card transaction, when applicable. **destination** `String` The destination of the transaction (merchant name, recipient, etc.). **source** `String` The source of the transaction (funding source, sender, etc.). **externalFundingSourceActivity** `Float` Activity from external funding sources (bank cards, bank accounts). **fluzBalanceActivity** `Float` Activity from Fluz internal balances (cash, rewards, prepayment). **fee** `Float` Transaction fee amount. **cashback** `Float` Cashback earned from this transaction. **giftCardPrepaymentBalanceAvailableBalance** `Float` Available balance in gift card prepayment after this transaction. **giftCardPrepaymentBalanceTotalBalance** `Float` Total balance in gift card prepayment after this transaction. **cashBalanceAvailableBalance** `Float` Available cash balance after this transaction. **cashBalanceTotalBalance** `Float` Total cash balance after this transaction. **seatBalanceAvailableBalance** `Float` Available rewards (seat) balance after this transaction. **seatBalanceTotalBalance** `Float` Total rewards (seat) balance after this transaction. **reserveBalanceAvailableBalance** `Float` Available reserve balance after this transaction. **reserveBalanceTotalBalance** `Float` Total reserve balance after this transaction. **otherCashBalanceAvailableBalance** `Float` Available balance in other cash accounts after this transaction. **otherCashBalanceTotalBalance** `Float` Total balance in other cash accounts after this transaction. **status** `TransactionStatus` Current status of the transaction. **referenceId** `String` External reference ID (e.g., purchase display ID, deposit ID). **description** `String` Human-readable description of the transaction. **note** `String` Additional notes about the transaction. **memo** `String` User-defined memo attached to this transaction. **transactionCategory** `String` User-defined category attached to this transaction. **attachmentUrl** `String` URL of the attachment associated with this transaction. **category** `String` Transaction category (e.g., Shopping, Travel, Bills). **cardLastFour** `String` Last four digits of the card used (if applicable). **cardDisplayName** `String` Display name of the card used (if applicable). **originalCurrencyAmount** `Float` Original amount in foreign currency (for international transactions). **currency** `String` ISO 4217 currency code of the original amount (e.g., EUR, GBP). **originalCurrencyCode** `String` ISO 4217 currency code of the original amount (e.g., EUR, GBP). **Deprecated:** Use `currency` instead. **conversionRate** `Float` Currency conversion rate applied. **merchantId** `UUID` ID of the merchant associated with this transaction. **merchantName** `String` Display name of the merchant, resolved from descriptor enrichment when available. **merchantCountry** `String` Country of the merchant, resolved from virtual card details or descriptor enrichment. **merchantState** `String` State/region of the merchant, resolved from virtual card details or descriptor enrichment. **merchantCity** `String` City of the merchant, resolved from virtual card details or descriptor enrichment. **merchantIndustry** `String` Industry classification of the merchant, resolved from descriptor enrichment. **merchantCategoryCode** `Int` Merchant category code (MCC), resolved from the virtual card event log, the merchant's primary MCC, or the descriptor's MCC, in that priority order. **merchantCategoryDescription** `String` Human-readable description for the resolved merchant category code. **merchantCategoryLogoUrl** `String` Logo URL associated with the resolved merchant category code. **merchantCategoryImageUrl** `String` Image URL associated with the resolved merchant category code. **descriptorId** `UUID` ID of the transaction descriptor. **virtualCardId** `UUID` Identifier of the virtual card involved in the transaction, when applicable. **cashbackRate** `Float` Cashback rate percentage applied. **bonusCashbackRate** `Float` Bonus cashback rate percentage applied. **channel** `String` Channel through which the transaction was initiated. **sourceType** `String` Type of the funding source used. **logoUrl** `String` Logo URL for the merchant or source. **platformInstitutionLogo** `String` Logo URL for the banking institution (for bank-funded transactions). **challengeLogoUrl** `String` Logo URL for challenges/milestones (for bonus transactions). **invitedAccountId** `UUID` Account ID of invited user (for referral-related transactions). **expectedClearedDate** `DateTime` Expected date when pending transaction will be cleared. **liabilityId** `UUID` ID of associated liability (for bill payments). **isPrepaymentBalanceUsed** `Boolean` Whether the gift-card prepayment balance was applied to this transaction. **isGiftCardBalanceAffected** `Boolean` Whether the gift-card prepayment balance was applied to this transaction. **Deprecated:** Use `isPrepaymentBalanceUsed` instead. **isCashBalanceUsed** `Boolean` Whether the cash balance was applied to this transaction. **isCashBalanceAffected** `Boolean` Whether the cash balance was applied to this transaction. **Deprecated:** Use `isCashBalanceUsed` instead. **isRewardsBalanceUsed** `Boolean` Whether the rewards (seat) balance was applied to this transaction. **isSeatBalanceAffected** `Boolean` Whether the rewards (seat) balance was applied to this transaction. **Deprecated:** Use `isRewardsBalanceUsed` instead. **isReserveBalanceUsed** `Boolean` Whether the reserve balance was applied to this transaction. **isReserveBalanceAffected** `Boolean` Whether the reserve balance was applied to this transaction. **Deprecated:** Use `isReserveBalanceUsed` instead. **isRefundedBySystem** `Boolean` Whether a virtual card transaction was automatically refunded by the system. **cashBalanceDepositIds** `[UUID]` IDs of the cash balance deposits related to this transaction. **depositIds** `[UUID]` IDs of the deposits related to this transaction. **withdrawIds** `[UUID]` IDs of related withdrawals **transferId** `UUID` ID of the transfer record (for P2P transfers). **connectedAppId** `String` ID of the application transaction was triggered from **connectedAppName** `String` Name of the application transaction was triggered from **digitalWalletToken** `String` Digital wallet token **digitalWalletProvider** `String` Digital wallet provider **usedUserCashBalanceId** `UUID` ID of the specific user cash balance used for this transaction. **spendAccountNickname** `String` Nickname of the spend account used. Falls back to "Main account" when no specific nickname is set. **createdAt** `DateTime` Timestamp when the transaction was created. **updatedAt** `DateTime` Timestamp when the transaction was last updated. **erpMetadata** `ErpTransactionMetadata` The current ERP-side categorization state of this transaction. Null when the account has no active ERP connection or when this transaction has no ERP metadata yet. # TransactionConnection Source: https://docs.fluz.app/api-reference/types/transaction-connection Paginated response for transactions query. **Object** Paginated response for transactions query. ## Fields **transactions** `[Transaction]` List of transactions. **totalCount** `Int!` Total count of transactions matching the filter. **hasNextPage** `Boolean!` Whether there are more results available. # TransferInternalBalanceResponse Source: https://docs.fluz.app/api-reference/types/transfer-internal-balance-response Response type for the transferInternalBalance mutation, returning the deposit, withdrawal records and updated balances. **Object** Response type for the transferInternalBalance mutation, returning the deposit, withdrawal records and updated balances. ## Fields **cashBalanceDeposit** `CashBalanceDeposit!` Cash balance deposit made as part of the mutation. **withdraw** `Withdraw!` Withdrawal transaction record created. **balances** `UserBalances` User's current balances after the transfer. # TransferObjectOwnerResponse Source: https://docs.fluz.app/api-reference/types/transfer-object-owner-response **Object** *No description provided in the schema yet.* ## Fields **success** `Boolean!` *No description provided in the schema yet.* **objectOwnerId** `UUID` *No description provided in the schema yet.* **accountId** `UUID` *No description provided in the schema yet.* **objectType** `String` *No description provided in the schema yet.* **objectId** `UUID` *No description provided in the schema yet.* **userId** `UUID` *No description provided in the schema yet.* **createdAt** `DateTime` *No description provided in the schema yet.* **updatedAt** `DateTime` *No description provided in the schema yet.* **error** `ObjectOwnerError` *No description provided in the schema yet.* # UnlockVirtualCardResponse Source: https://docs.fluz.app/api-reference/types/unlock-virtual-card-response **Object** *No description provided in the schema yet.* ## Fields **virtualCardId** `UUID!` *No description provided in the schema yet.* **unlocked** `Boolean!` *No description provided in the schema yet.* # UpdatedTransaction Source: https://docs.fluz.app/api-reference/types/updated-transaction UpdatedTransaction is the transaction record returned by the updateTransactionMetadata mutation: the persisted transaction row plus the memo/category/attachment just written. **Object** UpdatedTransaction is the transaction record returned by the updateTransactionMetadata mutation: the persisted transaction row plus the memo/category/attachment just written. It is kept separate from Transaction, which mirrors the standardized read payload returned by getTransactions. ## Fields **recordId** `UUID` Unique identifier for the transaction record. **user** `String!` The display name of the user associated with this transaction. **accountId** `UUID!` The ID of the account that owns this transaction. **userId** `UUID` The ID of the user that initiated this transaction. **transactionType** `String` The type of transaction (e.g., GIFT\_CARD\_PURCHASE, DEPOSIT, WITHDRAWAL, TRANSFER). **amount** `Float!` The primary transaction amount. **destination** `String` The destination of the transaction (merchant name, recipient, etc.). **source** `String` The source of the transaction (funding source, sender, etc.). **externalFundingSourceActivity** `Float` Activity from external funding sources (bank cards, bank accounts). **fluzBalanceActivity** `Float` Activity from Fluz internal balances (cash, rewards, prepayment). **fee** `Float` Transaction fee amount. **cashback** `Float` Cashback earned from this transaction. **giftCardPrepaymentBalanceAvailableBalance** `Float` Available balance in gift card prepayment after this transaction. **giftCardPrepaymentBalanceTotalBalance** `Float` Total balance in gift card prepayment after this transaction. **cashBalanceAvailableBalance** `Float` Available cash balance after this transaction. **cashBalanceTotalBalance** `Float` Total cash balance after this transaction. **seatBalanceAvailableBalance** `Float` Available rewards (seat) balance after this transaction. **seatBalanceTotalBalance** `Float` Total rewards (seat) balance after this transaction. **reserveBalanceAvailableBalance** `Float` Available reserve balance after this transaction. **reserveBalanceTotalBalance** `Float` Total reserve balance after this transaction. **otherCashBalanceAvailableBalance** `Float` Available balance in other cash accounts after this transaction. **otherCashBalanceTotalBalance** `Float` Total balance in other cash accounts after this transaction. **status** `TransactionStatus` Current status of the transaction. **referenceId** `String` External reference ID (e.g., purchase display ID, deposit ID). **description** `String` Human-readable description of the transaction. **note** `String` Additional notes about the transaction. **category** `String` Transaction category (e.g., Shopping, Travel, Bills). **cardLastFour** `String` Last four digits of the card used (if applicable). **cardDisplayName** `String` Display name of the card used (if applicable). **originalCurrencyAmount** `Float` Original amount in foreign currency (for international transactions). **originalCurrencyCode** `String` Currency code of the original amount (e.g., EUR, GBP). **conversionRate** `Float` Currency conversion rate applied. **merchantId** `UUID` ID of the merchant associated with this transaction. **descriptorId** `UUID` ID of the transaction descriptor. **virtualCardProgram** `String` Virtual card program used (e.g., LITHIC, MARQETA, HIGHNOTE\_CFSB). **cashbackRate** `Float` Cashback rate percentage applied. **bonusCashbackRate** `Float` Bonus cashback rate percentage applied. **channel** `String` Channel through which the transaction was initiated. **sourceType** `String` Type of the funding source used. **logoUrl** `String` Logo URL for the merchant or source. **platformInstitutionLogo** `String` Logo URL for the banking institution (for bank-funded transactions). **challengeLogoUrl** `String` Logo URL for challenges/milestones (for bonus transactions). **invitedAccountId** `UUID` Account ID of invited user (for referral-related transactions). **expectedClearedDate** `DateTime` Expected date when pending transaction will be cleared. **liabilityId** `UUID` ID of associated liability (for bill payments). **isGiftCardBalanceAffected** `Boolean` Whether this transaction affected the gift card prepayment balance. **isCashBalanceAffected** `Boolean` Whether this transaction affected the cash balance. **isSeatBalanceAffected** `Boolean` Whether this transaction affected the rewards (seat) balance. **isReserveBalanceAffected** `Boolean` Whether this transaction affected the reserve balance. **transferId** `UUID` ID of the transfer record (for P2P transfers). **connectedAppId** `String` ID of the application transaction was triggered from **connectedAppName** `String` Name of the application transaction was triggered from **usedUserCashBalanceId** `UUID` ID of the specific user cash balance used for this transaction. **withdrawIds** `[UUID]` IDs of related withdrawals **createdAt** `DateTime` Timestamp when the transaction was created. **updatedAt** `DateTime` Timestamp when the transaction was last updated. **memo** `String` User-defined memo attached to this transaction. **transactionCategory** `String` User-defined category attached to this transaction. **attachmentUrl** `String` URL of the attachment associated with this transaction. **erpMetadata** `ErpTransactionMetadata` The current ERP-side categorization state of this transaction. Null when the account has no active ERP connection or when this transaction has no ERP metadata yet. # UploadSoleProprietorshipDocumentResponse Source: https://docs.fluz.app/api-reference/types/upload-sole-proprietorship-document-response Response from uploading sole proprietorship document. **Object** Response from uploading sole proprietorship document. ## Fields **url** `String!` The public URL of the uploaded document. # User Source: https://docs.fluz.app/api-reference/types/user Represents the User who has access to the account. **Object** Represents the User who has access to the account. ## Fields **userId** `UUID` The userId associated with the account. **firstName** `String` The first name of the user. **lastName** `String` The last name of the user. # UserAddress Source: https://docs.fluz.app/api-reference/types/user-address UserAddress represents a User Address record. **Object** UserAddress represents a User Address record. ## Fields **userAddressId** `UUID!` *No description provided in the schema yet.* **streetAddressLine1** `String` *No description provided in the schema yet.* **streetAddressLine2** `String` *No description provided in the schema yet.* **country** `String!` *No description provided in the schema yet.* **city** `String!` *No description provided in the schema yet.* **state** `String` *No description provided in the schema yet.* **postalCode** `String` *No description provided in the schema yet.* # UserBalances Source: https://docs.fluz.app/api-reference/types/user-balances UserBalances represents the balances of a user's rewards, cash, and gift card cash. **Object** UserBalances represents the balances of a user's rewards, cash, and gift card cash. ## Fields **rewardsBalance** `RewardsBalance` *No description provided in the schema yet.* **cashBalance** `CashBalance` *No description provided in the schema yet.* **giftCardCashBalance** `CashBalance` *No description provided in the schema yet.* **userCashBalances** `[UserCashBalance]` *No description provided in the schema yet.* # UserCashBalance Source: https://docs.fluz.app/api-reference/types/user-cash-balance UserCashBalance represents a user's cash balance account. **Object** UserCashBalance represents a user's cash balance account. ## Fields **userCashBalanceId** `UUID!` Unique identifier for the user cash balance. **totalCashBalance** `String!` Total cash balance. **availableCashBalance** `String!` Available cash balance. **lifetimeCashBalance** `String!` Lifetime cash balance (cumulative total ever deposited). **nickname** `String` Custom nickname for the balance. **status** `UserCashBalanceStatus!` Status of the cash balance account. **createdAt** `DateTime!` Date and time when the account was created. # UserCashBalanceConnection Source: https://docs.fluz.app/api-reference/types/user-cash-balance-connection Paginated response for cash balances query. **Object** Paginated response for cash balances query. ## Fields **userCashBalances** `[UserCashBalanceDetail]` List of cash balances. **totalCount** `Int!` Total count of cash balances matching the filter. **hasNextPage** `Boolean!` Whether there are more results available. # UserCashBalanceDetail Source: https://docs.fluz.app/api-reference/types/user-cash-balance-detail UserCashBalanceDetail represents a user's cash balance account with additional details. **Object** UserCashBalanceDetail represents a user's cash balance account with additional details. ## Fields **userCashBalanceId** `UUID!` Unique identifier for the user cash balance. **totalCashBalance** `String!` Total cash balance. **availableCashBalance** `String!` Available cash balance. **lifetimeCashBalance** `String!` Lifetime cash balance (cumulative total ever deposited). **nickname** `String` Custom nickname for the balance. **status** `UserCashBalanceStatus!` Status of the cash balance account. **isDefault** `Boolean!` Whether cash balance is the default one. **createdAt** `DateTime!` Date and time when the account was created. **updatedAt** `DateTime!` Date and time when the account was updated. # UserLookupResult Source: https://docs.fluz.app/api-reference/types/user-lookup-result Represents a user recipient. **Object** Represents a user recipient. ## Fields **recipientId** `UUID!` The account ID of the recipient. **name** `String!` Display name for the recipient. Format: "First Last" # UserPurchase Source: https://docs.fluz.app/api-reference/types/user-purchase UserPurchase represents a record of a purchase made by a user, detailing the payment methods and other relevant information. **Object** UserPurchase represents a record of a purchase made by a user, detailing the payment methods and other relevant information. ## Fields **purchaseId** `UUID!` The ID associated with the purchase. **purchaseDisplayId** `String` The display ID of the purchase. **purchaseBankCardId** `UUID` The ID of the Bank Card used to make the purchase. **bankAccountId** `UUID` The ID of the Bank Account used to make the purchase. **purchaseAmount** `Float!` The amount of the purchase. **fluzpayAmount** `Float` The amount of the balance applied. **seatRewardValue** `Float!` The amount of the cashback reward from the purchase. **paypalVaultId** `UUID` The ID of the PayPal used to make the purchase. **createdAt** `DateTime!` The time the purchase was created. **giftCard** `GiftCard` A connection to the gift card associated with the purchase. **virtualCard** `VirtualCard` A connection to the virtual card associated with the purchase. **accountId** `UUID` The unique identifier for the fluz account. **purchaserUserId** `UUID` The unique identifier for the user who made the purchase. **memo** `String` The memo associated with the purchase. **transactionCategory** `String` The category associated with the purchase. **attachmentUrl** `String` URL of the attachment associated with this purchase. # VerifyUserInformationResponse Source: https://docs.fluz.app/api-reference/types/verify-user-information-response Represents the response returned from the verifyUserInformation mutation. **Object** Represents the response returned from the verifyUserInformation mutation. ## Fields **status** `UserVerificationStatus!` The verification status. **message** `String` The response message if any. # VirtualCard Source: https://docs.fluz.app/api-reference/types/virtual-card VirtualCard represents a record of a virtual card generated by a user. **Object** VirtualCard represents a record of a virtual card generated by a user. ## Fields **virtualCardId** `UUID!` The ID associated with the virtual card. **userId** `UUID!` The ID of the user who created the virtual card. **cardholderName** `String!` The cardholder name appeared on the virtual card. **expiryMonth** `String!` The expiration month of the virtual card. **expiryYear** `String!` The expiration year of the virtual card. **virtualCardLast4** `String!` The last 4 digits of the virtual card number. **status** `VirtualCardStatus!` The status of the virtual card. **cardType** `VirtualCardType!` The type of the virtual card. **initialAmount** `Float!` The initial amount requested when the card was generated. **usedAmount** `Float` The total amount spent on the virtual card. **createdAt** `DateTime!` The time the virtual card was generated. **authorizationSetting** `VirtualCardAuthorizationSetting` A connection to the authorization setting of the virtual card. # VirtualCardAddressInfo Source: https://docs.fluz.app/api-reference/types/virtual-card-address-info VirtualCardAddressInfo provides the address information associated with a virtual card. **Object** VirtualCardAddressInfo provides the address information associated with a virtual card. ## Fields **streetAddress** `String` The street address. **billingAddrLine2** `String` The second line of the billing address. **postalCode** `String` The postal code. **city** `String` The city. **state** `String` The state. # VirtualCardAuthorizationSetting Source: https://docs.fluz.app/api-reference/types/virtual-card-authorization-setting VirtualCardAuthorizationSetting defines the authorization settings for a virtual card. **Object** VirtualCardAuthorizationSetting defines the authorization settings for a virtual card. ## Fields **virtualCardAsaSettingsId** `UUID!` The unique identifier for the authorization setting of the virtual card. **virtualCardId** `UUID!` The unique identifier for the virtual card. **lockDate** `String` The date when the card was locked. **dailySpendLimit** `String` The daily spending limit for the card. **weeklySpendLimit** `String` The weekly spending limit for the card. **monthlySpendLimit** `String` The monthly spending limit for the card. **annualSpendLimit** `String` The annual spending limit for the card. **lifetimeSpendLimit** `String` The lifetime spending limit for the card. **dailySpent** `String` The amount spent daily on the card. **weeklySpent** `String` The amount spent weekly on the card. **monthlySpent** `String` The amount spent monthly on the card. **annualSpent** `String` The amount spent annually on the card. **lifetimeSpent** `String` The amount spent over the lifetime of the card. **tokenized** `Boolean` Whether the card is tokenized or not. **lockedByUser** `Boolean` Whether the card is locked by the user or not. **lockCardNextUse** `Boolean` Whether the card should be locked after the next use. **cardNickname** `String` The nickname of the card. # VirtualCardBalance Source: https://docs.fluz.app/api-reference/types/virtual-card-balance VirtualCardBalance provides the balance, status and lock state of a virtual card. **Object** VirtualCardBalance provides the balance, status and lock state of a virtual card. ## Fields **virtualCardId** `UUID!` Virtual Card ID. **spentAmount** `Float!` Current card spent amount. **remainingBalance** `Float!` Remaining card balance. **spendLimit** `Float!` Spend limit for card. **spendLimitDuration** `VirtualCardSpendLimitDuration` Card limit duration type. Default is Lifetime. **status** `VirtualCardStatus!` Card status. Authorizations are declined unless the card is ACTIVE. **locked** `Boolean!` Whether the card is locked by the user, the system, or Fluz support. Authorizations are declined while this is true. **authorizationSetting** `VirtualCardAuthorizationSetting` The card's authorization setting, the same shape revealVirtualCardByVirtualCardId returns. # VirtualCardBulkOrder Source: https://docs.fluz.app/api-reference/types/virtual-card-bulk-order VirtualCardBulkOrder provides information about bulk creation order. **Object** VirtualCardBulkOrder provides information about bulk creation order. ## Fields **orderId** `String!` Order ID. **orderStatus** `VirtualCardBulkOrderStatus!` Status of the order. **virtualCards** `[VirtualCardDetails]` Created virtual card details. **totalCards** `Int` Total cards count. **successfulCardCreations** `Int` Successful card creation count. **failedCardCreations** `Int` Failed card creation count. # VirtualCardDetails Source: https://docs.fluz.app/api-reference/types/virtual-card-details The VirtualCardDetails type represents the details of a virtual card. **Object** The VirtualCardDetails type represents the details of a virtual card. ## Fields **virtualCardId** `UUID` The card ID. **cardNumber** `String` The card number. **expiryMMYY** `String` The expiry date in MMYY format. **cvv** `String` The CVV code. **cardHolderName** `String` The card holder's name. **billingAddress** `VirtualCardAddressInfo` The billing address. **authorizationSetting** `VirtualCardAuthorizationSetting` The authorization setting. # VirtualCardOffer Source: https://docs.fluz.app/api-reference/types/virtual-card-offer Represents the Bank identification numbers (BINs). **Object** Represents the Bank identification numbers (BINs). ## Fields **offerId** `String` Virtual Card offer ID. **programName** `String` Virtual Card program name. **bin** `String` The bank bin. **bankName** `String` The bank name. **programLimits** `ProgramLimits` The bank program spent limits. **rewardValue** `String` The bank program earn rate. # VirtualCardTransaction Source: https://docs.fluz.app/api-reference/types/virtual-card-transaction Virtual card transaction type. **Object** Virtual card transaction type. ## Fields **transactionId** `UUID!` Unique identifier for the virtual card transaction. **merchantDescriptor** `String` Raw merchant descriptor reported for the card transaction, such as "FACEBK \*8KJ88V5MR2". Null when no descriptor is associated with the transaction. **transactionDate** `String!` *No description provided in the schema yet.* **transactionType** `VirtualCardTransactionListType!` *No description provided in the schema yet.* **transactionAmount** `Float` The transaction amount, always expressed in USD. Null for rows where the underlying amount has not been settled (e.g. AVS-only decline records). **transactionApproval** `String` *No description provided in the schema yet.* **transactionResponseCode** `String` *No description provided in the schema yet.* **transactionStatus** `String` *No description provided in the schema yet.* **merchantName** `String` *No description provided in the schema yet.* **paymentMethod** `String` *No description provided in the schema yet.* **mcc** `Int` The merchant category code (MCC) associated with the transaction. Null when the transaction has no descriptor record or the MCC is unavailable. **merchantCountryCode** `String` ISO country code of the merchant. Null when the transaction has no descriptor record or the merchant country is unavailable. **originalCurrencyCode** `String` ISO 4217 currency code of the original transaction (e.g. "USD" for domestic). Null when the original currency is unknown or not applicable to this row. **originalCurrencyAmount** `Float` The original transaction amount in the merchant's currency, expressed in that currency's minor units per ISO 4217 (e.g. cents for USD/HKD/EUR, yen for JPY, fils for KWD). Divide by the currency's decimal exponent to get the major-unit value. Null when originalCurrencyCode is null. **currencyConversionRate** `Float` The conversion rate applied to convert the original currency to USD. Equals 1.0 for USD transactions. Null when originalCurrencyCode is null. # VirtualCardTransactions Source: https://docs.fluz.app/api-reference/types/virtual-card-transactions Virtual card transactions, grouped by virtual card ID. **Object** Virtual card transactions, grouped by virtual card ID. ## Fields **virtualCardId** `UUID!` *No description provided in the schema yet.* **transactions** `[VirtualCardTransaction!]!` *No description provided in the schema yet.* # Withdraw Source: https://docs.fluz.app/api-reference/types/withdraw Withdraw represents a withdrawal transaction record. **Object** Withdraw represents a withdrawal transaction record. ## Fields **withdrawId** `UUID!` Unique identifier for the withdrawal. **amount** `String!` The amount withdrawn. **processingFee** `String!` Fee charged for processing the withdrawal. **chargedFee** `String!` Fee charged to the user for the withdrawal. **status** `String!` Internal status of the withdrawal. **displayStatus** `String!` User-friendly display status of the withdrawal. **withdrawMethod** `WithdrawMethods!` The withdrawal method used. **withdrawSource** `WithdrawSource!` The source balance from which funds were withdrawn. **submissionDate** `DateTime!` Date and time when the withdrawal was submitted. **createdAt** `DateTime!` Date and time when the withdrawal was created. **updatedAt** `DateTime!` Date and time when the withdrawal was last updated. **transactionLogId** `UUID` Identifier of the associated transaction log. **externalTransactionId** `String` External transaction identifier from payment gateway (for ACH). **payoutId** `String` Payout identifier from payment gateway (for PayPal/Venmo). **emailAddress** `String` Email address associated with the withdrawal. **bankAccountId** `UUID` Identifier of the bank account used (if applicable). **userCashBalanceId** `UUID` Identifier of the user cash balance account withdrawn from. **seatId** `UUID` The seat id associated with the account. # WithdrawCashBalanceResponse Source: https://docs.fluz.app/api-reference/types/withdraw-cash-balance-response Response type for the withdrawCashBalance mutation, returning the withdrawal record and updated balances. **Object** Response type for the withdrawCashBalance mutation, returning the withdrawal record and updated balances. ## Fields **withdraws** `[Withdraw!]!` List of withdrawal transaction records created (usually one, but can be multiple if split across seats). **balances** `UserBalances` User's current balances after the withdrawal. # WithdrawFeeEstimate Source: https://docs.fluz.app/api-reference/types/withdraw-fee-estimate Estimated fees and net amount for a withdrawal. **Object** Estimated fees and net amount for a withdrawal. ## Fields **fee** `Float!` The fee that will be charged for this withdrawal. **netAmount** `Float!` The net amount the user will receive after fees. **feePercent** `Float!` The fee percentage applied. **feeMaxCap** `Float` The maximum fee cap in dollars, if applicable. **settlementDelayDays** `Int` The number of business days until settlement. Null for expedited withdrawals that settle immediately. # AccountType Source: https://docs.fluz.app/api-reference/types/account-type Enum describing the types of accounts. **Enum** Enum describing the types of accounts. ## Values **CONSUMER** `enum value` Account is a consumer/personal account. **BUSINESS** `enum value` Account is a business account. # ApprovalRequestAction Source: https://docs.fluz.app/api-reference/types/approval-request-action Approval action used by approve/decline mutations. **Enum** Approval action used by approve/decline mutations. ## Values **APPROVE** `enum value` *No description provided in the schema yet.* **DECLINE** `enum value` *No description provided in the schema yet.* # ApprovalRequestApproverStatus Source: https://docs.fluz.app/api-reference/types/approval-request-approver-status Approval approver status. **Enum** Approval approver status. ## Values **PENDING** `enum value` *No description provided in the schema yet.* **EXPIRED** `enum value` *No description provided in the schema yet.* **APPROVED** `enum value` *No description provided in the schema yet.* **DECLINED** `enum value` *No description provided in the schema yet.* # ApprovalRequestStatus Source: https://docs.fluz.app/api-reference/types/approval-request-status Approval request status. **Enum** Approval request status. ## Values **INACTIVE** `enum value` *No description provided in the schema yet.* **PENDING** `enum value` *No description provided in the schema yet.* **EXPIRED** `enum value` *No description provided in the schema yet.* **APPROVED** `enum value` *No description provided in the schema yet.* **DECLINED** `enum value` *No description provided in the schema yet.* # BankAccountStatus Source: https://docs.fluz.app/api-reference/types/bank-account-status BankAccountStatus represents the current status of a bank account. **Enum** BankAccountStatus represents the current status of a bank account. ## Values **ENABLED** `enum value` *No description provided in the schema yet.* **DISABLED** `enum value` *No description provided in the schema yet.* # BankAccountType Source: https://docs.fluz.app/api-reference/types/bank-account-type BankAccountType represents the type of bank account. **Enum** BankAccountType represents the type of bank account. ## Values **SAVING** `enum value` *No description provided in the schema yet.* **CREDIT** `enum value` *No description provided in the schema yet.* **CHECKING** `enum value` *No description provided in the schema yet.* # BankCardStatus Source: https://docs.fluz.app/api-reference/types/bank-card-status BankCardStatus represents the status of bank card. **Enum** BankCardStatus represents the status of bank card. ## Values **ACTIVE** `enum value` *No description provided in the schema yet.* **INACTIVE** `enum value` *No description provided in the schema yet.* **PENDING\_REMOVAL** `enum value` *No description provided in the schema yet.* # BankCardType Source: https://docs.fluz.app/api-reference/types/bank-card-type BankCardType represents the type of bank card. **Enum** BankCardType represents the type of bank card. ## Values **DEBIT** `enum value` *No description provided in the schema yet.* **CREDIT** `enum value` *No description provided in the schema yet.* **PREPAID** `enum value` *No description provided in the schema yet.* # BarcodeTypeEnum Source: https://docs.fluz.app/api-reference/types/barcode-type-enum BarcodeTypeEnum defines the barcode type of the offer. **Enum** BarcodeTypeEnum defines the barcode type of the offer. ## Values **NONE** `enum value` *No description provided in the schema yet.* **C128** `enum value` *No description provided in the schema yet.* **PDF417** `enum value` *No description provided in the schema yet.* **QRCODE** `enum value` *No description provided in the schema yet.* # BulkItemStatus Source: https://docs.fluz.app/api-reference/types/bulk-item-status The lifecycle status of a single item (one target) within a bulk job. **Enum** The lifecycle status of a single item (one target) within a bulk job. ## Values **QUEUED** `enum value` *No description provided in the schema yet.* **RUNNING** `enum value` *No description provided in the schema yet.* **SUCCEEDED** `enum value` *No description provided in the schema yet.* **FAILED** `enum value` *No description provided in the schema yet.* **SKIPPED** `enum value` *No description provided in the schema yet.* **CANCELLED** `enum value` *No description provided in the schema yet.* # BulkJobStatus Source: https://docs.fluz.app/api-reference/types/bulk-job-status The lifecycle status of a bulk job. **Enum** The lifecycle status of a bulk job. ## Values **QUEUED** `enum value` *No description provided in the schema yet.* **RUNNING** `enum value` *No description provided in the schema yet.* **COMPLETED** `enum value` *No description provided in the schema yet.* **COMPLETED\_WITH\_ERRORS** `enum value` *No description provided in the schema yet.* **FAILED** `enum value` *No description provided in the schema yet.* **CANCELLED** `enum value` *No description provided in the schema yet.* # BulkOperationType Source: https://docs.fluz.app/api-reference/types/bulk-operation-type The bulk operation to perform. **Enum** The bulk operation to perform. Read exports resolve asynchronously to a downloadable file; the write operations become available in a later release. ## Values **GET\_TRANSACTIONS\_EXPORT** `enum value` *No description provided in the schema yet.* **GET\_BALANCES\_EXPORT** `enum value` *No description provided in the schema yet.* **CREATE\_TRANSFER** `enum value` *No description provided in the schema yet.* **DEPOSIT\_CASH\_BALANCE** `enum value` *No description provided in the schema yet.* **UPDATE\_TRANSACTION\_METADATA** `enum value` *No description provided in the schema yet.* # BulkTargetErrorCode Source: https://docs.fluz.app/api-reference/types/bulk-target-error-code Why a bulk operation could not be performed for one target. **Enum** Why a bulk operation could not be performed for one target. Failures are always per-target: one user's failure never fails the rest of the request. ## Values **TARGET\_NOT\_CONNECTED** `enum value` The id does not correspond to a user currently connected to your application. **INVALID\_TARGET\_IDENTIFIER** `enum value` The id is not a valid identifier. **INSUFFICIENT\_SCOPE** `enum value` The user has not granted the scopes this operation requires. **ACCOUNT\_NOT\_PERMITTED** `enum value` The user's grant does not permit this operation on the requested account. # BulkTargetMode Source: https://docs.fluz.app/api-reference/types/bulk-target-mode How a bulk operation selects its target users. **Enum** How a bulk operation selects its target users. ## Values **ALL\_CONNECTED** `enum value` Every user currently connected to your application. **SELECTED** `enum value` Only the users identified in the targets list. # BusinessAccountUsageType Source: https://docs.fluz.app/api-reference/types/business-account-usage-type Business account usage types. **Enum** Business account usage types. ## Values **CORPORATE\_GIFTING** `enum value` *No description provided in the schema yet.* **CORPORATE\_SPENDING\_ADMIN** `enum value` *No description provided in the schema yet.* **REWARDS\_MAXIMIZER** `enum value` *No description provided in the schema yet.* **GIFT\_CARD\_RESELLING** `enum value` *No description provided in the schema yet.* **PURCHASE\_GOODS\_FOR\_BUSINESS\_USE** `enum value` *No description provided in the schema yet.* **ONLINE\_SELLER\_RETAIL\_PURCHASING** `enum value` *No description provided in the schema yet.* # BusinessOwnerVerificationType Source: https://docs.fluz.app/api-reference/types/business-owner-verification-type Expected identity verification for an owner (returned by `getBusiness`). **Enum** Expected identity verification for an owner (returned by `getBusiness`). * SSN: owner is expected to verify via SSN (CIP). * DOCUMENTS: owner is expected to verify via uploaded documents / Plaid IDV. * NOT\_REQUIRED: no additional identity verification is expected for this owner. ## Values **SSN** `enum value` *No description provided in the schema yet.* **DOCUMENTS** `enum value` *No description provided in the schema yet.* **NOT\_REQUIRED** `enum value` *No description provided in the schema yet.* # BusinessStructureType Source: https://docs.fluz.app/api-reference/types/business-structure-type Business structure types supported for registration. **Enum** Business structure types supported for registration. ## Values **LLC** `enum value` *No description provided in the schema yet.* **CORPORATION** `enum value` *No description provided in the schema yet.* **PARTNERSHIP** `enum value` *No description provided in the schema yet.* **SOLE\_PROPRIETORSHIP** `enum value` *No description provided in the schema yet.* **COOP** `enum value` *No description provided in the schema yet.* # CashBalanceAvailabilityType Source: https://docs.fluz.app/api-reference/types/cash-balance-availability-type Enum defining the types of availability for a cash balance. **Enum** Enum defining the types of availability for a cash balance. ## Values **INSTANT** `enum value` The deposit can be settled immediately. **STANDARD** `enum value` Standard processing times applies. # CashBalanceDepositStatus Source: https://docs.fluz.app/api-reference/types/cash-balance-deposit-status Enum representing the status of a cash balance deposit. **Enum** Enum representing the status of a cash balance deposit. ## Values **AVAILABLE** `enum value` The deposit is available in the user's account. **FAILED** `enum value` The deposit attempt has failed. **PENDING** `enum value` The deposit is currently being processed. **REFUNDED** `enum value` The deposit was refunded. **REVERSED** `enum value` The deposit was reversed after being credited. **REVIEW** `enum value` The deposit is under review. # CashBalanceDepositType Source: https://docs.fluz.app/api-reference/types/cash-balance-deposit-type Enum representing different types of cash balance deposits. **Enum** Enum representing different types of cash balance deposits. ## Values **CASH\_BALANCE** `enum value` A regular cash balance deposit. **RESERVE\_BALANCE** `enum value` A deposit held in reserve balance. **GIFT\_CARD\_BALANCE** `enum value` A *non-withdrawable* deposit for gift card balance. # CloRateTypeEnum Source: https://docs.fluz.app/api-reference/types/clo-rate-type-enum Indicates the type of rate active for a specific period of a Card Linked Offer. **Enum** Indicates the type of rate active for a specific period of a Card Linked Offer. ## Values **REGULAR** `enum value` *No description provided in the schema yet.* **PROMO** `enum value` *No description provided in the schema yet.* # DeclinedTransactionStatus Source: https://docs.fluz.app/api-reference/types/declined-transaction-status DeclinedTransactionStatus represents the current state of a transaction. **Enum** DeclinedTransactionStatus represents the current state of a transaction. ## Values **DECLINED** `enum value` Transaction has been declined. **FAILED** `enum value` Transaction has failed. # DeliveryFormatType Source: https://docs.fluz.app/api-reference/types/delivery-format-type DeliveryFormatType defines the delivery format of the offer. **Enum** DeliveryFormatType defines the delivery format of the offer. ## Values **URL** `enum value` *No description provided in the schema yet.* **CODES** `enum value` *No description provided in the schema yet.* **PIN\_AS\_CODE** `enum value` *No description provided in the schema yet.* **PIN\_WITH\_URL** `enum value` *No description provided in the schema yet.* **CODE\_WITH\_PREFIX** `enum value` *No description provided in the schema yet.* # ErpReferenceItemSortKey Source: https://docs.fluz.app/api-reference/types/erp-reference-item-sort-key **Enum** *No description provided in the schema yet.* ## Values **NAME** `enum value` *No description provided in the schema yet.* **FULLY\_QUALIFIED\_NAME** `enum value` *No description provided in the schema yet.* **LAST\_CHANGED\_AT** `enum value` *No description provided in the schema yet.* # ErpTransactionMetadataSortKey Source: https://docs.fluz.app/api-reference/types/erp-transaction-metadata-sort-key **Enum** *No description provided in the schema yet.* ## Values **UPDATED\_AT** `enum value` *No description provided in the schema yet.* # ErpTransactionSyncStatus Source: https://docs.fluz.app/api-reference/types/erp-transaction-sync-status Sync state of a transaction's ERP metadata against the connected accounting provider. **Enum** Sync state of a transaction's ERP metadata against the connected accounting provider. ## Values **DRAFT** `enum value` Missing a required field (e.g. category) and cannot be synced yet. **READY** `enum value` All required fields are present, ready for sync. **SYNC\_STARTED** `enum value` Currently being exported to the ERP provider. **SYNCED** `enum value` Successfully exported to the ERP provider. **SYNC\_FAILED** `enum value` The last export attempt failed. # ExternalBusinessOwnerStatus Source: https://docs.fluz.app/api-reference/types/external-business-owner-status Per-owner verification progress for the business roster. **Enum** Per-owner verification progress for the business roster. ## Values **PENDING\_INVITE** `enum value` *No description provided in the schema yet.* **PENDING\_CIP** `enum value` *No description provided in the schema yet.* **READY** `enum value` *No description provided in the schema yet.* **FAILED** `enum value` *No description provided in the schema yet.* # ExternalKybStatus Source: https://docs.fluz.app/api-reference/types/external-kyb-status KYB lifecycle status exposed to API clients (subset of internal KybStatus). **Enum** KYB lifecycle status exposed to API clients (subset of internal KybStatus). ## Values **PENDING** `enum value` *No description provided in the schema yet.* **APPROVED** `enum value` *No description provided in the schema yet.* **DECLINED** `enum value` *No description provided in the schema yet.* # GenerateVCShareLinksInput Source: https://docs.fluz.app/api-reference/types/generate-vcshare-links-input Input type for generateVCShareLinks. **Input object** Input type for generateVCShareLinks. ## Input fields **cardLimit** `Int!` *required* The limit on the card you want to share. **offerId** `String!` *required* The offer ID for the virtual card. **daysUntilExpiration** `Int` The number of days the card should be valid for. **quantity** `Int!` *required* The number of share links to generate. **shareMethod** `ShareMethodType!` *required* The method you want to share cards. **recipientListPhone** `[String]` The list of phone numbers to send the share link to via SMS. **recipientListEmail** `[String]` The list of email addresses to send the share link to. **recipientUserIds** `[UUID]` Known Fluz user IDs to bind as share-link recipients when shareMethod is EXISTING\_USER. Required length must equal quantity. Mutually exclusive with recipientRegistrations. **recipientRegistrations** `[ShareLinkRecipientRegistrationInput]` Inline pre-register payloads when shareMethod is REGISTER\_USER. TGS creates or reuses placeholder users (no seat) before generating links. Required length must equal quantity. Mutually exclusive with recipientUserIds. **userCashBalanceId** `UUID` The spend account you want to use to fund the virtual card. **virtualCardId** `UUID` Existing virtual card to bind to the hosted link. When provided, claim grants access instead of issuing a new card. **usePrepaymentBalance** `Boolean` Set whether to use your Prepayment Balance as an additional funding source. The default is false. **useRewardsBalance** `Boolean` Set whether to use your Fluz Rewards Balance as an additional funding source. The default is false. # GetCardProvisioningUrlInput Source: https://docs.fluz.app/api-reference/types/get-card-provisioning-url-input **Input object** *No description provided in the schema yet.* ## Input fields **virtualCardId** `String` The card to provision — the `virtualCardId` returned by `createVirtualCard`. Must be a UUIDv4, must belong to the caller's account, and must be ACTIVE. Provide exactly one of `virtualCardId` or `offerId`. Prefer this one: it names the card unambiguously no matter how many cards the account holds on the offer. **offerId** `String` The virtual card offer to provision. Must be a UUIDv4, active, tokenization-eligible, and accessible to the caller's account. Provide exactly one of `virtualCardId` or `offerId`. An offer id only identifies a card while the account holds at most one active card on that offer; once there are several, this query fails with `VirtualCard.AMBIGUOUS_CARD_SELECTION` and you must pass `virtualCardId`. **platform** `ProvisioningPlatform` Optional platform hint. Defaults to IOS. See ProvisioningPlatform for what this value controls (it does NOT lock the URL to a single platform — iOS and Android opens are auto-routed correctly via platform-aware redirects). # GetOfferQuoteInput Source: https://docs.fluz.app/api-reference/types/get-offer-quote-input **Input object** *No description provided in the schema yet.* ## Input fields **merchantSlug** `String!` *required* Human readable unique identifier for merchant. Can be found in merchant list. **denomination** `Float!` *required* The purchase amount **paymentMethod** `PaymentMethodType` The payment method to be used for the purchase. Default is FLUZPAY # GetVCShareLinksInput Source: https://docs.fluz.app/api-reference/types/get-vcshare-links-input Input type for generateVCShareLinks. **Input object** Input type for generateVCShareLinks. ## Input fields **shareObjectStatuses** `[ShareObjectStatus]` Specify the share object status to filter on. **shareRequestBatchIds** `[String]` The list of batch ID's to retrieve. Each share link included in these batches will be fetched. **shareRequestDisplayIds** `[String]` The list of display ID's to retrieve. Only the share link with these display ID's will be fetched. # GetVirtualCardBalanceInput Source: https://docs.fluz.app/api-reference/types/get-virtual-card-balance-input Input type for get virtual card balance. **Input object** Input type for get virtual card balance. ## Input fields **virtualCardIds** `[UUID!]!` *required* The virtual card ids to fetch balances. # GetVirtualCardBulkOrderStatusInput Source: https://docs.fluz.app/api-reference/types/get-virtual-card-bulk-order-status-input Input type for get bulk virtual card creation order. **Input object** Input type for get bulk virtual card creation order. ## Input fields **orderId** `String!` *required* *No description provided in the schema yet.* # GetVirtualCardOffersInput Source: https://docs.fluz.app/api-reference/types/get-virtual-card-offers-input Input type for get virtual card offers. **Input object** Input type for get virtual card offers. ## Input fields **cardBrandLocked** `Boolean` Specify if virtual card offers are brand locked. **cardType** `VirtualCardOfferType` Specify virtual card network type. **cardNetwork** `VirtualCardNetwork` Specify virtual card network type. # GetVirtualCardTransactionsInput Source: https://docs.fluz.app/api-reference/types/get-virtual-card-transactions-input Input type for fetching transactions for multiple virtual cards. **Input object** Input type for fetching transactions for multiple virtual cards. ## Input fields **virtualCardIds** `[UUID!]` A list of virtual card IDs to fetch transactions for. When omitted, transactions are returned across all cards belonging to the authenticated account, and pagination defaults to a limit of 100 transactions across the account if paginate.limit is not provided. **filters** `VirtualCardTransactionFiltersInput` Filters to apply to the transaction data, such as date range or transaction type. **paginate** `OffsetInput` Pagination settings for the results, including offset and limit. When virtualCardIds is provided, the limit/offset apply per card. When virtualCardIds is omitted, the limit/offset apply across the entire account in a single query. # GetWithdrawFeeEstimateInput Source: https://docs.fluz.app/api-reference/types/get-withdraw-fee-estimate-input Input for estimating withdrawal fees before submitting a withdrawal. **Input object** Input for estimating withdrawal fees before submitting a withdrawal. ## Input fields **amount** `Float!` *required* The amount to withdraw. **method** `WithdrawMethods!` *required* The withdrawal method. **source** `WithdrawSource` The source balance from which to withdraw. **isExpedited** `Boolean` Whether to estimate fees for an expedited withdrawal. # GiftCardStatus Source: https://docs.fluz.app/api-reference/types/gift-card-status GiftCardStatus represents the current state of a gift card. **Enum** GiftCardStatus represents the current state of a gift card. ## Values **ACTIVE** `enum value` The default status when a Gift Card is generated. **NOT\_ACTIVE** `enum value` The Gift Card has been marked as used by the user. **ARCHIVED** `enum value` The Gift Card is not currently active. **RESTRICTED** `enum value` The Gift Card has been marked as restricted. # LockVirtualCardInput Source: https://docs.fluz.app/api-reference/types/lock-virtual-card-input Input type for lock virtual card. **Input object** Input type for lock virtual card. ## Input fields **virtualCardId** `UUID!` *required* The virtual card to update. # MerchantInput Source: https://docs.fluz.app/api-reference/types/merchant-input Input type for getReferralUrl. **Input object** Input type for getReferralUrl. ## Input fields **id** `UUID` The unique identifier for the merchant. **slug** `String` Human readable unique identifier for merchant. Can be found in merchant list. **name** `String` Merchant name # ObjectOwnerObjectType Source: https://docs.fluz.app/api-reference/types/object-owner-object-type Supported object domains for metadata-only ownership. **Enum** Supported object domains for metadata-only ownership. ## Values **VIRTUAL\_CARDS** `enum value` *No description provided in the schema yet.* **GIFT\_CARDS** `enum value` *No description provided in the schema yet.* **FUNDING\_SOURCES** `enum value` *No description provided in the schema yet.* **SPEND\_ACCOUNTS** `enum value` *No description provided in the schema yet.* # OfferDenominationType Source: https://docs.fluz.app/api-reference/types/offer-denomination-type OfferDenominationType is the type of denomination of a specific offer. **Enum** OfferDenominationType is the type of denomination of a specific offer. ## Values **VARIABLE** `enum value` VARIABLE means that the offer has a variable denomination. **FIXED** `enum value` FIXED means that the offer has fixed value denominations. **VARIABLENOCENTS** `enum value` VARIABLENOCENTS means that the offer has a variable denomination but the value must be an integer. # OfferType Source: https://docs.fluz.app/api-reference/types/offer-type OfferType is the type of offer. **Enum** OfferType is the type of offer. ## Values **GIFT\_CARD\_OFFER** `enum value` GIFT\_CARD\_OFFER means that the offer is a gift card. **EXCLUSIVE\_RATE\_OFFER** `enum value` EXCLUSIVE\_RATE\_OFFER means that the offer is a special rate for gift cards. **CARD\_LINKED\_OFFER** `enum value` CARD\_LINKED\_OFFER means that the offer is a special rate for virtual cards. # OfferTypesInput Source: https://docs.fluz.app/api-reference/types/offer-types-input Input type for getMerchants. **Input object** Input type for getMerchants. ## Input fields **giftCardOffer** `Boolean` *No description provided in the schema yet.* **cardLinkedOffer** `Boolean` *No description provided in the schema yet.* # OffsetInput Source: https://docs.fluz.app/api-reference/types/offset-input The page info. **Input object** The page info. ## Input fields **limit** `Int` The number of items to return per page. The maximum for limit is 20. **offset** `Int` The number of items to skip. # OwnerAddressInput Source: https://docs.fluz.app/api-reference/types/owner-address-input Address for a business owner. **Input object** Address for a business owner. Required for owners subject to full KYC verification. ## Input fields **streetAddressLine1** `String!` *required* Street address line 1. **streetAddressLine2** `String` Street address line 2 (optional). **city** `String!` *required* City. **state** `String!` *required* State / province name. **postalCode** `String!` *required* Postal code. **country** `String!` *required* Country name. # PayPalAccountStatus Source: https://docs.fluz.app/api-reference/types/pay-pal-account-status PayPalAccountStatus represents the current status of a PayPal account. **Enum** PayPalAccountStatus represents the current status of a PayPal account. ## Values **ACTIVE** `enum value` *No description provided in the schema yet.* **REMOVED** `enum value` *No description provided in the schema yet.* # PaycheckDepositType Source: https://docs.fluz.app/api-reference/types/paycheck-deposit-type Distribution method for a paycheck direct-deposit form. **Enum** Distribution method for a paycheck direct-deposit form. ## Values **FULL** `enum value` Deposit the entire paycheck. **FIXED** `enum value` Deposit a fixed dollar amount. **PERCENTAGE** `enum value` Deposit a percentage of the paycheck. Requires depositPercentage. # PaymentMethodType Source: https://docs.fluz.app/api-reference/types/payment-method-type PaymentMethodType represents the type of payment method. **Enum** PaymentMethodType represents the type of payment method. ## Values **BANK\_CARD** `enum value` *No description provided in the schema yet.* **BANK\_ACCOUNT** `enum value` *No description provided in the schema yet.* **PAYPAL** `enum value` *No description provided in the schema yet.* **FLUZPAY** `enum value` *No description provided in the schema yet.* **APPLE\_PAY** `enum value` *No description provided in the schema yet.* **GOOGLE\_PAY** `enum value` *No description provided in the schema yet.* # PlaidBankAccountFilterInput Source: https://docs.fluz.app/api-reference/types/plaid-bank-account-filter-input Optional filters for listing persisted Plaid bank accounts owned by the authenticated account. **Input object** Optional filters for listing persisted Plaid bank accounts owned by the authenticated account. ## Input fields **platformItemId** `String` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* # PlaidBankAccountInput Source: https://docs.fluz.app/api-reference/types/plaid-bank-account-input Input for selecting one persisted Plaid bank account. **Input object** Input for selecting one persisted Plaid bank account. ## Input fields **bankAccountId** `UUID!` *required* *No description provided in the schema yet.* # PlaidBankBalanceFilterInput Source: https://docs.fluz.app/api-reference/types/plaid-bank-balance-filter-input Optional filters for listing latest persisted Plaid bank balances. **Input object** Optional filters for listing latest persisted Plaid bank balances. ## Input fields **platformItemId** `String` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* # PlaidBankTransactionFilterInput Source: https://docs.fluz.app/api-reference/types/plaid-bank-transaction-filter-input Input for listing persisted historical bank transactions populated by identity-service. **Input object** Input for listing persisted historical bank transactions populated by identity-service. ## Input fields **platformItemId** `String` *No description provided in the schema yet.* **bankInstitutionAuthId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **startDate** `DateTime` *No description provided in the schema yet.* **endDate** `DateTime` *No description provided in the schema yet.* **paginate** `OffsetInput` *No description provided in the schema yet.* # PlaidDeviceOs Source: https://docs.fluz.app/api-reference/types/plaid-device-os PlaidDeviceOs enables Plaid OAuth redirects for native Link SDK integrations. **Enum** PlaidDeviceOs enables Plaid OAuth redirects for native Link SDK integrations. ## Values **IOS** `enum value` *No description provided in the schema yet.* **ANDROID** `enum value` *No description provided in the schema yet.* # PlaidIntegrationChange Source: https://docs.fluz.app/api-reference/types/plaid-integration-change What a registration or disable call changed. **Enum** What a registration or disable call changed. ## Values **CREATED** `enum value` *No description provided in the schema yet.* **UNCHANGED** `enum value` *No description provided in the schema yet.* **UPDATED** `enum value` *No description provided in the schema yet.* **REACTIVATED** `enum value` *No description provided in the schema yet.* **DISABLED** `enum value` *No description provided in the schema yet.* # PlaidIntegrationEnvironment Source: https://docs.fluz.app/api-reference/types/plaid-integration-environment **Enum** *No description provided in the schema yet.* ## Values **SANDBOX** `enum value` *No description provided in the schema yet.* **PRODUCTION** `enum value` *No description provided in the schema yet.* # PlaidIntegrationStatus Source: https://docs.fluz.app/api-reference/types/plaid-integration-status **Enum** *No description provided in the schema yet.* ## Values **ACTIVE** `enum value` *No description provided in the schema yet.* **DISABLED** `enum value` *No description provided in the schema yet.* # PlaidLinkAddressInput Source: https://docs.fluz.app/api-reference/types/plaid-link-address-input Address attached to Plaid-linked bank accounts when identity-service requires one after Link. **Input object** Address attached to Plaid-linked bank accounts when identity-service requires one after Link. ## Input fields **streetAddressLine1** `String!` *required* *No description provided in the schema yet.* **streetAddressLine2** `String` *No description provided in the schema yet.* **city** `String!` *required* *No description provided in the schema yet.* **state** `String!` *required* *No description provided in the schema yet.* **postalCode** `String!` *required* *No description provided in the schema yet.* **country** `String!` *required* *No description provided in the schema yet.* # PlaidLinkMode Source: https://docs.fluz.app/api-reference/types/plaid-link-mode PlaidLinkMode identifies whether the link token starts a new link or repairs an existing Plaid connection. **Enum** PlaidLinkMode identifies whether the link token starts a new link or repairs an existing Plaid connection. ## Values **NEW** `enum value` *No description provided in the schema yet.* **RELINK** `enum value` *No description provided in the schema yet.* # ProvisioningPlatform Source: https://docs.fluz.app/api-reference/types/provisioning-platform Target platform hint for the returned provisioning URL. **Enum** Target platform hint for the returned provisioning URL. Affects only the desktop / unknown-user-agent fallback that Branch.io routes to; actual iOS and Android opens are routed correctly regardless of this value, because the shortened link carries platform-aware redirects. ## Values **IOS** `enum value` End-user is expected to open the URL on iOS. Fallback resolves to the Fluz App Clip launcher (default). **ANDROID** `enum value` End-user is expected to open the URL on Android. Fallback resolves to the Fluz Android app deep link. **OTHER** `enum value` End-user platform is unknown or web/desktop. Fallback resolves to the Fluz web app. # PurchaseGiftCardInput Source: https://docs.fluz.app/api-reference/types/purchase-gift-card-input Input type for purchase gift card. **Input object** Input type for purchase gift card. ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **offerId** `UUID` The unique identifier for the offer. **amount** `Float!` *required* The amount to purchase. **balanceAmount** `Float` The balance amount. **bankAccountId** `UUID` The unique identifier for the bank account. **bankCardId** `UUID` The unique identifier of the bank card. **paypalVaultId** `UUID` The unique identifier of the PayPal account. **exclusiveRateId** `UUID` The unique identifier of the exclusive rate. **merchantSlug** `String` Human readable unique identifier for merchant. Can be found in merchant list. **defaultToBalance** `Boolean` Use balance as the fallback payment method if the primary payment method fails. Defaults to true. **minRewardRate** `Float` The minimum reward rate to purchase if merchantSlug option is chosen. **userCashBalanceId** `UUID` The unique identifier of the user cash balance id used for this purchase **memo** `String` Optional memo to attach to this purchase transaction. **transactionCategory** `String` Optional category name to attach to this purchase transaction. **attachmentId** `String` ID of a previously uploaded attachment (PDF or PNG). Use the /api/v1/file-upload/transaction-memo-attachment endpoint to upload. # PurchaseScopeFilter Source: https://docs.fluz.app/api-reference/types/purchase-scope-filter PurchaseScopeFilter indicates the type of filter to use for getUserPurchases query - TOKEN_USER: use the userId from the token - TOKEN_ACCOUNT: use the accountId from the token Presence of both filters means both userId and accountId will be used. **Enum** PurchaseScopeFilter indicates the type of filter to use for getUserPurchases query * TOKEN\_USER: use the userId from the token * TOKEN\_ACCOUNT: use the accountId from the token Presence of both filters means both userId and accountId will be used. ## Values **TOKEN\_USER** `enum value` *No description provided in the schema yet.* **TOKEN\_ACCOUNT** `enum value` *No description provided in the schema yet.* # RedeemFluzGiftCardInput Source: https://docs.fluz.app/api-reference/types/redeem-fluz-gift-card-input Input type for redeeming a Fluz Gift Card to the user's account. **Input object** Input type for redeeming a Fluz Gift Card to the user's account. ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **code** `String!` *required* The Fluz Gift Card code (as printed on or delivered with the card). # RegisterBusinessConfirmInput Source: https://docs.fluz.app/api-reference/types/register-business-confirm-input Ownership roster certifications required at registration. **Input object** Ownership roster certifications required at registration. ## Input fields **allOwnersWith25PercentageOwnershipListed** `Boolean!` *required* Certify that all natural persons who directly or indirectly own 25% or more of the business are listed. Required when at least one owner has `ownershipPercentage` >= 25. **noOwnersMoreThan25Percentage** `Boolean!` *required* Certify that no natural person directly or indirectly owns 25% or more of the business. Required when no owner has `ownershipPercentage` >= 25. # RegisterBusinessInput Source: https://docs.fluz.app/api-reference/types/register-business-input Input for registering a new business. **Input object** Input for registering a new business. ## Input fields **businessName** `String!` *required* Legal business name. **dbaName** `String` Doing Business As name (optional). **businessStructure** `BusinessStructureType!` *required* Business structure type. **businessLegalAddress** `BusinessLegalAddressInput!` *required* Business legal address. **stateOfIncorporation** `String!` *required* State of incorporation. **taxId** `String!` *required* Tax ID / EIN Number. Must be in format XX-XXXXXXX (2 digits, hyphen, 7 digits). **soleProprietorshipDocumentUrl** `String` Optional URL to sole proprietorship documentation. Use the REST endpoint POST /api/v1/file-upload/business-application-document with `documentType=SOLE_PROPRIETORSHIP` to upload the file first and get the URL. Must be an absolute http/https URL of at most 255 characters. **authorizedSignerDocumentUrl** `String` URL of the authorized signer's authorization document (e.g. corporate resolution, power of attorney). Required when the applicant has less than 25% ownership and is not the control person. Use the REST endpoint POST /api/v1/file-upload/business-application-document with `documentType=AUTHORIZED_SIGNER` to upload the file first and get the URL. Must be an absolute http/https URL of at most 255 characters. **businessCategoryId** `UUID!` *required* Business category ID (from getBusinessCategories query). **businessSubCategoryId** `UUID!` *required* Business sub-category ID (from getBusinessCategories query). **natureOfBusiness** `String` Brief description of the nature of business (optional). **websiteUrl** `String` Business website URL (optional). **businessAccountUsage** `[BusinessAccountUsageType]` Business account usage types. Must be one of the predefined enum values. If businessAccountUsage is not provided or is empty, businessAccountUsageOther must be filled. **businessAccountUsageOther** `String` Other business account usage description. Required if businessAccountUsage is not provided or is empty. **externalReferenceId** `String` Optional external reference ID stored on the OAuth user record for this business account. **confirm** `RegisterBusinessConfirmInput!` *required* Ownership roster certifications. See `RegisterBusinessConfirmInput` for field requirements. **owners** `[BusinessOwnerInput!]!` *required* List of business owners. Roster rules: * At least one owner is required. * Exactly one owner must have `isControlPerson: true`. * Exactly one owner must match the bearer token user by email or phone (the applicant). * Total `ownershipPercentage` across all owners must not exceed 100. * When `businessStructure` is `SOLE_PROPRIETORSHIP`: exactly one owner, that owner must be the control person with 100% ownership. Per-owner data requirements: * `isUsPerson` is required on every owner, regardless of role. * Applicant, invited owners, and non-beneficial owners (\< 25%, not control person): base fields only. * All other owners: base fields plus `dob` and `address` (`lastFourSsnDigits` also required when `isUsPerson` is true). # RegisterPlaidIntegrationInput Source: https://docs.fluz.app/api-reference/types/register-plaid-integration-input Input for registering the application's own Plaid credentials (BYOI Plaid). **Input object** Input for registering the application's own Plaid credentials (BYOI Plaid). Registering the same clientId and secret again returns the current integration unchanged. ## Input fields **clientId** `String!` *required* *No description provided in the schema yet.* **secret** `String!` *required* *No description provided in the schema yet.* **name** `String` *No description provided in the schema yet.* # RegisterUserEnrollmentStep Source: https://docs.fluz.app/api-reference/types/register-user-enrollment-step A step the person completes in the registration consent flow after approving the application. Fluz enables consent mode per application. **Enum** A step the person completes in the registration consent flow after approving the application. Fluz enables consent mode per application. ## Values **KYC** `enum value` *No description provided in the schema yet.* **FUNDING\_SOURCE** `enum value` *No description provided in the schema yet.* **PIN** `enum value` *No description provided in the schema yet.* # RegisterUserStatus Source: https://docs.fluz.app/api-reference/types/register-user-status Outcome of registerUser for applications in consent mode, which Fluz enables per application. **Enum** Outcome of registerUser for applications in consent mode. Fluz enables consent mode per application. ## Values **PENDING\_CONSENT** `enum value` The person has not approved the application yet. Show them authorizationUrl; once they approve, call generateUserAccessToken with the externalReferenceId. **LINKED** `enum value` The application already holds a grant for this externalReferenceId. Call generateUserAccessToken with it. # RemovePlaidBankInstitutionInput Source: https://docs.fluz.app/api-reference/types/remove-plaid-bank-institution-input Input for removing a connected Plaid bank institution. **Input object** Input for removing a connected Plaid bank institution. ## Input fields **bankInstitutionAuthId** `UUID!` *required* *No description provided in the schema yet.* **reason** `String` *No description provided in the schema yet.* # RequestAccountTransferInput Source: https://docs.fluz.app/api-reference/types/request-account-transfer-input Represents the request payload for account-transfer approval requests. **Input object** Represents the request payload for account-transfer approval requests. Mirrors createTransfer semantics; the sender is determined from authentication credentials. ## Input fields **idempotencyKey** `UUID!` *required* *No description provided in the schema yet.* **amount** `Float!` *required* *No description provided in the schema yet.* **destination** `TransferDestination` *No description provided in the schema yet.* **bankCardId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **paypalVaultId** `UUID` *No description provided in the schema yet.* **memo** `String` *No description provided in the schema yet.* **transactionCategory** `String` *No description provided in the schema yet.* **attachmentId** `String` ID of a previously uploaded transaction attachment. **useRewardsBalance** `Boolean` When true, fund the transfer from the sender's rewards balance (in addition to the resolved spend/cash balance). Defaults to false (cash-only). # RequestDocumentVerificationLinkInput Source: https://docs.fluz.app/api-reference/types/request-document-verification-link-input Input type for requesting user verification **Input object** Input type for requesting user verification ## Input fields **gaveConsent** `Boolean!` *required* Indicates whether the user has given consent for verification **prefillData** `Boolean` Whether to prefill the verification form with existing user data **reset** `Boolean` Whether to reset an existing verification session **overrideMaxAttempts** `Boolean` Whether to ignore the max attempts check **firstName** `String` The user's first name **lastName** `String` The user's last name **streetLine1** `String` The street address line 1 **streetLine2** `String` The street address line 2 **city** `String` The address city **region** `String` The address region/state **postalCode** `String` The postal code **country** `String` The address country in ISO 3166-1 alpha-2 format **dateOfBirth** `String` The user's date of birth in YYYY-MM-DD format (RFC 3339 Section 5.6) **emailAddress** `String` The user's email address **phoneNumber** `String` The user's phone number in E.164 format # RequestGiftCardPurchaseInput Source: https://docs.fluz.app/api-reference/types/request-gift-card-purchase-input **Input object** *No description provided in the schema yet.* ## Input fields **offerId** `UUID!` *required* *No description provided in the schema yet.* **purchaseAmount** `Float!` *required* *No description provided in the schema yet.* **channel** `SourcePlatformChannel!` *required* *No description provided in the schema yet.* **fluzpayAmount** `Float` *No description provided in the schema yet.* **bankCardId** `UUID` *No description provided in the schema yet.* **bankAccountId** `UUID` *No description provided in the schema yet.* **paypalVaultId** `UUID` *No description provided in the schema yet.* **userCashBalanceId** `UUID` *No description provided in the schema yet.* **seatId** `UUID` *No description provided in the schema yet.* **exclusiveRateId** `UUID` *No description provided in the schema yet.* **merchantSlug** `String` *No description provided in the schema yet.* **gcWarehouseMerchantId** `UUID` *No description provided in the schema yet.* **gcWarehouseUserRewardRate** `Float` *No description provided in the schema yet.* **gcWarehouseDiscountRate** `Float` *No description provided in the schema yet.* **memo** `String` *No description provided in the schema yet.* **categoryId** `UUID` *No description provided in the schema yet.* # RequestInternalTransferInput Source: https://docs.fluz.app/api-reference/types/request-internal-transfer-input **Input object** *No description provided in the schema yet.* ## Input fields **amount** `Float!` *required* *No description provided in the schema yet.* **sourceUserCashBalanceId** `UUID!` *required* *No description provided in the schema yet.* **destinationUserCashBalanceId** `UUID!` *required* *No description provided in the schema yet.* # RequestReimbursementInput Source: https://docs.fluz.app/api-reference/types/request-reimbursement-input **Input object** *No description provided in the schema yet.* ## Input fields **amount** `Float!` *required* *No description provided in the schema yet.* **sourceUserCashBalanceId** `UUID!` *required* *No description provided in the schema yet.* **destinationUserCashBalanceId** `UUID!` *required* *No description provided in the schema yet.* **memo** `String` *No description provided in the schema yet.* **categoryId** `UUID` *No description provided in the schema yet.* # RequestVirtualCardInput Source: https://docs.fluz.app/api-reference/types/request-virtual-card-input **Input object** *No description provided in the schema yet.* ## Input fields **seatId** `UUID!` *required* *No description provided in the schema yet.* **offerId** `UUID!` *required* *No description provided in the schema yet.* **purchaseAmount** `Float!` *required* *No description provided in the schema yet.* **channel** `SourcePlatformChannel!` *required* *No description provided in the schema yet.* **fluzpayAmount** `Float!` *required* *No description provided in the schema yet.* **spendLimitDuration** `VirtualCardSpendLimitDuration!` *required* *No description provided in the schema yet.* **virtualCardPIN** `String` *No description provided in the schema yet.* **brandLocked** `Boolean` *No description provided in the schema yet.* **mccId** `String` *No description provided in the schema yet.* **shareWith** `[String!]` *No description provided in the schema yet.* **pinAuthToken** `String` *No description provided in the schema yet.* # RequestVirtualCardLimitChangeInput Source: https://docs.fluz.app/api-reference/types/request-virtual-card-limit-change-input **Input object** *No description provided in the schema yet.* ## Input fields **virtualCardId** `UUID!` *required* *No description provided in the schema yet.* **spendLimit** `Float!` *required* *No description provided in the schema yet.* **limitDuration** `VirtualCardSpendLimitDuration!` *required* *No description provided in the schema yet.* # ScopeType Source: https://docs.fluz.app/api-reference/types/scope-type Enum describing the various types of access scopes available within the system. **Enum** Enum describing the various types of access scopes available within the system. These scopes define the extent of access granted to user tokens for specific operations. ## Values **LIST\_PAYMENT** `enum value` Allows access to user's payment methods and account balance. **LIST\_PURCHASES** `enum value` Allows access to user's purchase history. **LIST\_OFFERS** `enum value` Allows access to offers catalog and inventory data. **MAKE\_DEPOSIT** `enum value` Allows making a deposit to the user's balance. **MAKE\_INTERNAL\_TRANSFER** `enum value` Allows making an internal transfer between user's balances. **MAKE\_WITHDRAWAL** `enum value` Allows making a withdrawal from the user's balance. **PURCHASE\_GIFTCARD** `enum value` Allows purchasing a gift card. **REVEAL\_GIFTCARD** `enum value` Allows revealing a gift card code. **PCI\_COMPLIANCE** `enum value` Allows handling payment card information. This scope is granted to a PCI compliant developer and all of their applications. You cannot request this when generating a token. PERSONAL/Private applications are exempt from PCI\_COMPLIANCE requirement. **MANAGE\_PAYMENT** `enum value` Allows making changes to user's payment methods. **REVEAL\_VIRTUALCARD** `enum value` Allows revealing a virtual card details. **CREATE\_VIRTUALCARD** `enum value` Allows access to create virtual card. **EDIT\_VIRTUALCARD** `enum value` Allows access to edit virtual card. **VERIFY\_KYC** `enum value` Allows application to request user KYC verification. **MAKE\_PAYOUT\_TRANSFER\_SEND** `enum value` Allows sending payout transfers from user's account to other Fluz accounts. Required when account is the SOURCE in a transfer. **MAKE\_PAYOUT\_TRANSFER\_RECEIVE** `enum value` \[DEPRECATED] Previously used for receiving payout transfers. No longer enforced - users do not need authorization to receive money. Kept for backward compatibility with existing tokens. **QUERY\_RECIPIENT** `enum value` Allows querying a recipient. **REGISTER\_BUSINESS** `enum value` Allows to register a new business. **CREATE\_SHARE\_LINK** `enum value` Allows access to create a link to share an object. **MANAGE\_SUBUSERS** `enum value` Allows managing authorized users (add/remove) on an account. **VIEW\_SUBUSERS** `enum value` Allows read-only access to authorized users on an account. **LIST\_APPROVALS** `enum value` Allows read-only access to approval requests on an account. **MANAGE\_APPROVALS** `enum value` Allows approving or declining approval requests on an account. **REQUEST\_VIRTUAL\_CARD** `enum value` Allows requesting manager approval to create a virtual card. **REQUEST\_VIRTUAL\_CARD\_LIMIT\_CHANGE** `enum value` Allows requesting manager approval to change a virtual card limit. **REQUEST\_GIFT\_CARD** `enum value` Allows requesting manager approval to purchase a gift card. **REQUEST\_INTERNAL\_TRANSFER** `enum value` Allows requesting manager approval for an internal transfer. **REQUEST\_ACCOUNT\_TRANSFER** `enum value` Allows requesting manager approval for an account-to-account transfer. **REQUEST\_REIMBURSEMENT** `enum value` Allows requesting manager approval for a reimbursement. **VIEW\_ERP\_TRANSACTION\_METADATA** `enum value` Allows read-only access to ERP (e.g. QuickBooks) chart of accounts, vendors, customers, and transaction categorization state on an account. **MANAGE\_ERP\_TRANSACTION\_METADATA** `enum value` Allows managing ERP (e.g. QuickBooks) transaction categorization on an account. **BYOI\_PLAID** `enum value` Allows the application to use its own Plaid credentials (BYOI Plaid) for the user's Plaid bank links. # SetBackupFundingSourceInput Source: https://docs.fluz.app/api-reference/types/set-backup-funding-source-input Input type for setting an account's backup funding source. **Input object** Input type for setting an account's backup funding source. The bank card must belong to the account and be ACTIVE. ## Input fields **bankCardId** `UUID!` *required* The ID of the bank card to set as the backup funding source. # SetPrimaryFundingSourceInput Source: https://docs.fluz.app/api-reference/types/set-primary-funding-source-input Input type for setting an account's primary funding source. **Input object** Input type for setting an account's primary funding source. The bank account must belong to the account and be ENABLED. ## Input fields **bankAccountId** `UUID!` *required* The ID of the bank account to set as the primary funding source. # SetVirtualCardPINInput Source: https://docs.fluz.app/api-reference/types/set-virtual-card-pininput Input type for set Virtual Card PIN. **Input object** Input type for set Virtual Card PIN. ## Input fields **pin** `String!` *required* The PIN to set on the cards. Must match your user PIN. # ShareCardType Source: https://docs.fluz.app/api-reference/types/share-card-type ShareCardType represents the type of object you want to share. **Enum** ShareCardType represents the type of object you want to share. ## Values **SINGLE\_LOAD** `enum value` *No description provided in the schema yet.* # ShareLinkRecipientRegistrationInput Source: https://docs.fluz.app/api-reference/types/share-link-recipient-registration-input Input type for generateVCShareLinks. **Input object** Input type for generateVCShareLinks. ## Input fields **firstName** `String!` *required* The first name of the recipient to register. **lastName** `String!` *required* The last name of the recipient to register. **phoneNumber** `String!` *required* The phone number of the recipient to register. **regionCode** `String!` *required* The region code of the recipient's phone number. **emailAddress** `String!` *required* The email address of the recipient to register. **dateOfBirth** `String!` *required* The date of birth of the recipient to register. **billingAddress** `VirtualCardBillingAddressInput!` *required* Billing address used for the recipient's virtual card. **acceptCardholderAgreement** `Boolean!` *required* Must be `true`. Records the recipient's acceptance of the cardholder agreement on their behalf. # SubmitBulkOperationInput Source: https://docs.fluz.app/api-reference/types/submit-bulk-operation-input Submit an asynchronous bulk operation across your connected users. **Input object** Submit an asynchronous bulk operation across your connected users. ## Input fields **operation** `BulkOperationType!` *required* The operation to perform across the target users. **targetSpec** `BulkTargetSpecInput` Which connected users this operation applies to. Required ONLY for the read exports (GET\_BALANCES\_EXPORT, GET\_TRANSACTIONS\_EXPORT). The write operations (CREATE\_TRANSFER, DEPOSIT\_CASH\_BALANCE, UPDATE\_TRANSACTION\_METADATA) name their targets in their own items, so targetSpec is ignored for those. **exportOptions** `BulkExportOptionsInput` Options for the read-export operations. Ignored by other operations. **transferOptions** `BulkTransferOptionsInput` Per-destination amounts for CREATE\_TRANSFER. Required for that operation; ignored by others. **depositOptions** `BulkDepositOptionsInput` Per-target deposits for DEPOSIT\_CASH\_BALANCE. Required for that operation; ignored by others. **metadataOptions** `BulkMetadataOptionsInput` Per-target transaction edits for UPDATE\_TRANSACTION\_METADATA. Required for that operation; ignored by others. **idempotencyKey** `String` Your idempotency key for this submission. May instead be supplied via the Idempotency-Key header; if both are given they must match. Reusing a key with a different request is rejected. # TransactionFilterInput Source: https://docs.fluz.app/api-reference/types/transaction-filter-input Input filter for querying transactions. **Input object** Input filter for querying transactions. ## Input fields **recordId** `[UUID]` Filter by specific transaction record IDs. **status** `[TransactionStatus]` Filter by transaction status. **amount** `Float` Filter by exact amount. **amountGte** `Float` Filter by minimum amount (greater than or equal). **amountLte** `Float` Filter by maximum amount (less than or equal). **cashbackAmount** `Float` Filter by exact cashback amount. **cashbackAmountGte** `Float` Filter by minimum cashback amount. **cashbackAmountLte** `Float` Filter by maximum cashback amount. **cashbackPercentage** `Float` Filter by exact cashback percentage. **cashbackPercentageGte** `Float` Filter by minimum cashback percentage. **cashbackPercentageLte** `Float` Filter by maximum cashback percentage. **feeAmount** `Float` Filter by exact fee amount. **feeAmountGte** `Float` Filter by minimum fee amount. **feeAmountLte** `Float` Filter by maximum fee amount. **createdGte** `DateTime` Filter by creation date (greater than or equal). **createdLte** `DateTime` Filter by creation date (less than or equal). **updatedGte** `DateTime` Filter by update date (greater than or equal). **updatedLte** `DateTime` Filter by update date (less than or equal). **merchantId** `[UUID]` Filter by merchant IDs. **merchant** `[String]` Filter by merchant names (destination field). **transactionType** `[String]` Filter by transaction types. **channel** `[String!]` Filter by channel. **category** `[String]` Filter by transaction category. **virtualCardProgram** `[String]` Filter by virtual card program. **Deprecated:** Obsolete. **virtualCard** `[UUID]` Filter by specific virtual card IDs. **fundingSource** `[String]` Filter by funding source names. **userCashBalanceId** `[UUID]` Filter by spend account IDs. **referenceId** `String` Filter by reference ID. **liabilityId** `UUID` Filter by liability ID (for bill payments). # TransferDestination Source: https://docs.fluz.app/api-reference/types/transfer-destination Identifies the destination for a transfer. **Input object** Identifies the destination for a transfer. Provide either accountId or externalReferenceId (not both). ## Input fields **accountId** `UUID` The destination account ID. **externalReferenceId** `String` Alternative to accountId. Identifies the destination by an external reference ID. **userCashBalanceId** `UUID` Target a specific cash balance on the destination account. Must belong to the destination account. If omitted, the system resolves the appropriate cash balance automatically. # TransferInternalBalanceInput Source: https://docs.fluz.app/api-reference/types/transfer-internal-balance-input Input type for a transfer between user cash balance accounts. **Input object** Input type for a transfer between user cash balance accounts. ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **amount** `Float!` *required* The amount to transfer. **sourceUserCashBalanceId** `UUID!` *required* Unique identifier for the user's *source* cash balance. **destinationUserCashBalanceId** `UUID!` *required* Unique identifier for the user's *destination* cash balance. **memo** `String` Memo for the transfer. Limited to 255 characters. **transactionCategory** `String` Optional category name to attach to this transfer transaction. **attachmentId** `String` ID of a previously uploaded attachment (PDF or PNG). Use the /api/v1/file-upload/transaction-memo-attachment endpoint to upload. # UnlockVirtualCardInput Source: https://docs.fluz.app/api-reference/types/unlock-virtual-card-input Input type for unlock virtual card. **Input object** Input type for unlock virtual card. ## Input fields **virtualCardId** `UUID!` *required* The virtual card to update. # UpdateBankCardNicknameInput Source: https://docs.fluz.app/api-reference/types/update-bank-card-nickname-input Input type for updating a bank card's nickname. **Input object** Input type for updating a bank card's nickname. ## Input fields **bankCardId** `UUID!` *required* The ID of the bank card to update. **nickname** `String!` *required* The nickname for the bank card. # UpdateBankCardPreferredMerchantCategoryCodeInput Source: https://docs.fluz.app/api-reference/types/update-bank-card-preferred-merchant-category-code-input Input type for updating a bank card's preferred merchant category code. **Input object** Input type for updating a bank card's preferred merchant category code. ## Input fields **bankCardId** `UUID!` *required* The ID of the bank card to update. **preferredMerchantCategoryCode** `String!` *required* The preferred merchant category code for the bank card. # UpdateErpTransactionMetadataInput Source: https://docs.fluz.app/api-reference/types/update-erp-transaction-metadata-input Optional ERP-aware categorization for this transaction (e.g. **Input object** Optional ERP-aware categorization for this transaction (e.g. QuickBooks category, vendor, customer, billable status, and memo). Omit this field, or pass null, to leave ERP categorization unchanged. An empty object is not a valid update — omit the field entirely instead. Within the object: omit a field to leave it unchanged, pass null on a \*ReferenceItemId field to clear it, or pass a value to set it. Use categoryName, vendorName, or customerName to create or select an entity by name rather than by ID. These fields are ignored when the corresponding \*ReferenceItemId field is provided. This memo is separate from the top-level memo field and has its own length limit (up to 4000 characters, vs. 255 for the top-level memo). Setting one does not affect the other. ## Input fields **categoryReferenceItemId** `UUID` *No description provided in the schema yet.* **categoryName** `String` *No description provided in the schema yet.* **vendorReferenceItemId** `UUID` *No description provided in the schema yet.* **vendorName** `String` *No description provided in the schema yet.* **customerReferenceItemId** `UUID` *No description provided in the schema yet.* **customerName** `String` *No description provided in the schema yet.* **isBillable** `Boolean` *No description provided in the schema yet.* **memo** `String` *No description provided in the schema yet.* # UpdateTransactionMetadataInput Source: https://docs.fluz.app/api-reference/types/update-transaction-metadata-input Input for updating transaction metadata (memo, category, attachment). **Input object** Input for updating transaction metadata (memo, category, attachment). Only provided fields are updated — omitted fields are left unchanged. Set a field to null to clear it. ## Input fields **recordId** `UUID!` *required* The record ID of the transaction to update. **memo** `String` User-defined memo to attach to this transaction. Max 255 characters. Set to null to clear. **transactionCategory** `String` Category name to attach to this transaction. Set to null to clear. **attachmentId** `String` ID of a previously uploaded attachment (PDF or PNG). Use the /api/v1/file-upload/transaction-memo-attachment endpoint to upload. Set to null to clear. **erpTransactionMetadata** `UpdateErpTransactionMetadataInput` Optional ERP-aware categorization to apply in the same call. See UpdateErpTransactionMetadataInput for the full null/omission semantics. If this portion fails, the whole call fails and nothing is updated. # UpdateUserCashBalanceInput Source: https://docs.fluz.app/api-reference/types/update-user-cash-balance-input Input type for updating a user cash balance account. **Input object** Input type for updating a user cash balance account. ## Input fields **userCashBalanceId** `UUID!` *required* Identifier of the UserCashBalance that needs to be updated. **nickname** `String!` *required* Nickname for the cash balance account. # UserAddressInput Source: https://docs.fluz.app/api-reference/types/user-address-input Input type for adding an address to the user. **Input object** Input type for adding an address to the user. ## Input fields **streetAddressLine1** `String!` *required* *No description provided in the schema yet.* **streetAddressLine2** `String` *No description provided in the schema yet.* **country** `String!` *required* *No description provided in the schema yet.* **city** `String!` *required* *No description provided in the schema yet.* **state** `String!` *required* *No description provided in the schema yet.* **postalCode** `String` *No description provided in the schema yet.* # UserCashBalanceFilterInput Source: https://docs.fluz.app/api-reference/types/user-cash-balance-filter-input Input filter for querying cash balances. **Input object** Input filter for querying cash balances. ## Input fields **userCashBalanceId** `[UUID!]` Filter by specific cash balance IDs. **nickname** `[String!]` Filter by cash balance nicknames. **status** `[UserCashBalanceStatus!]` Filter by cash balance status. **isDefault** `Boolean` Whether cash balance is the default one. **createdGte** `DateTime` Filter by creation date (greater than or equal). **createdLte** `DateTime` Filter by creation date (less than or equal). **updatedGte** `DateTime` Filter by update date (greater than or equal). **updatedLte** `DateTime` Filter by update date (less than or equal). # UserLookupInput Source: https://docs.fluz.app/api-reference/types/user-lookup-input Input for looking up a user recipient by a specific identifier. **Input object** Input for looking up a user recipient by a specific identifier. Exactly one identifier must be provided. ## Input fields **phoneNumber** `String` Lookup by phone number (E.164 format recommended, e.g., +14155551234). 10-digit US numbers are automatically normalized. **email** `String` Lookup by email address (case-insensitive). # UserPurchaseFilterInput Source: https://docs.fluz.app/api-reference/types/user-purchase-filter-input Input filter type for getUserPurchases - Default [TOKEN_USER]. **Input object** Input filter type for getUserPurchases * Default \[TOKEN\_USER]. ## Input fields **scope** `[PurchaseScopeFilter!]` Which purchases are visible. `TOKEN_USER` (the default) returns only purchases made by the token's own user. `TOKEN_ACCOUNT` returns any purchase on the account. The values combine with AND, not OR: passing both applies both filters, returning only purchases the token's user made on the token's account. To see orders placed by other users on the account, pass `TOKEN_ACCOUNT` on its own. **purchaseDisplayId** `String` Fetch a single purchase by the `purchaseDisplayId` returned at purchase time — the same value exposed as `referenceId` on [`getTransactions`](/api-reference/queries/get-transactions). Use it to look up one order instead of paginating the full history. Applied on top of `scope` — it narrows what `scope` already allows and never widens it. A purchase placed by another user on the account is therefore only returned when `scope` is `TOKEN_ACCOUNT` on its own, since including `TOKEN_USER` also restricts results to the token's own user. An empty string is treated as "not provided" and returns the list filtered by `scope`. # VirtualCardBillingAddressInput Source: https://docs.fluz.app/api-reference/types/virtual-card-billing-address-input Input type for a new billing address when issuing a virtual card. **Input object** Input type for a new billing address when issuing a virtual card. All fields marked non-null are required by the card issuer (Highnote). ## Input fields **streetAddressLine1** `String!` *required* Street address (e.g., "123 Main St"). PO boxes are not accepted by the issuer. **streetAddressLine2** `String` Optional secondary line (e.g., apartment, suite). **country** `String!` *required* Country name. Currently only "United States" is supported by the card issuer. **city** `String!` *required* City name. **state** `String!` *required* State name (e.g., "New York") or two-letter US state code. **postalCode** `String!` *required* 5-digit US ZIP code. Required by the card issuer. # VirtualCardOrderItemInput Source: https://docs.fluz.app/api-reference/types/virtual-card-order-item-input **Input object** *No description provided in the schema yet.* ## Input fields **quantity** `Int!` *required* The number of virtual cards to create with this specific configuration. **spendLimit** `Float!` *required* The maximum amount that you can charge to the card. You will only be charged for the amount you actually used. **spendLimitDuration** `VirtualCardSpendLimitDuration` Card limit duration type. Default is Lifetime. **lockDate** `String` The date when the card will be locked. The default is 47 months. **lockCardNextUse** `Boolean` The setting to lock the card after next use. The default is false. **cardNickname** `String` The card's nickname. This nickname will be applied to all cards in this item. **primaryFundingSource** `VirtualCardFundingSource` Primary Funding Source for Virtual Card. **bankAccountId** `UUID` The unique identifier for the bank account. **userCashBalanceId** `UUID` The unique identifier of the user cash balance id **billingAddress** `VirtualCardBillingAddressInput` A new billing address to register with the card program before issuing cards in this item. A UserAddress (type: BILLING) is created if one does not already exist with the same street / city / state / postal code. Ignored when userAddressId is also provided. **userAddressId** `UUID` An existing UserAddress ID belonging to the caller's account to use as the billing address. Takes precedence over billingAddress when both are provided. **memo** `String` An optional note to attach to the transaction. **transactionCategory** `String` An optional category name. A matching category will be found or created for the account. # VirtualCardTransactionFiltersInput Source: https://docs.fluz.app/api-reference/types/virtual-card-transaction-filters-input Input type for filtering virtual card transactions. **Input object** Input type for filtering virtual card transactions. ## Input fields **transactionTypes** `[VirtualCardTransactionListType!]` A list of transaction types to filter by (e.g., PURCHASE, REFUND). **dateRangeStart** `String` Inclusive lower bound for the transaction date range (ISO 8601 timestamp). **dateRangeEnd** `String` Inclusive upper bound for the transaction date range (ISO 8601 timestamp). # WithdrawCashBalanceInput Source: https://docs.fluz.app/api-reference/types/withdraw-cash-balance-input **Input object** *No description provided in the schema yet.* ## Input fields **idempotencyKey** `UUID!` *required* A unique client generated UUID to ensure a request is processed only once. **amount** `Float!` *required* The amount to withdraw. **method** `WithdrawMethods!` *required* The withdrawal method to use. **source** `WithdrawSource` The source balance from which to withdraw funds. **bankAccountId** `UUID` Identifier of the bank account for ACH withdrawals. **bankCardId** `UUID` Identifier of the bank card for push-to-card withdrawals. Required when method is BANK\_CARD. The card must support OCT transactions. **paypalVaultId** `UUID` Identifier of the PayPal vault for PayPal withdrawals. **venmoAccountId** `UUID` Identifier of the Venmo account for Venmo withdrawals. **cashBalanceId** `UUID` Identifier of the specific cash balance account to withdraw from (required when source is CASH\_BALANCE). **isExpedited** `Boolean` If true, process the withdrawal immediately (expedited). If false or omitted, the withdrawal is processed on a standard settlement schedule. Expedited withdrawals may incur different fees — use the getWithdrawFeeEstimate query to preview. Currently applicable to BANK\_CARD withdrawals. # Authentication reference Source: https://docs.fluz.app/api-reference/authentication The exact request/response shapes for minting user access tokens with your API key. For the conceptual overview, see [Authentication](/concepts/authentication). This page documents the exact GraphQL contract for minting tokens on the transactional graph: | Environment | Endpoint | | - | - | | Staging | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | | Production | `https://transactional-graph.fluzapp.com/api/v1/graphql` | ## `generateUserAccessToken` Mint a user access token with your application's **API key**. The API key is sent in the `Authorization: Basic ` header — it is never passed as a GraphQL argument. The token is minted for a `userId` / `accountId` pair (shown alongside your API key in the Developer Console) and carries the scopes you request. ### Request ```http theme={null} POST /api/v1/graphql HTTP/1.1 Host: transactional-graph.staging.fluzapp.com Authorization: Basic Content-Type: application/json { "query": "mutation ($userId: UUID!, $accountId: UUID!, $scopes: [ScopeType!]!) { generateUserAccessToken(userId: $userId, accountId: $accountId, scopes: $scopes) { token scopes } }", "variables": { "userId": "", "accountId": "", "scopes": ["LIST_OFFERS", "PURCHASE_GIFTCARD"] } } ``` An optional `seatId: UUID` argument selects the seat used for transactions; it defaults to the most recently created seat. See the full argument reference at [generateUserAccessToken](/api-reference/mutations/generate-user-access-token). ### Response ```json theme={null} { "data": { "generateUserAccessToken": { "token": "eyJhbGciOi...", "scopes": ["LIST_OFFERS", "PURCHASE_GIFTCARD"] } } } ``` Attach the returned `token` as `Authorization: Bearer ` on subsequent requests. Tokens are short-lived JWTs — mint a new one when it expires (see [Replace an expired access token](/get-started/refresh-expired-access-token)). ### Requirements * The user must have granted your application the requested scopes; unknown or ungranted scopes cause the mutation to fail. * `PCI_COMPLIANCE` cannot be requested when generating a token — it is granted at the application level to PCI-compliant developers. ## Discovering `userId` / `accountId` Also authorized by `Basic `: * [`getApplicationUsers`](/api-reference/queries/get-application-users) — the users who have granted scopes to your application, with their accounts. * [`getAccountsByUserId`](/api-reference/queries/get-accounts-by-user-id) — all accounts for a given user. * [`getApplicationScopes`](/api-reference/queries/get-application-scopes) — the scopes available to your application. ## Customer accounts (OAuth platforms) If you are building a platform that operates on customer accounts, the customer first authorizes your app through the OAuth grant flow, and your server exchanges the resulting code at the token exchange endpoint — see [the OAuth grant flow](/client-facing-o-auth-grant-flow) and [Build a platform](/build-a-platform). ## Scopes See the scope table in [Authentication](/concepts/authentication#scopes). Unknown scopes cause the mutation to fail. # Errors Source: https://docs.fluz.app/api-reference/errors Error codes, HTTP status codes, and how to troubleshoot with request IDs. GraphQL responses use HTTP 200 for successes, application-level errors, and variables that don't match their declared type (`BAD_USER_INPUT`). Inspect the `errors` array to know what happened. Other requests rejected before they run use other statuses, for example `429` when you exceed the request rate, and `400` with `GRAPHQL_VALIDATION_FAILED` when a query doesn't match the schema or exceeds the [query cost limits](/concepts/rate-limits). A query that doesn't parse, or is over the token limit, gets `500` with the same `GRAPHQL_VALIDATION_FAILED` code. That's a rejected query, not a server fault, so don't retry it. ## Error shape ```json theme={null} { "errors": [ { "message": "Offer out of stock", "extensions": { "code": "GC-0009", "path": ["purchaseGiftCard"], "requestId": "req_01H...Offer out of stock", "code": "GC-0009", "path": ["purchaseGiftCard"] } ] } ``` Error codes are namespaced by area: * `AUTH-*` — authentication and token issues * `BS-*` — general business / validation errors * `GC-*` — gift card operations * `VC-*` — virtual card operations ## Domain error codes Fluz uses prefixed error codes so you can branch on the failing subsystem. | Prefix | Domain | Example | | - | - | - | | `AUTH-XXXX` | Authentication and authorization | Invalid token, missing scope | | `VC-XXXX` | Virtual cards | `VC-0025` — invalid or unsupported address | | `GC-XXXX` | Gift cards | `GC-0009` — offer out of stock; `GC-0002` — amount not in allowed denominations | | `BS-XXXX` | Balances and settlement | Insufficient funds, holds | Codes not in this list surface as generic validation or server errors. ## Troubleshooting 1. **Check scope on `AUTH-*`.** These almost always mean the token was minted without the required scope; mint a new token. 2. `AUTH-0002`**Re-query before retrying `GC-*`.** Offers and stock change frequently — pull a fresh offer and retry. 3. **Re-query before retrying business errors.** `GC-0009` (stock) and similar mean the world changed — don't retry with stale**Don't retry validation errors.** `VC-*` and `BS-*` codes with `4xx`-like intent will fail identically on retry until you fix the input or balance. 4. **Retry transient server errors with backoff and jitterRetry 5xx / network errors with jitter.** Money-moving mutations are de-duplicated; see [Idempotency](/concepts/idempotency). # Pagination Source: https://docs.fluz.app/api-reference/pagination Offset pagination, page-size caps, and the connection response shape. List operations across the API paginate with offset inputs and return connection-style objects. There are two page-size regimes depending on the surface. ## Request shape Pass an offset input in the operation's `paginate` argument: | Input | Used by | `limit` max | Fields | | - | - | - | - | | [`OffsetInput`](/api-reference/types/offset-input) | Standard listings (`getTransactions`, `getUserCashBalances`, …) | **20** | `limit`, `offset` | | [`BulkPaginationInput`](/api-reference/types/bulk-pagination-input) | Bulk API listings | **100** (also the default) | `limit`, `offset` | ```graphql theme={null} query { getTransactions( filter: { } paginate: { limit: 20, offset: 0 } ) { transactions { transactionId amount createdAt } totalCount hasNextPage } } ``` ## Response shape Paginated queries return a connection object with the items plus paging metadata — for example [`TransactionConnection`](/api-reference/types/transaction-connection): * the item list (e.g. `transactions`) * `totalCount` — total items matching the filter * `hasNextPage` — whether more results are available Other connection types follow the same pattern: [`UserCashBalanceConnection`](/api-reference/types/user-cash-balance-connection), [`DeclinedTransactionConnection`](/api-reference/types/declined-transaction-connection), and [`PlaidBankTransactionPage`](/api-reference/types/plaid-bank-transaction-page). ## Walking a full result set Increase `offset` by `limit` while `hasNextPage` is `true`: 1. Request `{ limit: 20, offset: 0 }`. 2. If `hasNextPage` is `true`, request `{ limit: 20, offset: 20 }`, then `40`, and so on. 3. Stop when `hasNextPage` is `false`. > [!NOTE] > > Results can shift between pages if new records are created while you're paging — transaction listings are ordered most-recent-first. For large exports, filter by a fixed date range so the underlying set is stable while you walk it. # Rate limits Source: https://docs.fluz.app/api-reference/rate-limits Request rate limits, query cost limits, and best practices for staying within them. Fluz applies fair-use rate limits on the transactional graph and the OAuth service. Limits are tuned to typical integration patterns; contact your account team if your production workload needs headroom. ## When you're throttled Rate-limited requests return HTTP `429` with the body `{"message": "Rate limit exceeded"}`. Back off before retrying and add jitter to smooth bursts. A query over the cost, depth, or alias limits is rejected with HTTP `400` instead, with `GRAPHQL_VALIDATION_FAILED` in the GraphQL `errors` array. Retrying won't help: remove fields or split the query. [Rate limits](/concepts/rate-limits) lists the limits and how query cost is counted. ## Best practices 1. **Ask for less.** Trim your selection set to the fields you actually use. 2. **Fetch lists in pages.** A query that returns 20 items costs the same as one that returns a single item, and far less than 20 separate requests. 3. **Use webhooks instead of polling.** See [Configure App Widget](/developers/configure-app-widget) for webhook setup. 4. **Retry with exponential backoff and jitter** — never in a tight loop. 5. **Refresh access tokens ahead of expiry** to avoid 401 → mint-token → retry cycles under load. # Upload sole proprietorship document Source: https://docs.fluz.app/api-reference/rest/upload-sole-proprietorship-document REST endpoint for uploading the supporting document required when registering a SOLE_PROPRIETORSHIP business. Most of the Fluz API is GraphQL — file upload is the exception. When [`registerBusiness`](/api-reference/mutations/register-business) is called with `businessStructure: SOLE_PROPRIETORSHIP`, upload the supporting document through this REST endpoint first, then pass the returned URL as `soleProprietorshipDocumentUrl` in [`RegisterBusinessInput`](/api-reference/types/register-business-input). ## Endpoint | Environment | URL | | - | - | | Staging | `POST https://transactional-graph.staging.fluzapp.com/api/v1/file-upload/sole-proprietorship-document` | | Live | `POST https://transactional-graph.fluzapp.com/api/v1/file-upload/sole-proprietorship-document` | ## Request Send the document as `multipart/form-data`, authenticated with the same access token you use for the registration flow: ```bash theme={null} curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/file-upload/sole-proprietorship-document \ -H "Authorization: Bearer " \ -F "file=@business-license.pdf" ``` ## Response Returns the public URL of the uploaded document — see [`UploadSoleProprietorshipDocumentResponse`](/api-reference/types/upload-sole-proprietorship-document-response): ```json theme={null} { "url": "https://storage.fluzapp.com/documents/..." } ``` Supply this value in `soleProprietorshipDocumentUrl` when submitting `registerBusiness`. For the full registration walkthrough, including the KYB lifecycle, see [Business registration](/business-registration). # BasicUserCashBalance Source: https://docs.fluz.app/api-reference/types/basic-user-cash-balance BasicUserCashBalance interface **Interface** BasicUserCashBalance interface ## Fields **userCashBalanceId** `UUID!` Unique identifier for the user cash balance. **totalCashBalance** `String!` Total cash balance. **availableCashBalance** `String!` Available cash balance. **lifetimeCashBalance** `String!` Lifetime cash balance (cumulative total ever deposited). **nickname** `String` Custom nickname for the balance. **status** `UserCashBalanceStatus!` Status of the cash balance account. **createdAt** `DateTime!` Date and time when the account was created. # DateTime Source: https://docs.fluz.app/api-reference/types/date-time A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. **Scalar** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. Custom scalar. # ShareLinkRecipientRegistrationStatus Source: https://docs.fluz.app/api-reference/types/share-link-recipient-registration-status ShareLinkRecipientRegistrationStatus represents the outcome of registering one recipient during a generateVCShareLinks call with shareMethod REGISTER_USER. **Enum** ShareLinkRecipientRegistrationStatus represents the outcome of registering one recipient during a generateVCShareLinks call with shareMethod REGISTER\_USER. ## Values **CREATED** `enum value` Fluz created a new placeholder user for this recipient. **REUSED** `enum value` Fluz matched this recipient to an existing placeholder user and reused it instead of creating a new one. **FAILED** `enum value` Registration failed for this recipient — see `error` for the reason. No share link was generated for this entry. # ShareMethodType Source: https://docs.fluz.app/api-reference/types/share-method-type ShareMethodType represents the type of share method. **Enum** ShareMethodType represents the type of share method. ## Values **PHONE\_NUMBER** `enum value` Fluz texts the hosted link to each recipient in `recipientListPhone`. The virtual card is created at claim time, not at generation. **EMAIL** `enum value` Fluz emails the hosted link to each recipient in `recipientListEmail`. The virtual card is created at claim time, not at generation. **GENERATE\_URL** `enum value` Fluz returns the hosted URLs in the response for you to distribute yourself. No recipient data is required or accepted. The virtual card is created at claim time, not at generation. **EXISTING\_USER** `enum value` Binds each link to a known Fluz user ID supplied in `recipientUserIds`; only that user can claim it. Unlike the other methods, the virtual card is created immediately, at generation time, rather than at claim. **REGISTER\_USER** `enum value` Fluz creates or reuses a placeholder user (no seat) for each entry in `recipientRegistrations`, then binds the link to that user the same way as `EXISTING_USER`. The virtual card is created immediately, at generation time, rather than at claim. # ShareObjectStatus Source: https://docs.fluz.app/api-reference/types/share-object-status ShareObjectStatus represents the status of the shared object. **Enum** ShareObjectStatus represents the status of the shared object. ## Values **PENDING** `enum value` *No description provided in the schema yet.* **ISSUED** `enum value` *No description provided in the schema yet.* **USED** `enum value` *No description provided in the schema yet.* **EXPIRED** `enum value` *No description provided in the schema yet.* # ShareObjectType Source: https://docs.fluz.app/api-reference/types/share-object-type ShareObjectType represents the type of object you want to share. **Enum** ShareObjectType represents the type of object you want to share. ## Values **VIRTUAL\_CARD** `enum value` *No description provided in the schema yet.* # SortOrder Source: https://docs.fluz.app/api-reference/types/sort-order **Enum** *No description provided in the schema yet.* ## Values **ASC** `enum value` *No description provided in the schema yet.* **DESC** `enum value` *No description provided in the schema yet.* # SourcePlatformChannel Source: https://docs.fluz.app/api-reference/types/source-platform-channel Source channel used in approval request payloads. **Enum** Source channel used in approval request payloads. ## Values **MOBILE\_APP** `enum value` *No description provided in the schema yet.* **BROWSER** `enum value` *No description provided in the schema yet.* **ENTERPRISE** `enum value` *No description provided in the schema yet.* **API** `enum value` *No description provided in the schema yet.* **UWP** `enum value` *No description provided in the schema yet.* **CSV** `enum value` *No description provided in the schema yet.* **WIDGET** `enum value` *No description provided in the schema yet.* **APP\_CLIP** `enum value` *No description provided in the schema yet.* **SCHEDULED\_TASK\_HANDLER** `enum value` *No description provided in the schema yet.* # StockInfoType Source: https://docs.fluz.app/api-reference/types/stock-info-type StockInfoType describes the stock for either fixed or variable offers. **Union** StockInfoType describes the stock for either fixed or variable offers. Used by the Offer type. ## Possible types * [`StockInfoFixedType`](/api-reference/types/stock-info-fixed-type) * [`StockInfoVariableType`](/api-reference/types/stock-info-variable-type) # TransactionStatus Source: https://docs.fluz.app/api-reference/types/transaction-status TransactionStatus represents the current state of a transaction. **Enum** TransactionStatus represents the current state of a transaction. ## Values **PENDING** `enum value` Transaction is pending and not yet settled. **SETTLED** `enum value` Transaction has been completed and settled. # UACRoleStatusType Source: https://docs.fluz.app/api-reference/types/uacrole-status-type Enum describing the status of a UAC role assignment. **Enum** Enum describing the status of a UAC role assignment. ## Values **PENDING** `enum value` *No description provided in the schema yet.* **ACTIVE** `enum value` *No description provided in the schema yet.* **INACTIVE** `enum value` *No description provided in the schema yet.* **DECLINED** `enum value` *No description provided in the schema yet.* # UACRoleType Source: https://docs.fluz.app/api-reference/types/uacrole-type Enum describing the UAC roles that can be assigned to authorized users. **Enum** Enum describing the UAC roles that can be assigned to authorized users. OWNER is excluded - it cannot be assigned or removed via this API. ## Values **ADMIN** `enum value` *No description provided in the schema yet.* **MANAGER** `enum value` *No description provided in the schema yet.* **SPENDER** `enum value` *No description provided in the schema yet.* **VIEWER** `enum value` *No description provided in the schema yet.* **GUEST** `enum value` *No description provided in the schema yet.* # UserCashBalanceStatus Source: https://docs.fluz.app/api-reference/types/user-cash-balance-status Enum representing the status of a user cash balance account. **Enum** Enum representing the status of a user cash balance account. ## Values **ACTIVE** `enum value` The cash balance account is active and can be used. **CLOSED** `enum value` The cash balance account has been closed. **SUSPENDED** `enum value` The cash balance account is temporarily suspended. # UserVerificationStatus Source: https://docs.fluz.app/api-reference/types/user-verification-status Enum describing the user verification status. **Enum** Enum describing the user verification status. ## Values **APPROVED** `enum value` User verification is approved. **DECLINED** `enum value` User verification is declined. **DUPLICATE** `enum value` User verification is a duplicate. **ERROR** `enum value` User verification error. # UUID Source: https://docs.fluz.app/api-reference/types/uuid A version 4 UUID is randomly generated. **Scalar** A version 4 UUID is randomly generated. 4 bits are used to indicate version 4, and 2 or 3 bits to indicate the variant (102 or 1102 for variants 1 and 2 respectively) Custom scalar. # VirtualAccountNumberDebitControlMode Source: https://docs.fluz.app/api-reference/types/virtual-account-number-debit-control-mode Controls which debits are permitted against a virtual account number. **Enum** Controls which debits are permitted against a virtual account number. ## Values **ALLOW\_ALL** `enum value` Allow all debits. **BLOCK\_ALL** `enum value` Block all debits. **WHITELIST\_ONLY** `enum value` Only allow debits from whitelisted originators. **APPROVAL\_REQUIRED** `enum value` Debits require explicit approval before they are posted. # VirtualAccountNumberStatus Source: https://docs.fluz.app/api-reference/types/virtual-account-number-status Status of a virtual account number. **Enum** Status of a virtual account number. ## Values **ACTIVE** `enum value` The virtual account number is active and can receive deposits. **CLOSED** `enum value` The virtual account number has been closed and can no longer be used. # VirtualCardBulkOrderStatus Source: https://docs.fluz.app/api-reference/types/virtual-card-bulk-order-status VirtualCardBulkOrderStatus represents the current state of a bulk virtual card order status. **Enum** VirtualCardBulkOrderStatus represents the current state of a bulk virtual card order status. ## Values **PENDING** `enum value` *No description provided in the schema yet.* **IN\_PROGRESS** `enum value` *No description provided in the schema yet.* **COMPLETED** `enum value` *No description provided in the schema yet.* **FAILED** `enum value` *No description provided in the schema yet.* **CANCELED** `enum value` *No description provided in the schema yet.* # VirtualCardFundingSource Source: https://docs.fluz.app/api-reference/types/virtual-card-funding-source VirtualCardFundingSource represents the funding sources available for Virtual Cards. **Enum** VirtualCardFundingSource represents the funding sources available for Virtual Cards. ## Values **FLUZ\_BALANCE** `enum value` *No description provided in the schema yet.* **BANK\_ACCOUNT** `enum value` *No description provided in the schema yet.* # VirtualCardNetwork Source: https://docs.fluz.app/api-reference/types/virtual-card-network VirtualCardNetwork represents the card network of a virtual card (MASTERCARD or VISA). **Enum** VirtualCardNetwork represents the card network of a virtual card (MASTERCARD or VISA). ## Values **MASTERCARD** `enum value` *No description provided in the schema yet.* **VISA** `enum value` *No description provided in the schema yet.* # VirtualCardOfferType Source: https://docs.fluz.app/api-reference/types/virtual-card-offer-type VirtualCardOfferType represents the card type of a virtual card (DEBIT or PREPAID). **Enum** VirtualCardOfferType represents the card type of a virtual card (DEBIT or PREPAID). ## Values **DEBIT** `enum value` *No description provided in the schema yet.* **PREPAID** `enum value` *No description provided in the schema yet.* # VirtualCardSpendLimitDuration Source: https://docs.fluz.app/api-reference/types/virtual-card-spend-limit-duration VirtualCardSpendLimitDuration represents the card limit duration type. **Enum** VirtualCardSpendLimitDuration represents the card limit duration type. ## Values **DAILY** `enum value` *No description provided in the schema yet.* **WEEKLY** `enum value` *No description provided in the schema yet.* **MONTHLY** `enum value` *No description provided in the schema yet.* **ANNUAL** `enum value` *No description provided in the schema yet.* **LIFETIME** `enum value` *No description provided in the schema yet.* # VirtualCardStatus Source: https://docs.fluz.app/api-reference/types/virtual-card-status VirtualCardStatus represents the current state of a virtual card. **Enum** VirtualCardStatus represents the current state of a virtual card. ## Values **PENDING** `enum value` *No description provided in the schema yet.* **ACTIVE** `enum value` *No description provided in the schema yet.* **USED** `enum value` *No description provided in the schema yet.* **CANCELLED** `enum value` *No description provided in the schema yet.* **REFUNDED** `enum value` *No description provided in the schema yet.* **EXPIRED** `enum value` *No description provided in the schema yet.* **DISPUTED** `enum value` *No description provided in the schema yet.* # VirtualCardTransactionListType Source: https://docs.fluz.app/api-reference/types/virtual-card-transaction-list-type **Enum** *No description provided in the schema yet.* ## Values **PURCHASE** `enum value` *No description provided in the schema yet.* **REFUND** `enum value` *No description provided in the schema yet.* **DECLINE** `enum value` *No description provided in the schema yet.* # VirtualCardType Source: https://docs.fluz.app/api-reference/types/virtual-card-type VirtualCardType indicates whether the card is multi-use or single-use, defining its usability. **Enum** VirtualCardType indicates whether the card is multi-use or single-use, defining its usability. ## Values **MULTI\_USE** `enum value` MULTI\_USE is a Virtual Card that can be stored and reused. **SINGLE\_USE** `enum value` SINGLE\_USE is a Virtual Card that can be used once. **SINGLE\_LOAD** `enum value` SINGLE\_LOAD is a Virtual Card that can be loaded once, and reused until the card limit is reached. # WithdrawMethods Source: https://docs.fluz.app/api-reference/types/withdraw-methods Enum representing different withdrawal methods available to users. **Enum** Enum representing different withdrawal methods available to users. ## Values **PAYPAL** `enum value` Withdraw to a PayPal account. **BANK\_ACH** `enum value` Withdraw via ACH transfer to a bank account. **BANK\_CARD** `enum value` Withdraw via push-to-card to a debit card. The card must be OCT-eligible (most Visa/Mastercard debit cards). Standard withdrawals settle after a delay; set isExpedited to true for instant delivery. **VENMO** `enum value` Withdraw to a Venmo account. **FLUZPAY** `enum value` Withdraw to a Fluz internal balance. # WithdrawSource Source: https://docs.fluz.app/api-reference/types/withdraw-source Enum representing the source balance type for withdrawals. **Enum** Enum representing the source balance type for withdrawals. ## Values **CASH\_BALANCE** `enum value` Withdraw from the user's cash balance. **REWARDS\_BALANCE** `enum value` Withdraw from the user's rewards balance. # Get help with Fluz Source: https://docs.fluz.app/support Reach the support team, talk to sales, or check live system status — whatever you need when you're stuck. Whether you're troubleshooting an integration, evaluating Fluz for your business, or checking whether something's down, here's where to go. > **[Contact support](https://fluz.app/us/contact-us)** > > Get help from the Fluz team with your account, integration, or a live issue. > **[Contact sales](https://fluz.app/christopheradams/)** > > Talk through pricing, volume, and the right plan for your use case. > **[Status checker](https://you-up.fluz.app/)** > > Check real-time uptime and recent incidents for the Fluz API and services.