Skip to content
In this section

Virtual Accounts

Inspect dashboard virtual accounts and understand the separate virtual-account issuance preview.

A virtual account is an issued IBAN that lets customers pay a seller by bank transfer without exposing the seller's primary bank account.

Caution

The virtualAccounts and virtualAccount queries are not exposed on /dashboard/graphql. They exist on the Fynex staff surface only, backed by the banking-provider integration; a dashboard_session calling them receives Cannot query field "virtualAccounts" on type "Query". They are documented below for reference — ask your Fynex representative for account listings until a seller-facing read ships. The REST issuance preview described below creates a different account record and is not returned by those queries either.


When to use virtual accounts

  • Collect bank-transfer payments without exposing the seller's primary IBAN.
  • Isolate funds per customer — issue one virtual account per customer and reconcile inflows by IBAN.
  • Reconcile provider account activity using the provider-specific account surface enabled for the seller.

Provider boundaries

  • The GraphQL operations below list and inspect banking-provider-backed virtual accounts.
  • POST /api/v1/accounts/virtual is a tier-gated issuance preview. It currently returns only an internal Fynex UUID.
  • There is no seller-facing read/status endpoint for issued accounts in this API version, and the GraphQL operations below cannot resolve the UUID returned by REST issuance.
  • Provider settlement events update the issued account/payment records, but they do not currently create seller wallet-ledger entries. Do not use wallet polling as confirmation of an incoming transfer.

Use the issuance preview only as part of a coordinated rollout with Fynex. Do not present its returned UUID as an IBAN or build automated account-status or incoming-funds reconciliation until the corresponding seller read and wallet-ledger surfaces are released.


GraphQL operations

Caution

Staff surface only — not callable with a dashboard_session. Shown for reference.

Operation Signature
List virtual accounts virtualAccounts(first: Int, after: String, type: VirtualAccountType): VirtualAccountConnection!
Get a single account virtualAccount(id: ID!): VirtualAccount

VirtualAccountType enum

Value Description
CLIENT Account issued for a specific customer / payer
OPERATING Account used for the seller's own operating funds

Type reference

VirtualAccount

Field Type Description
id ID! Internal Fynex account ID
accountId String! The issued IBAN or account number to share with the payer
virtualAccountId String! Banking partner's own reference for this account
currency Currency! Account currency
type VirtualAccountType! CLIENT or OPERATING
status String! Current provisioning status
balance Float! Current balance (in major currency units)
createdAt String! ISO timestamp of account creation
customerInfo VirtualAccountCustomer Optional customer metadata (name, email, phone, address)

VirtualAccountConnection

Cursor-based pagination is used for virtualAccounts:

Field Type Description
edges [VirtualAccountEdge!]! Paginated account nodes
pageInfo PageInfo! Cursor and page metadata

PageInfo fields: hasNextPage, hasPreviousPage, startCursor, endCursor, totalCount, currentPage, totalPages.

Advance pages by passing the endCursor value as the after argument on the next call.


Requesting a virtual account

Caution

Tier-gated preview. POST /api/v1/accounts/virtual may not be enabled for all seller tiers. It does not yet provide a public status/read flow. Confirm the rollout and reconciliation process with Fynex before using it.

POST /api/v1/accounts/virtual
Content-Type: application/json
Authorization: Bearer <seller-api-token>
Idempotency-Key: va_customer-jane_2026-08-08

{
	"name": "Jane Smith"
}

The seller is derived from the bearer token; the request does not accept a merchant or seller ID. A successful request returns 201 Created:

{
	"accountId": "0f834fa8-f5ab-4d80-96b1-fc2d9e4e1824"
}

accountId is the internal Fynex UUID for the issuance record, not the issued IBAN. Retrying the same request with the same Idempotency-Key returns the same UUID without issuing another account. Reusing that key with a different name is rejected. Keep the key stable until the request has a definitive response. The dashboard GraphQL queries continue to use the dashboard cookie, but they do not expose this issuance record; REST issuance uses seller bearer authentication.


Code samples — list virtual accounts

curl

# Step 1 — login
curl -sc cookies.txt \
  -X POST https://api.fynex.ai/api/v1/login/dashboard \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your_password"}'

# Step 2 — list virtual accounts (first 20, CLIENT type)
curl -b cookies.txt \
  -X POST https://api.fynex.ai/dashboard/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "# Not available on /dashboard/graphql — staff surface only\nquery ListVAs($first: Int, $after: String, $type: VirtualAccountType) { virtualAccounts(first: $first, after: $after, type: $type) { pageInfo { hasNextPage endCursor totalCount } edges { cursor node { id accountId virtualAccountId currency type status balance createdAt customerInfo { name email } } } } }",
    "variables": { "first": 20, "type": "CLIENT" }
  }'

JavaScript

const BASE = 'https://api.fynex.ai';

async function login(email, password) {
  const res = await fetch(`${BASE}/api/v1/login/dashboard`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error(`Login failed: ${res.status}`);
}

async function gql(query, variables = {}) {
  const res = await fetch(`${BASE}/dashboard/graphql`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  const { data, errors } = await res.json();
  if (errors?.length) throw new Error(errors[0].message);
  return data;
}

await login('you@example.com', 'your_password');

// Fetch all CLIENT-type virtual accounts using cursor pagination
let after = undefined;
let allAccounts = [];

do {
  const { virtualAccounts } = await gql(
    `# Not available on /dashboard/graphql — staff surface only
     query ListVAs($first: Int, $after: String, $type: VirtualAccountType) {
       virtualAccounts(first: $first, after: $after, type: $type) {
         pageInfo {
           hasNextPage
           endCursor
           totalCount
         }
         edges {
           node {
             id
             accountId
             virtualAccountId
             currency
             type
             status
             balance
             createdAt
             customerInfo {
               name
               email
             }
           }
         }
       }
     }`,
    { first: 20, after, type: 'CLIENT' }
  );

  allAccounts = allAccounts.concat(virtualAccounts.edges.map((e) => e.node));
  after = virtualAccounts.pageInfo.hasNextPage ? virtualAccounts.pageInfo.endCursor : undefined;
} while (after);

console.log(`Fetched ${allAccounts.length} virtual accounts`);
for (const va of allAccounts) {
  console.log(`${va.accountId} (${va.currency}) — status: ${va.status}, balance: ${va.balance}`);
}

Python

import requests

BASE = "https://api.fynex.ai"
session = requests.Session()

session.post(
    f"{BASE}/api/v1/login/dashboard",
    json={"email": "you@example.com", "password": "your_password"},
).raise_for_status()

query = """
  # Not available on /dashboard/graphql — staff surface only
  query ListVAs($first: Int, $after: String, $type: VirtualAccountType) {
    virtualAccounts(first: $first, after: $after, type: $type) {
      pageInfo {
        hasNextPage
        endCursor
        totalCount
      }
      edges {
        node {
          id
          accountId
          virtualAccountId
          currency
          type
          status
          balance
          createdAt
          customerInfo {
            name
            email
          }
        }
      }
    }
  }
"""

all_accounts = []
after = None

while True:
    variables = {"first": 20, "type": "CLIENT"}
    if after:
        variables["after"] = after

    resp = session.post(
        f"{BASE}/dashboard/graphql",
        json={"query": query, "variables": variables},
    )
    resp.raise_for_status()
    result = resp.json()
    if "errors" in result:
        raise RuntimeError(result["errors"][0]["message"])

    page = result["data"]["virtualAccounts"]
    all_accounts.extend(edge["node"] for edge in page["edges"])

    if page["pageInfo"]["hasNextPage"]:
        after = page["pageInfo"]["endCursor"]
    else:
        break

print(f"Total virtual accounts: {len(all_accounts)}")
for va in all_accounts:
    print(f"{va['accountId']} ({va['currency']}) — {va['status']}, balance: {va['balance']}")

See also

  • GraphQL Authentication — Obtain the dashboard_session cookie required for all GraphQL calls.
  • Wallets — Inspect wallet-ledger activity for products that post funds to the seller wallet. The issuance preview does not currently create those entries.
  • Concepts — The object model behind virtual accounts: sellers, payees, wallets and how funds move between them.