A Introduction#

FRAGMENT is a toolkit for building products that move and track money. It includes an API and Dashboard for designing, implementing, and operating your Ledger.

Don't have a FRAGMENT workspace? Get access.

B Design your Ledger#

A Ledger uses a Schema to define functionality for a specific product and use case. A Schema may be shared across multiple Ledgers. Updating a Schema will trigger migrations to update each Ledger. Use the Ledger designer in the Dashboard to model and store your Schema.

Ledgers track money using:

  • Ledger Accounts, balances that represent the financial state of a business
  • Ledger Entries, financial events that update Ledger Accounts

a. Ledger Accounts

#

A Ledger Account has a balance. Changes to a Ledger Account's balance are called Ledger Lines.

There are four types of Ledger Accounts, split into two layers:

State

Assets: what you own
Liabilities: what you owe

Change

Income: what you've earned
Expense: what you've spent

State Ledger Accounts track your product's financial relationships with your bank, payment systems and users. Balance Sheets are a report of State Ledger Account balances.

Change Ledger Accounts track when and how your product makes a profit or loss. They produce Income Statements.

Within a Schema, the chartOfAccounts key contains a nested tree of Ledger Accounts, up to a maximum depth of 10:

Ledger Accounts that share a parent require a unique key, but the same key can be used in different parts of the tree.

For some Ledger Accounts, you must set additional properties:

  • linkedAccount enables reconciliation with an external system
  • template allows multiple instances to be created on demand
  • currencyMode configures the account's currency mode, single or multi
  • consistencyConfig configures the whether the Ledger Account's balances and Ledger Lines are strongly or eventually consistent

b. Ledger Entries

#

A Ledger Entry is a single update to a Ledger. Define a Ledger Entry type in your Schema for every financial event in your product and bank.

A Ledger Entry must be balanced, which means it follows the Accounting Equation:


Assets - Liabilities = Income - Expense

How you balance a Ledger Entry depends upon its net effect to the Ledger's balances.

When the net change to the State Ledger Accounts is zero, the financial event being recorded did not change the net worth of the business. In this example, an increase to an asset account is balanced by an increase in a liability account:

When the net change to the State Ledger Accounts is non-zero, the financial event being recorded made a profit or loss. In this example, a difference in asset and liability accounts is balanced by an increase in an income account.

C Deploy your Ledger#

Once you've designed your Ledger, you can deploy it using the dashboard, the API, or by embedding our CLI in your CI.

a. Dashboard

#

You can edit and store your Schema and create Ledgers directly from the FRAGMENT Dashboard. This is useful during development, but is not recommended for production workflows.

b. API

#

You can call the API to store your Schema and create Ledgers. This is useful if you want to automate your Ledger deployment or have multiple environments which you want to keep in-sync. If you are creating many Schemas and Ledgers, you can also call the storeSchema and createLedger APIs directly from your product.

Call storeSchema to store a Schema. Depending on your use case, you may share one Schema for all your users, or create a Schema per user.

storeSchema query
mutation QuickstartStoreSchema($schema: SchemaInput!) {
  storeSchema(schema: $schema) {
    ... on StoreSchemaResult {
      schema {
        key
        name
        version {
          version
          created
          json
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
storeSchema variables
{
  "schema": {
    "key": "quickstart-schema",
    "name": "Quickstart Schema",
    "chartOfAccounts": {
      "defaultCurrency": {
        "code": "USD"
      },
      "defaultCurrencyMode": "single",
      "accounts": [
        {
          "key": "assets",
          "type": "asset",
          "children": [
            {
              "key": "banks",
              "children": [
                {
                  "key": "user-cash"
                }
              ]
            }
          ]
        },
        {
          "key": "liabilities",
          "type": "liability",
          "children": [
            {
              "key": "users",
              "template": true,
              "consistencyConfig": {
                "ownBalanceUpdates": "strong"
              },
              "children": [
                {
                  "key": "available"
                },
                {
                  "key": "pending"
                }
              ]
            }
          ]
        },
        {
          "key": "income",
          "type": "income"
        },
        {
          "key": "expense",
          "type": "expense",
          "children": []
        }
      ]
    },
    "ledgerEntries": {
      "types": [
        {
          "type": "user_funds_account",
          "description": "Funding {{user_id}} for {{funding_amount}}.",
          "lines": [
            {
              "account": { "path": "assets/banks/user-cash" },
              "key": "funds_arrive_in_bank",
              "amount": "{{funding_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "key": "increase_user_balance",
              "amount": "{{funding_amount}}"
            }
          ]
        },
        {
          "type": "p2p_transfer",
          "description": "P2P of {{transfer_amount}} from {{from_user_id}} to {{to_user_id}}.",
          "lines": [
            {
              "account": {
                "path": "liabilities/users:{{from_user_id}}/available"
              },
              "key": "decrease_from_user",
              "amount": "-{{transfer_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{to_user_id}}/available" },
              "key": "increase_to_user",
              "amount": "{{transfer_amount}}"
            }
          ],
          "conditions": [
            {
              "account": {
                "path": "liabilities/users:{{from_user_id}}/available"
              },
              "postcondition": {
                "ownBalance": {
                  "gte": "0"
                }
              }
            }
          ]
        }
      ]
    }
  }
}
 

Create a Ledger using the createLedger mutation.

createLedger
mutation QuickstartCreateLedger(
  $ik: SafeString!
  $ledger: CreateLedgerInput!
  $schema: SchemaMatchInput
) {
  createLedger(
    ik: $ik,
    ledger: $ledger,
    schema:$schema
  ) {
    ... on CreateLedgerResult {
      ledger {
        ik
        name
        created
        schema {
          key
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}

The schema.key field is set to the key from the storeSchema API call.

createLedger variables
{
  "ik": "quickstart-ledger",
  "ledger": {
    "name": "Quickstart Ledger"
  },
  "schema": {
    "key": "quickstart-schema"
  }
}

c. CLI

#

The FRAGMENT CLI can be installed in your CI and used to store your Schema.

Here's an example of how you can use the CLI in a Github Action workflow:

Fragment CLI in Github Actions
- name: Set up Homebrew
  id: set-up-homebrew
  uses: Homebrew/actions/setup-homebrew@master
- name: Install Fragment CLI
  run: |
    brew tap fragment-dev/tap
    brew install fragment-dev/tap/fragment-cli
    echo "Fragment CLI installed"
- name: Authenticate with Fragment
  run: |
    fragment login \
      --client-id ${{ vars.FRAGMENT_CLIENT_ID }} \
      --client-secret ${{ vars.FRAGMENT_CLIENT_SECRET }} \
      --api-url ${{ vars.FRAGMENT_API_URL }} \
      --oauth-url ${{ vars.FRAGMENT_OAUTH_URL }} \
      --oauth-scope ${{ vars.FRAGMENT_OAUTH_SCOPE }}
- name: Store Schema
  run: |
    fragment store-schema --path my-schema.jsonc
 
D Integrate the API#

This section covers how to build a reliable and fault tolerant integration with FRAGMENT.

To integrate the API, you need to:

  1. Select the AWS region for your workspace.
  2. Generate an SDK based off your GraphQL queries and mutations.
  3. Authenticate your API client.
  4. Call the API with an idempotency key.
  5. Handle errors from the API.
  6. Decide whether to call the API synchronously or asynchronously.

a. Select region

#

FRAGMENT has a GraphQL API to read and write data from your Ledger. The API is available in the following AWS regions:

  • us-east-1
  • us-east-2
  • us-west-2
  • eu-west-1

You can find the region for your workspace in the API URL in the settings tab of the dashboard. If you need to create a new workspace, you can do so through the top-left dropdown in the dashboard.

If you don't see your desired AWS region, contact us.

b. Generate an SDK

#

To ensure you always make valid queries, generate a typed SDK for your programming language. Do so using an SDK codegen tool and the current version of the FRAGMENT GraphQL schema.

FRAGMENT hosts the latest GraphQL schema at:

https://api.fragment.dev/schema.graphql

Use this URL as the SDK codegen input. All changes to the GraphQL API are backwards-compatible. You only need to generate a new version of the SDK to use new features.

Typescript#

graphql-codegen is a tool for generating Typescript SDKs:

  1. Add necessary dependencies to your package.json:
    • @graphql-codegen/typescript-graphql-request
    • @graphql-codegen/cli
    • graphql-request
    • graphql-tag
yarn add @graphql-codegen/typescript-graphql-request \
  @graphql-codegen/cli \
  graphql-request \
  graphql-tag
  1. Configure a codegen.yml file. A starter configuration is provided below.
  2. Define the GraphQL queries that your app uses in the queries folder.
  3. Generate the SDK by calling yarn graphql-codegen.

An example codegen.yml:

codegen.yml

schema: "https://api.fragment.dev/schema.graphql"
generates:
  fragment-sdk.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-graphql-request
      - add:
          content: "/* This file was auto-generated. Don't edit it directly. See 'codegen.yml' */"
    documents: './queries/**/*.ts'
    config:
      scalars:
        Int96: string

As an example, to generate a typed call to addLedgerEntry, define your query in ./queries/addLedgerEntry.ts as:

addLedgerEntry.ts

import gql from 'graphql-tag';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const QUERY = gql`
  mutation addLedgerEntry(
    $entry: LedgerEntryInput!,
    $ik: SafeString!
  ) {
    addLedgerEntry(ik: $ik, entry: $entry) {
      __typename
      ... on AddLedgerEntryResult {
        entry {
          id
          posted
          created
          date
          description
        }
        lines {
          id
          date
          posted
          created
          amount
          type
          ledgerId
          accountId
          ledgerEntryId
        }
        isIkReplay
      }
      ... on Error {
        code
        message
      }
    }
  }
`;

c. Authenticate

#

FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The call requesting an access token follows the OAuth2 spec. You can use any OAuth2 library that supports the client credentials grant to retrieve the token or make an HTTP request. The flow is:

  1. Create an API client in the FRAGMENT dashboard. All API Clients have full access to your workspace.
  2. Note the auth endpoint URL and OAuth scope for your API Client. This is specific to the AWS region of your workspace.
  3. Get a fresh access token from the token endpoint. The access token expires in 1 hour. We recommend retrieving a new token for each set of calls you make. You can generate and use multiple tokens at the same time.
  4. Use that token in an Authorization header with the value Bearer {{access_token}} when calling the GraphQL API.

Your token request payload should be in x-www-form-urlencoded format, with the keys grant_type, scope and client_id.

Token request body
grant_type=client_credentials&scope={{scope}}&client_id={{client_id}}

Your token request headers should contain the following, where client_credentials is the Base64-encoded version of: {{client_id}}:{client_secret}}.

Token request headers
{
    "Content-Type": "application/x-www-form-urlencoded",
    "Authorization": "Basic {{client_credentials}}",
    "Accept": "*/*"
}

The response is a JSON object containing your access token.

Token response body
{
    "access_token": "<access token>",
    "expires_in": 3600,
    "token_type": "Bearer"
}

Putting it all together:

Javascript
import axios from "axios";

const btoa = (str) => Buffer.from(str).toString("base64");

// credentials from the dashboard
const clientId = "2h4t0cv7qv5c9r0qgs1o3erkuk";
const secret = "superSecretClientSecret";
const scope = "https://api.us-west-2.fragment.dev/*";
const authUrl = "https://auth.us-west-2.fragment.dev/oauth2/token";

const getToken = async () => {

  // encode the client id and secret
  const auth = btoa(`${clientId}:${secret}`);

  // create the request body
  const data = new URLSearchParams();
  data.append("grant_type", "client_credentials");
  data.append("scope", scope);
  data.append("client_id", clientId);

  // retrieve the token
  const response = await axios.request({
    method: "POST",
    url: authUrl,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${auth}`,
      Accept: "*/*",
    },
    data,
  });

  if (!response.data.access_token) {
    throw new Error(
      "didn't get an access token from auth endpoint"
    );
  }
  return response.data.access_token;
};
 

d. Use idempotency

#

To ensure write operations are executed exactly once, all write mutations in FRAGMENT are idempotent. This lets you safely retry operations without risk of duplicate effects, so your application only needs to guarantee that it calls the API at least one time.

Mutations require you to provide a unique idempotency key (ik). For calls to reconcileTx, syncCustomAccounts and syncCustomTxs, FRAGMENT internally uses the transaction or account ID from the request as the idempotency key.

If you call the API more than once with the same ik and the same values, it executes only once. FRAGMENT ignores additional requests and returns the original response with isikReplay: true in the response.

Idempotency keys are scoped per-Ledger; if you send multiple requests with the same IK to different Ledgers, all mutations execute and return isIkReplay: false.

e. Error handling

#

According to the official GraphQL spec, all requests will return an object containing:

  • data: the result of your query or mutation. In the Fragment API, all successful mutations return a union type that represents an application error or a successful result. For example, the response type of the addLedgerEntry mutation is:

    API Response Union Types
    union AddLedgerEntryResponse =
      AddLedgerEntryResult | BadRequestError | InternalError

    Queries can return null if an error was raised during the request. This is accompanied by a non-empty errors list.

  • errors: a list of errors raised during the request. This typically gets populated if you query for an entity with an invalid ID or don't provide a required argument. When calling the API, your code should handle errors in this order:

  1. Handle retryable HTTP errors. These are 429 or 5XX.
  2. Handle non-retryable HTTP errors such as other 4XX status codes. This typically happens if your query does not conform to the GraphQL spec, like omitting a required variable or selecting a nonexistent field. This class of error can be avoided if you use a generated, typed SDK.
  3. Handle null responses from queries by checking the errors key. This typically happens if you query an item with an invalid ID.
  4. Query the __typename field. This contains BadRequestError, InternalError or your MutationResult type.
  5. Handle InternalError with retries and exponential backoff.
  6. Handle BadRequestError by failing the operation.
  7. Handle <Mutation>Result types such as AddLedgerEntryResult.

Here is an example of this written in Typescript:

Handling API Responses
import assert from 'assert';

type RequestParams = {
  query: string;
  variables: Record<string, unknown>;
};

const makeRequest = async ({ query, variables }: RequestParams): Promise<Response> => {
  const token = await getToken();
  return fetch('Fragment GraphQL URL', {
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });
};

const query = '''
  mutation addLedgerEntry($ik: SafeString, $entry: LedgerEntryInput!) {
    addLedgerEntry(ik: $ik, entry: $entry) {
      __typename
      ...
    }
  }
''';
const variables = { ik: 'sample-ik', entry: {...} };

const handleRequest = (req: Response) => {
  if (req.status === 429 || req.status >= 500) {
    // Rate-limited or intermittent http failures. Retry the request.
    return handleRequest(await makeRequest({ query, variables }));
  }
  if ((req.status >= 400 || req.status < 500) && req.status !== 429) {
    // Invalid GraphQL request provided to Fragment. Handle the error.
    throw new Error('Invalid GraphQL request');
  }

  // .json() checks that it was a 200
  const response = await req.json();
  if (response.data.addLedgerEntry.__typename === 'InternalError') {
    // Retry the request in case of internal errors, with backoff.
    return handleRequest(await makeRequest({ query, variables }));
  }
  if (response.data.addLedgerEntry.__typename === 'BadRequestError') {
    // Invalid request provided to Fragment. Handle the error.
    throw new Error('Invalid API request to Fragment');
  }
  return response;
};

const response = handleRequest(await makeRequest({ query, variables }));
// Entry successfully posted to Fragment. Handle the response.
assert(response.data.addLedgerEntry.__typename === 'AddLedgerEntryResult');
handlePostedEntry(response.data.addLedgerEntry);
 

f. Sync or async

#

You can call FRAGMENT to update your Ledger with your product both synchronously and asynchronously.

To show users balances in real time, call the API synchronously. This ensures product flows block until the Ledger is updated; your users won't need to refresh to see updated balances. Sync updates can be both strongly or eventually consistent, depending on the use case. To make updates fault tolerant, implement your API calls with retries and exponential backoff.

For updates that don't need to be shown in real time, you can call the API asynchronously. This ensures your product flows don't block user activity while the balances are updated in your Ledger. Async updates are eventually consistent. A good easy pattern here is to have a table in your database that represents a call to FRAGMENT. Update this table transactionally from your product, then use an async queue that calls the API at least once for each transaction.

E Post Ledger Entries#

Posting a Ledger Entry to your Ledger is a two-step process:

  1. Define the structure of the Ledger Entry in your Schema.
  2. Post the Ledger Entry using the API.

a. Schema

#

Ledger Entries are defined in your Schema under the ledgerEntries.types key. Every Ledger Entry has a type that must be unique within the Schema.

Ledger Entry definition
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Fund {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "account": { 
              "path": "assets/banks/user-cash"
            },
            "key": "funds_arrive_in_bank",
            "amount": "{{funding_amount}}"
          },
          {
            "account": { 
              "path": "liabilities/users:{{user_id}}/available"
            },
            "key": "increase_user_balance",
            "amount": "{{funding_amount}}"
          }
        ]
      }
    ]
  }
}

The amounts of a Ledger Line can be parameterized using {{handlebar}} syntax and can contain basic arithmetic (+ or -):

Ledger Entry definition
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_with_fee",
        "description": "Fund {{user_id}} for {{funding_amount}} with {{fee_amount}} fee",
        "lines": [
          {
            "account": { 
              "path": "assets/banks/user-cash"
            },
            "key": "funds_arrive_in_bank",
            "amount": "{{funding_amount}}"
          },
          {
            "account": { 
              "path": "liabilities/users:{{user_id}}/available"
            },
            "key": "increase_user_balance",
            "amount": "{{funding_amount}} - {{fee_amount}}"
          },
          {
            "account": { "path": "income/funding-fees" },
            "key": "take_fee",
            "amount": "{{fee_amount}}"
          }
        ]
      }
    ]
  }
}

Ledger Entries must be balanced by the Accounting Equation. If they are not, the Ledger designer throws an error.

b. Post to the API

#

Call the addLedgerEntry mutation to post a Ledger Entry:

addLedgerEntry mutation
mutation AddLedgerEntry(
  $ik: SafeString!
  $entry: LedgerEntryInput!
) {
  addLedgerEntry(
    ik: $ik,
    entry: $entry
  ) {
    __typename
    ... on AddLedgerEntryResult {
      entry {
        type
        created
        posted
      }
      lines {
        amount
        key
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 

Set the Ledger Entry's type and the required parameters.

addLedgerEntry variables
{
  "ik": "add-ledger-entry",
  "entry": {
    "ledger": {
      "ik": "ledger-ik"
    },
    "type": "user_funds_account",
    "posted": "1234-01-01T01:01:01",
    "parameters": {
      "user_id": "testing-user",
      "funding_amount": "200"
    }
  }
}
Minor Units#

All numbers in FRAGMENT are integers representing the smallest unit, encoded as strings. For example, USD $2.50 is provided as "250".

Tags#

You can specify tags on a Ledger Entry to store arbitrary key-value pairs. See Store metadata.

Idempotency#

To ensure a Ledger Entry is only posted once, provide an Idempotency Key ik to the addLedgerEntry mutation. This identifies the Ledger Entry and lets you safely retry the API call. See Integrate the API.

Timestamps#

Ledger Entries have two timestamps:

  • posted, the time the money movement event happened. You provide this to the addLedgerEntry API call.
  • created, the time at which the Ledger Entry was posted to the API. FRAGMENT auto-generates this value.

You can post entries with a posted timestamp in the past or future. Posting a Ledger Entry updates all balances from the posted time into the future.

c. Linked Accounts

#

Linked Ledger Accounts are used for reconciling your Ledger with your external financial systems.

To define a Ledger Entry to a Linked Ledger Account, you must specify the tx of the Ledger Line. This is the ID of the transaction at the external system.

Linked Ledger Account Entry
{
  "key": "linked-ledger-account-schema",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "reconciliation-type",
        "lines": [
          {
            "key": "reconciliation-line-key",
            "account": { "path": "bank" },
            "amount": "{{some_amount}}",
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          },
          {...other line}
        ]
      }
    ]
  }
}

To post Ledger Entries to a Linked Ledger Account, call reconcileTx instead of addLedgerEntry:

reconcileTx query
mutation ReconcileTx(
    $entry: LedgerEntryInput!
  ) {
    reconcileTx(entry: $entry) {
      __typename
      ... on ReconcileTxResult {
        entry {
          ik
          posted
          created
        }
        isIkReplay
        lines {
          amount
          currency {
            code
          }
        }
      }
      ... on Error {
        code
        message
      }
    }
  }
 

The query variables are the same as addLedgerEntry, except posted and ik are omitted. They are inferred from the external Tx.

reconcileTx variables
{
  "entry": {
    "type": "reconciliation-type",
    "ledger": {
      "ik": "ledger-ik"
    },
    "parameters": {
      "bank_transaction_id": "tx_1234"
    }
  }
}

Read more about creating and using Linked Ledger Accounts in Reconcile transactions.

d. Currencies

#

If a Ledger Account is in currencyMode: multi, you must specify the currency of the Ledger Lines posted to it.

Ledger Entry definition
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Funding {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "account": { "path": "assets/banks/user-cash" },
            "key": "funds_arrive_in_bank",
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "USD"
            }
          },
          {
            "account": { "path": "liabilities/users:{{user_id}}/available" },
            "key": "increase_user_balance",
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "USD"
            }
          }
        ]
      }
    ]
  }
}

Read more about how to implement a product that handles multiple currencies in Handle currencies.

e. Using conditions

#

Ledger Entry conditions are rules defined in your Schema used to manage concurrency and enforce correctness within your Ledger. If a condition is not met, the Ledger Entry is not posted and the mutation throws a BadRequestError.

Conditions are defined in the Schema:

Ledger Entry Template with conditions
{
  "type": "user_withdraws_funds",
  "description": "{{user_id}} withdraws for {{withdraw_amount}}",
  "lines": [
    {
      "account": { "path": "assets/banks/user-cash" },
      "key": "funds_leave_bank",
      "amount": "-{{withdraw_amount}}"
    },
    {
      "account": { "path": "liabilities/users:{{user_id}}/available" },
      "key": "decrease_user_balance",
      "amount": "-{{withdraw_amount}}"
    }
  ],
  "conditions": [
    {
      "account": {
        "path": "liabilities/users:{{user_id}}/available"
      },
      "postcondition": {
        "ownBalance": {
          "gte": "0"
        }
      }
    }
  ]
}

Read more about using Ledger Entry Conditions in Configure consistency.

f. Using Groups

#

Ledger Entry Groups provide a way to tie together related Ledger Entries. You can configure them on a Ledger Entry Type in the Schema.

Ledger Entry Type with Groups
{
  "type": "user_initiates_withdrawal",
  "description": "{{user_id}} initiates withdrawal",
  "lines": [
    {
      "account": {
        "path": "liabilities/users:{{user_id}}/available"
      },
      "key": "decrease_user_balance",
      "amount": "-{{withdraw_amount}}"
    },
    {...other line}
  ],
  "groups": [
    {
      "key": "withdrawal",
      "value": "{{withdrawal_id}}"
    }
  ]
}

Read more about using Ledger Entry Groups in Group Ledger Entries.

g. Runtime Entries

#

You can define a Ledger Entry whose Ledger Lines are defined at runtime.

This can be useful if you're building a product where your end user, not you, defines the Ledger Entry structure. This is common for FRAGMENT users offering accounting services to their users.

To support runtime-defined Ledger Entries, omit the lines field in the Schema.

Runtime-defined Ledger Entry

{
  "type": "runtime_entry",
  "description": "Runtime-defined ledger entry"
}

Then, set the lines field when posting the Ledger Entry using addLedgerEntry or reconcileTx:

AddRuntimeLedgerEntry query
mutation AddRuntimeLedgerEntry(
  $ik: SafeString!
  $entry: LedgerEntryInput!
) {
  addLedgerEntry(
    ik: $ik,
    entry: $entry
  ) {
    ... on AddLedgerEntryResult {
      entry {
        type
        created
        posted
      }
      lines {
        amount
        key
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 
AddRuntimeLedgerEntry variables
{
  "ik": "add-arbitrary-ledger-entry",
  "entry": {
    "type": "runtime_entry",
    "lines": [
        {
          "account": { "path": "assets/banks/user-cash" },
          "key": "funds_arrive_in_bank",
          "amount": "100"
        },
        {
          "account": { "path": "liabilities/users:test-user/available" },
          "key": "increase_user_balance",
          "amount": "100"
        }

    ]
  }
}
F Reconcile Transactions#

Your Ledger should accurately track the real-world movement of money within your product. This is done by reconciling transactions from your external financial systems with your Ledger.

The overall workflow is a three-step process:

  1. Transact at your external financial system.
  2. Sync the real-world money movement to FRAGMENT.
  3. Reconcile the transaction to a Ledger Entry using the API.

This whole process happens through a FRAGMENT Link.

d. Linked Accounts

#

After you set up the Link, create Linked Ledger Accounts, accounts in your Ledger that map 1:1 with accounts at your external financial system.

To define a Linked Ledger Account, set linkedAccount on a Ledger Account in your Schema:

Linked Ledger Account with hardcoded IDs
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "children": [{
          "key": "operating",
          "name": "Operating Bank",
          "linkedAccount": {
            "linkId": "some-link-id",
            "externalId": "bank-account-1"
          }
        }]
      }
    ]
  }
}

The linkId comes from the Link you create in the dashboard. The externalId is the ID of the account at your external financial system.

The CLI automatically replaces variables with ${ENV_VAR} syntax when running fragment store-schema. This lets you use different External Accounts per environment:

Linked Ledger Account with env var IDs
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "children": [{
          "key": "operating",
          "name": "Operating Bank",
          "linkedAccount": {
            "linkId": "${BANK_LINK_ID}",
            "externalId": "${BANK_ACCOUNT_ID}"
          }
        }]
      }
    ]
  }
}

To create a Linked Ledger Account that is part of a Ledger Account template, parameterize the linkedAccount field:

Linked Ledger Account with parameter IDs
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "children": [{
          "key": "operating",
          "name": "Operating Bank",
          "template": true,
          "linkedAccount": {
            "linkId": "{{BANK_LINK_ID}}",
            "externalId": "{{BANK_ACCOUNT_ID}}"
          }
        }]
      }
    ]
  }
}

These parameters are then required when posting a Ledger Entry to a Linked Ledger Account. This can be useful if you're creating bank accounts per customer, for example.

e. Transact externally

#

Transactions at your external financial system are initiated in one of two ways:

  • You initiate a payment by calling their API.
  • You are notified by the external system that a transaction has occurred. This can happen via webhook, by polling their API, or receiving a periodic transaction report.

f. Sync transactions

#

Once a transaction occurs, you need to sync it with your FRAGMENT Ledger.

If you're using a Native Link, FRAGMENT automatically syncs transactions. You can skip ahead to Reconcile transactions.

If you're using a Custom Link, your sync process should call syncCustomTxs. Your sync process can periodically enumerate transactions in your external system or be triggered by webhook.

You may also want to sync and reconcile when your product makes a payment. You should only sync transactions that are settled, not pending or declined.

Once you have a set of transactions to sync, call syncCustomTxs:

syncCustomTxs mutation
mutation SyncTransactions(
  $link: LinkMatchInput!
  $txs: [CustomTxInput!]!
) {
  syncCustomTxs(link: $link, txs: $txs) {
    __typename
    ... on SyncCustomTxsResult {
      txs {
        id
        externalId
        amount
        date
        description
      }
    }
    ... on Error {
      code
      message
    }
  }
}
syncCustomTxs variables
{
  "link": { "id": "some-link-id" },
  "txs": [
    {
      "account": {
        "linkId": "some-link-id",
        "id": "bank-account-1"
      },
      "externalId": "tx-1",
      "description": "Processed ACH batch",
      "amount": "-100",
      "posted": "1968-01-01"
    },
    {
      "account": {
        "linkId": "some-link-id",
        "externalId": "bank-account-2"
      },
      "externalId": "tx-2",
      "description": "Received RTP payment",
      "amount": "100",
      "posted": "1968-01-01T16:45:00Z"
    }
  ]
}

You should ensure that externalId is a stable and unique identifier for each transaction, within the scope of its account. This identifier enforces idempotentency. This identifier is typically the ID of the transaction at the external system. Make sure you use the lowest level transaction ID available, not the ID of a higher level construct that may be linked to multiple transactions, like a payment.

Calling syncCustomTxs with a different description for an existing externalId updates the name of the External Account. amount and posted timestamp are immutable.

You can sync transactions from different accounts in the same API call, but they must all belong to the same Custom Link.

g. Reconcile a Tx

#

To reconcile a transaction from an external system, follow the same two-step process as when posting Ledger Entries:

  1. Define the structure of the Ledger Entry in your Schema.
  2. Post the Ledger Entry using the API.

Ledger Entries posting to a Linked Ledger Account must specify the Tx from the External Account tied to the Ledger Account. This lets FRAGMENT know which transaction to reconcile:

LedgerEntrySchema with Linked Ledger Account
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_via_link",
        "description": "Funding {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "account": { "path": "assets/operating" },
            "key": "funds_arrive_in_bank",
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          },
          {
            "account": { "path": "liabilities/users:{{user_id}}/available" },
            "key": "increase_user_balance"
          }
        ]
      }
    ]
  }
}

Notes:

  • Ledger Lines posting to a Linked Ledger Account must match 1:1 with the Tx that's being reconciled.
  • bank_transaction_id represents the ID of the transaction at the external system.

Instead of calling addLedgerEntry, Linked Ledger Accounts use the reconcileTx mutation:

reconcileTx mutation
mutation ReconcileTx(
  $entry: LedgerEntryInput!
) {
  reconcileTx(
    entry: $entry
  ) {
    ... on ReconcileTxResult {
      entry {
        type
        created
        posted
      }
      lines {
        amount
        key
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 

The parameters look similar to addLedgerEntry. Specify the Ledger Entry type that you are using and provide the parameters defined in the Schema:

reconcileTx variables
{
  "entry": {
    "type": "user_funding",
    "ledger": {
      "ik": "ledger-ik"
    },
    "parameters": {
      "txId": "tx_12345",
      "customerId": "customer-1"
    }
  }
}

ik and posted are optional when posting Ledger Entries with reconcileTx:

  • ik: the Tx.externalId is used to ensure that reconciling a transaction is idempotent
  • posted: the timestamp of the Ledger Entry is taken from the Tx to ensure the Ledger mirrors the external system
Reconciling multiple Txs#

Book transfers are a common type of money movement which produce two Txs at your bank as part of one payment.

To reconcile multiple Txs using reconcileTx:

LedgerEntrySchema with multiple Txs
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_via_link",
        "description": "Funding {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "key": "funds_arrive_in_operating_bank",
            "account": { "path": "assets/operating-bank-account" },
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          },
          {
            "key": "funds_leave_holding_bank",
            "account": { "path": "assets/holding-bank-account" },
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          }
        ]
      }
    ]
  }
}

Notes:

  • Reconciling multiple Txs is only permitted if they have the same posted timestamp.
  • You can only reconcile multiple Txs within the same Link.

h. Unreconciled Txs

#

Transactions synced to FRAGMENT but not reconciled to a Ledger are considered unreconciled.

You can query unreconciled transactions in a Linked Ledger Account using the unreconciledTxs field on theLedgerAccount. It is recommended to periodically call this query to ensure your Ledger stays up to date. It can also be used to build reconciliation UIs:

unreconciledTxs query
query GetUnreconciledTxs (
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    id
    unreconciledTxs {
      nodes {
        id
        description
        amount
        externalId
        externalAccountId
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

The variables specify the path of the Linked Ledger Account you are querying and the Ledger it belongs to.

unreconciledTxs variables
{
  "ledgerAccount": {
    "path": "assets/operating",
    "ledger": {...}
  }
}

unreconciledTxs is eventually consistent, so if you've recently transacted at a Native Link or just synced manually for a Custom Link, you may not see the transaction in the query results immediately.

i. Stripe Txs

#

Transactions synced to FRAGMENT within a Stripe Link have special handling. Every Balance Transaction at Stripe has two amount fields:

  1. The gross amount of the transaction
  2. The fee paid for the transaction, to Stripe

This doesn't neatly map 1:1 with Fragment's recon model, so FRAGMENT creates two Txs for each Balance Transaction at Stripe: one for the gross amount and the other for the fee. This allows you to account for these amounts independently in your Ledger. The external IDs for these Txs are:

  1. {{stripe_tx_id}}_gross for the gross amount Tx
  2. {{stripe_tx_id}}_fee for the fee amount Tx

For example, a Stripe Balance Transaction with ID txn_123 will result in two Txs in FRAGMENT with external IDs txn_123_gross and txn_123_fee. You can reconcile both of these in a single reconcileTx call.

G Store metadata#

You can provide up to 10 tags on Ledger Entries to store arbitrary key-value pairs, like IDs from your product.

a. Schema Entry tags

#

You can define tags on Ledger Entry types in your Schema:

Schema-defined tags
{
  "key": "...",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Fund {{user_id}}",
        "lines": [
          {
            "key": "increase_user_balance",
            "account": { 
              "path": "liabilities/users:{{user_id}}/available"
            },
            "amount": "{{funding_amount}}"
          },
          {...other line}
        ],
        "tags": [
          {
            "key": "user",
            "value": "{{user_id}}"
          },
          {
            "key": "deposit_flow",
            "value": "{{deposit_flow_id}}"
          },
          {
            "key": "deposit_flow_type",
            "value": "ach"
          }
        ]
      }
    ]
  }
}

You can use the same parameter for both tag values and account paths.

When posting a Ledger Entry, include tag values as parameters:

addLedgerEntry mutation
mutation AddLedgerEntry(
  $ik: SafeString!
  $entry: LedgerEntryInput!
) {
  addLedgerEntry(
    ik: $ik,
    entry: $entry
  ) {
    __typename
    ... on AddLedgerEntryResult {
      entry {
        type
        ik
        tags {
          key
          value
        }
      }
      lines {
        amount
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 
addLedgerEntry variables with Schema-defined tags
{
  "ik": "fund-abc",
  "entry": {
    "ledger": {
      "ik": "ledger-ik"
    },
    "type": "user_funds_account",
    "parameters": {
      "user_id": "user-1",
      "funding_amount": "200",
      "deposit_flow_id": "deposit-123"
    }
  }
}

The Ledger Entry will have the tags you defined in the Schema:

addLedgerEntry response with tags
{
  "data": {
    "addLedgerEntry": {
      "entry": {
        "type": "user_funds_account",
        "ik": "fund-abc",
        "tags": [
          {
            "key": "user",
            "value": "user-1"
          },
          {
            "key": "deposit_flow",
            "value": "deposit-123"
          },
          {
            "key": "deposit_flow_type",
            "value": "ach"
          }
        ]
      },
      "lines": [...]
    }
  }
}

b. Runtime Entry tags

#

You can define tags at runtime when posting a Ledger Entry:

addLedgerEntry variables with runtime-defined tags
{
  "ik": "add-ledger-entry",
  "entry": {
    "ledger": {
      "ik": "ledger-ik"
    },
    "type": "user_funds_account",
    "parameters": {
      "user_id": "testing-user",
      "funding_amount": "200",
      "deposit_flow_id": "abc"
    },
    "tags": [
      {
        "key": "deposit_flow_type",
        "value": "ach"
      },
      {
        "key": "operator",
        "value": "alice"
      }
    ]
  }
}

If you define tags both at runtime and in the Schema, the Ledger Entry will get the combined set of tags:

addLedgerEntry response with tags
{
  "data": {
    "addLedgerEntry": {
      "entry": {
        "type": "user_funds_account",
        "ik": "fund-abc",
        "tags": [
          {
            "key": "user",
            "value": "user-1"
          },
          {
            "key": "deposit_flow",
            "value": "deposit-123"
          },
          {
            "key": "deposit_flow_type",
            "value": "ach"
          },
          {
            "key": "operator",
            "value": "alice"
          }
        ]
      },
      "lines": [...]
    }
  }
}

You can specify the same tag key in both places only if they have the same value.

c. Updating Entry tags

#

In addition to tags defined in your Schema, you can add and update tags on a posted Ledger Entry.

updateLedgerEntry mutation
mutation UpdateLedgerEntryTags(
  $ledgerEntry: LedgerEntryMatchInput!
  $update: UpdateLedgerEntryInput!
) {
  updateLedgerEntry(
    ledgerEntry: $ledgerEntry,
    update: $update
  ) {
    __typename
    ... on UpdateLedgerEntryResult {
      entry {
        type
        ik
        tags {
          key
          value
        }
        lines {
          nodes {
            amount
            description
            account {
              path
            }
          }
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 
updateLedgerEntry variables to add and update tags
{
  "ledgerEntry": {
    "ik": "add-ledger-entry",
    "ledger": {
      "ik": "ledger-ik"
    }
  },
  "update": {
    "tags": [
      {
        "key": "operator",
        "value": "bob"
      },
      {
        "key": "supervisor",
        "value": "eve"
      }
    ]
  }
}

Notes:

  • You can only update a Ledger Entry a maximum of 10 times.
H Read balances#

FRAGMENT supports querying a Ledger Account for its latest balances, historical balances and balance changes.

a. Latest

#

Use the ledgerAccount query to look up a Ledger Account's balance.

GetBalances query
query GetBalance(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    balance
  }
}
GetBalances variables
{
  "ledgerAccount": {
    "path": "assets/bank/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}

b. Aggregated

#

Ledger Accounts have three balances:

  • ownBalance is the sum of all Ledger Lines posted to the Ledger Account, excluding Ledger Lines in child Ledger Accounts
  • childBalance is the sum of all Ledger Lines posted to the children of this Ledger Account
  • balance is the sum of all Ledger Lines posted to this Ledger Account and its children
GetAggregatedBalances query
query GetAggregatedBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    childBalance
    balance
  }
}
GetAggregatedBalances variables
{
  "ledgerAccount": {
    "path": "liabilities/users"
  }
}

c. Consistent

#

Balance reads are eventually consistent by default. This means that the balance may not reflect all the Ledger Lines in the account.

To read a strongly consistent balance, a Ledger Account must have its balances updated in a strongly consistent manner. This is set in the Schema on a Ledger Account's consistencyConfig:

Strongly consistent Ledger Accounts
{
  "key": "strongly-consistent-ledger-accounts",
  "name": "Strongly consistent Ledger Accounts",
  "chartOfAccounts": {
    "defaultCurrency": { "code": "USD" },
    "accounts": [
      {
        "key": "liabilities",
        "type": "liability",
        "children": [
          {
            "key": "users",
            "template": true,
            "consistencyConfig": {
              "ownBalanceUpdates": "strong"
            },
            "children": [
              {
                "key": "available",
                "consistencyConfig": {
                  "ownBalanceUpdates": "strong"
                }
              },
              {
                "key": "pending"
              },
              {
                "key": "blocked"
              }
            ]
          }
        ]
      }
    ]
  }
}
 

Once a Ledger Account's balance is updated consistently, set the consistencyMode on balance queries to determine the consistency of the read you issue.

Strongly consistent ownBalance read
query GetStronglyConsistentBalance(
    $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalance(consistencyMode: strong)
  }
}

consistencyMode can be set to:

  • strong to perform a strongly consistent balance read
  • eventual to perform an eventually consistent balance read
  • use_account to use the value of consistencyConfig.ownBalanceUpdates when performing a balance read
Strongly consistent ownBalance read variables
{
  "ledgerAccount": {
    "path": "liabilities/users:user-1/available",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}

Only ownBalance can be queried with consistencyMode: strong.

Read the Configure consistency section to learn more about FRAGMENT's consistency semantics and Ledger Account consistency modes.

d. Historical

#

To query the balance of a Ledger Account at a particular point in time use the at argument:

Balance queries
query GetHistoricalBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    end_of_year: balance(at: "1969")
    end_of_month: balance(at: "1969-07")
    end_of_day: balance(at: "1969-07-21")
    end_of_hour: balance(at: "1969-07-21T02")
  }
}

If you don't specify at, you'll get the latest balance. at is supported for all balances and works granularly to the hour.

e. Balance changes

#

You can also query the net change on a Ledger Account over a specific reporting period. This can be useful for generating financial statements.

Similar to balances, there are three types of balance changes:

  • ownBalanceChange, how much ownBalance changed
  • childBalanceChange, how much childBalance changed
  • balanceChange, how much balance changed

Balance change queries require you to specify a period. This can be a year, quarter, month, day or hour.

Balance change queries
query GetBalanceChanges(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalanceChange(period: "1969")
    childBalanceChange(period: "1969")
    balanceChange(period: "1969")
    currentYear: balanceChange(period: "1969")
    lastYear: balanceChange(period: "1968")
    lastYearQ4: balanceChange(period: "1968-Q4")
    lastYearDecember: balanceChange(period: "1968-12")
    lastYearChristmas: balanceChange(period: "1968-12-25")
    lastYearLastHour: balanceChange(period: "1968-12-31T23")
  }
}

You can also perform multiple balance queries using aliases.

Balance change query variables
{
  "ledgerAccount": {
    "path": "liabilities/users:user-1/available",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}

f. Multi-currency mode

#

For Ledger Accounts in currencyMode: multi, use the currency argument to query the balance in a specific currency.

Multi-currency balance queries
query GetMultiCurrencyBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    latestUSDBalance: balance(currency: { code: USD })
    latestGBPBalance: balance(currency: { code: GBP })

    USDBalanceChange:
      balanceChange(period: "1969", currency: { code: USD })
    GBPBalanceChange:
      balanceChange(period: "1969", currency: { code: GBP })
  }
}
Multi-currency balance query variables
{
  "ledgerAccount": {
    "path": "liabilities/users:user-1/available",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}

You can also query balances in all of a multi-currency Ledger Account's currencies, see Handle currencies.

g. Timezone offsets

#

Balance queries respect the Ledger's balanceUTCOffset when specifying periods and times. This field is specified when creating the Ledger.

  • If a Ledger has an offset of -08:00, then querying balance(at: "1969-01-31") returns the balance at midnight PT on that date, or 8am on 1969-02-01 UTC.
  • Querying balanceChange(period: "1969") returns the net change between 8am on 1969-01-01 UTC and 8am on 1969-01-01 UTC. This gives you the net change between midnights local time.
  • Daylight savings is ignored, so every day throughout the year has exactly 24 hours.
I Query data#

You can flexibly query your data to generate financial reports, embed FRAGMENT in your product and build internal dashboards.

a. Basics

#
GraphQL#

FRAGMENT is a GraphQL API, so data in the system is modeled as a graph. Entities in the API are nodes, and their relationships with other entities are edges. Ledgers have Ledger Accounts; Ledger Accounts and Ledger Entries have Ledger Lines; and so on.

FRAGMENT exposes several queries as entry points to the data graph. They return data about a single entity, or a list of entities.

When making a query, you can fetch related entities using expansions. As opposed to a REST interface, where you may require several round-trips to query nested data, expansions are nested queries that you let you retrieve related data in a single request. For example, you can expand from a Ledger Entry to all the Ledger Lines in it in one API call.

Connection types#

FRAGMENT uses connection types to return lists of entities. A connection type is a list of nodes, and a pageInfo object that contains cursors to the next and previous pages of results.

ListLedgerAccounts
query ListLedgerAccounts($ledger: LedgerMatchInput!) {
  ledger(ledger: $ledger) {
    ledgerAccounts {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
Filtering#

You can filter connection types with a filter argument.

For example, you can filter a list of Ledger Accounts by their type:

FilterLedgerAccounts query
query FilterLedgerAccounts(
    $ledger: LedgerMatchInput!,
    $filter: LedgerAccountsFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerAccounts {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
FilterLedgerAccounts variables - by type
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "type": {
      "equalTo": "asset"
    }
  }
}

You can combine filters by adding multiple components to the filter block. Results are AND'd:

FilterLedgerAccounts variables - by type
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "type": {
      "equalTo": "asset",
      "hasParentLedgerAccount": true
    }
  }
}
Pagination#

Fields that return lists support cursor-based pagination:

  • Results are returned under a nodes property as an array. The pageInfo property contains cursors pointing to the next page and previous pages.
  • You can send a cursor to the after (or before) arguments on list fields to retrieve a specific page.
  • The first (or last) argument sets the page size. The default is 20 and the maximum is 200.
  • Once a page size has been set in the initial request, all subsequent pagination requests have to use the same page size.
  • Results are returned in a deterministic order. This is generally reverse chronological, newest first. This sort is by posted date for Ledger Lines and Ledger Entries, or creation date for other entities.

This query uses pagination to retrieve two Ledger Accounts at a time:

GetLedgerAccounts query with pagination
query GetLedgerAccounts(
    $ledgerIk: SafeString!
    $after: String
    $first: Int
    $before: String
  ) {
    ledger(ledger: { ik: $ledgerIk }) {
      ledgerAccounts(
        after: $after
        first: $first
        before: $before
      ) {
        nodes {
          path
          name
          type
        }
        pageInfo {
          hasNextPage
          endCursor
          hasPreviousPage
          startCursor
        }
      }
    }
  }
GetLedgerAccounts variables
{
  "ledgerIk": "ik-used-to-create-ledger",
  "first": 2
}

The response is:

GetLedgerAccounts first page response
{
  "data": {
    "ledger": {
      "ledgerAccounts": {
        "nodes": [
          {
            "path": "assets/test-assets/test:1",
            "name": "Test 1",
            "type": "asset"
          },
          {
            "path": "assets/test-assets/test:2",
            "name": "Test 2",
            "type": "asset"
          }
        ],
        "pageInfo": {
          "hasNextPage": true,
          "endCursor": "<some-end-cursor>",
          "hasPreviousPage": false,
          "startCursor": null
        }
      }
    }
  }
}

To retrieve the next page, send the same query but with the after parameter set on ledgerAccounts:

GetLedgerAccounts next page variables
{
  "ledgerIk": "ik-used-to-create-ledger",
  "after": "<some-end-cursor>"
}

The response is:

GetLedgerAccounts second page response
{
  "data": {
    "ledger": {
      "ledgerAccounts": {
        "nodes": [
          {
            "path": "assets/test-assets/test:3",
            "name": "Test 3",
            "type": "asset"
          },
          {
            "path": "assets/test-assets/test:4",
            "name": "Test 4",
            "type": "asset"
          }
        ],
        "pageInfo": {
          "hasNextPage": false,
          "endCursor": null,
          "hasPreviousPage": true,
          "startCursor": "<some-start-cursor>"
        }
      }
    }
  }
}

To retrieve the previous page of results, send the same query but with the before parameter set on ledgerAccounts. The response is the first page of results.

GetLedgerAccounts previous page variables
{
  "ledgerIk": "ik-used-to-create-ledger",
  "before": "<some-start-cursor>"
}

b. Ledgers

#
Lookup#

Use the ledger query to retrieve a Ledger by the IK used to create it:

GetLedger
query GetLedger($ledger: LedgerMatchInput!) {
  ledger(ledger: $ledger) {
    name
    created
    balanceUTCOffset
    ledgerAccounts {
      nodes {
        name
        type
      }
    }
    schema {
      key
    }
  }
}
GetLedger variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  }
}
List#

Use the ledgers query to list all the Ledgers in your workspace:

ListLedgers
query ListLedgers {
  ledgers {
    nodes {
      name
      created
      balanceUTCOffset
      ledgerAccounts {
        nodes {
          name
          type
        }
      }
      schema {
        key
      }
    }
    pageInfo {
      hasNextPage
      endCursor
      hasPreviousPage
      startCursor
    }
  }
}

The response is a paginated list of Ledgers.

c. Ledger Accounts

#
Lookup#

Use the ledgerAccount query to retrieve a Ledger Account by its path in the Schema:

GetLedgerAccount
query GetLedgerAccount(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    path
    name
    balance
    type
    lines {
      nodes {
        amount
        posted
      }
    }
  }
}

The IK of the Ledger needs to be provided along with the Ledger Account's path:

GetLedgerAccount Variables
{
  "ledgerAccount": {
    "path": "assets/bank/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}
Lookup multiple#

You can also retrieve multiple Ledger Accounts using the ledgerAccounts query and the in filter:

ListLedgerAccounts
query ListLedgerAccounts($ledger: LedgerMatchInput!) {
  ledger(ledger: $ledger) {
    ledgerAccounts {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
GetLedgerAccount Variables with In
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "ledgerAccount": {
      "in": [
        {
          "path": "assets/banks/user-cash"
        },
        {
          "path": "income-root/income-revenue-root"
        }
      ]
    }
  }
}
List#

Use the ledger.ledgerAccounts query to list all Ledger Accounts within a Ledger:

ListLedgerAccounts
query ListLedgerAccounts($ledger: LedgerMatchInput!) {
  ledger(ledger: $ledger) {
    ledgerAccounts {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
ListLedgerAccounts variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  }
}

The response is a paginated list of Ledger Accounts.

List balances#

Since a Ledger Account can have balances in multiple currencies, you can list its balance and balance changes across all currencies:

GetLedgerAccountBalances
query GetLedgerAccountBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    path
    name
    balances {
      nodes {
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
    end_of_year_balances: balances(at: "1969") {
      nodes {
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
    last_year: balanceChanges(period: "1968") {
      nodes {
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
  }
}
GetLedgerAccountBalances Variables
{
  "ledgerAccount": {
    "path": "assets/bank/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}

Read more about handling multi-currency balances in Handle currencies.

Filter by type#

Use the type parameter to filter Ledger Account lists:

FilterLedgerAccounts query
query FilterLedgerAccounts(
    $ledger: LedgerMatchInput!,
    $filter: LedgerAccountsFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerAccounts(filter: $filter) {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}

Use type to filter Ledger Accounts by their type:

FilterLedgerAccounts variables - by type
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "type": {
      "equalTo": "asset"
    }
  }
}

You can also filter for multiple types in one query, using in. This can be useful to Generate reports:

FilterLedgerAccounts variables - type in
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "type": {
      "in": ["asset", "liability"]
    }
  }
}
Filter by path#

Use path and wildcard matching (*) in place of template variables to query all instances of Ledger Accounts with template: true.

FilterLedgerAccounts query
query FilterLedgerAccounts(
    $ledger: LedgerMatchInput!,
    $filter: LedgerAccountsFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerAccounts(filter: $filter) {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
FilterLedgerAccounts variables - by external account
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "path": {
      "matches": "liability-root/user:*/pending"
    }
  }
}

Read more about filtering Ledger Accounts in filtering.

Filter by parent#

Use hasParentLedgerAccount to filter Ledger Accounts by their parent status:

FilterLedgerAccounts query
query FilterLedgerAccounts(
    $ledger: LedgerMatchInput!,
    $filter: LedgerAccountsFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerAccounts(filter: $filter) {
      nodes {
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
FilterLedgerAccounts variables - by external account
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "hasParentLedgerAccount": false
  }
}

Read more about filtering Ledger Accounts in filtering.

d. Ledger Lines

#
Lookup#

Use the ledgerLine query to retrieve a Ledger Line by its ID:

GetLedgerLine
query GetLedgerLine(
  $ledgerLine: LedgerLineMatchInput!
) {
  ledgerLine(ledgerLine: $ledgerLine) {
    amount
    currency {
      code
      customCurrencyId
    }
    account {
      name
      type
    }
  }
}
FilterLedgerAccountLines Variables
{
  "ledgerLine": {
    "id": "<ledger line ID>"
  }
}
List#

Use the ledgerAccount.lines query to list the lines in a Ledger Account:

GetLedgerAccountLines
query GetLedgerAccountLines(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    path
    name
    lines {
      nodes {
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
  }
}
GetLedgerAccountLines Variables
{
  "ledgerAccount": {
    "path": "assets/bank/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}
Filter by posted#

Use posted to filter Ledger Lines by their posted timestamp between any two points in time:

FilterLedgerAccountLines
query GetLedgerAccountLines(
  $ledgerAccount: LedgerAccountMatchInput!,
  $filter: LedgerLinesFilterSet!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    path
    name
    lines(filter: $filter) {
      nodes {
        id
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
  }
}
FilterLedgerAccountLines Variables
{
  "ledgerAccount": {
    "path": "assets/banks/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  },
  "filter": {
    "posted": {
      "after": "1969-07-01T00:00:00.000Z",
      "before": "1969-07-30T23:59:59.999Z"
    }
  }
}

The after and before filters are inclusive, so use timestamps for the first and last moments of the period you're querying for.

Filter by key#

Use key to filter Ledger Lines by their keys in your Schema:

FilterLedgerAccountLines
query GetLedgerAccountLines(
  $ledgerAccount: LedgerAccountMatchInput!,
  $filter: LedgerLinesFilterSet!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    path
    name
    lines(filter: $filter) {
      nodes {
        id
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
  }
}
FilterLedgerAccountLines Variables
{
  "ledgerAccount": {
    "path": "assets/banks/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  },
  "linesFilter": {
    "key": {
      "equalTo": "increase_user_balance"
    }
  }
}

e. Ledger Entries

#
Lookup#

Use the ledgerEntry query to retrieve a Ledger Entry by its IK. You provide the IK when posting entries via addLedgerEntry:

GetLedgerEntry
query GetLedgerEntry(
  $ledgerEntry: LedgerEntryMatchInput!
) {
  ledgerEntry(ledgerEntry: $ledgerEntry) {
    id
    ik
    ledger {
      id
      name
    }
    lines {
      nodes {
        amount
        currency {
          code
          customCurrencyId
        }
      }
    }
  }
}
GetLedgerEntry Variables - by IK
{
  "ledgerEntry": {
    "ik": "<ledger entry IK>"
  }
}

When you Reconcile transactions using reconcileTx, the IK is the Transaction's externalId. Query entry.ik in ReconcileTxResult to retrieve it:

Querying IK in reconcileTx
mutation ReconcileTx(
  $entry: LedgerEntryInput!
) {
  reconcileTx(entry: $entry) {
    ... on ReconcileTxResult {
      entry {
        ik
        type
        created
        posted
      }
      lines {
        amount
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}

A Ledger Entry can also be retrieved using its ID:

GetLedgerEntry Variables - by ID
{
  "ledgerEntry": {
    "id": "<ledger entry ID>"
  }
}
Lookup multiple#

You can also retrieve multiple Ledger Entries using the ledgerEntries query and the in filter:

ListLedgerEntries
query ListLedgerEntries(
  $ledger: LedgerMatchInput!
  $filter: LedgerEntriesFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerEntries(filter: $filter) {
      nodes {
        ik
        type
        posted
        lines {
          nodes {
            amount
            account {
              path
            }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
ListLedgerEntries Variables with In
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "ledgerEntry": {
      "in": [
        {
          "ik": "fund-user-1-account"
        },
        {
          "ik": "fund-user-2-account"
        }
      ]
    }
  }
}
List by group#

You can get a paginated list of Ledger Entries in a given group using the ledgerEntryGroup.ledgerEntries expansion:

GetGroupedLedgerEntries
query GetGroupedLedgerEntries(
  $ledger: LedgerMatchInput!,
  $entryGroup: EntryGroupMatchInput!,
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroup(ledgerEntryGroup: $entryGroup) {
      ledgerEntries {
        nodes {
          ik
          description
          posted
        }
        pageInfo {
          endCursor
          hasNextPage
          hasPreviousPage
          startCursor
        }
      }
    }
  }
}
GetGroupedLedgerEntries Variables
{
  "ledgerEntryGroup": {
    "key": "withdrawal",
    "value": "12345"
  },
  "ledger": {
    "ik": "quickstart-ledger"
  }
}

The response is:

GetGroupedLedgerEntries Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroup": {
        "ledgerEntries": {
          "nodes": [
            {
              "ik": "ledger-entry-2",
              "description":"User user-id withdrawal settled",
              "posted": "1969-06-21T02:56:05.000Z"
            },
            {
              "ik": "ledger-entry-1",
              "description": "User user-id initiated withdrawal for 50000.",
              "posted": "1969-06-16T13:32:00.000Z"
            }
          ],
          "pageInfo": {
            "hasNextPage": false,
            "endCursor": null,
            "hasPreviousPage": false,
            "startCursor": null
          }
        }
      }
    }
  }
}
Expand lines#

Use the ledgerEntry.lines expansion to list the Ledger Lines in a Ledger Entry:

GetLedgerEntryLines
query GetLedgerEntryLines(
  $ledgerEntry: LedgerEntryMatchInput!
) {
  ledgerEntry(ledgerEntry: $ledgerEntry) {
    id
    ik
    lines {
      nodes {
        account {
          path
        }
        amount
        currency {
          code
          customCurrencyId
        }
      }
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
    }
  }
}
GetLedgerEntryLines Variables
{
  "ledgerEntry": {
    "ik": "<ledger entry IK>"
  }
}
Filter by posted#

Similar to Ledger Lines, Ledger Entries can be filtered by their posted timestamp between any two points in time using posted:

FilterLedgerEntries
query FilterLedgerEntries(
  $ledger: LedgerMatchInput!,
  $entriesFilter: LedgerEntriesFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerEntries(filter: $entriesFilter) {
      nodes {
        ik
        type
        posted
        lines {
          nodes {
            amount
            account {
              path
            }
          }
        }
      }
    }
  }
}

Use after and before to filter Ledger Entries by their posted timestamp between any two points in time:

FilterLedgerEntries Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "entriesFilter": {
    "posted": {
      "after": "01-01-1968",
      "before": "01-01-1969"
    }
  }
}

Use date to filter Ledger Entries by the date they were posted on:

FilterLedgerEntries Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "entriesFilter": {
    "date": {
      "equalTo": "12-31-1968"
    }
  }
}
Filter by type#

Use type to filter Ledger Entries by their type defined in your Schema:

FilterLedgerEntries
query FilterLedgerEntries(
  $ledger: LedgerMatchInput!,
  $entriesFilter: LedgerEntriesFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerEntries(filter: $entriesFilter) {
      nodes {
        ik
        type
        posted
        lines {
          nodes {
            amount
            account {
              path
            }
          }
        }
      }
    }
  }
}
FilterLedgerEntries Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "entriesFilter": {
    "type": {
      "in": ["withdrawal", "p2p_transfer"]
    }
  }
}

To retrieve Ledger Entries of multiple types, use the in operator:

FilterLedgerEntries Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "entriesFilter": {
    "type": {
      "in": ["withdrawal", "p2p_transfer"]
    }
  }
}
Filter by tag#

Use tag to filter Ledger Entries by tags:

FilterLedgerEntries
query FilterLedgerEntries(
  $ledger: LedgerMatchInput!,
  $entriesFilter: LedgerEntriesFilterSet!
) {
  ledger(ledger: $ledger) {
    ledgerEntries(filter: $entriesFilter) {
      nodes {
        ik
        type
        posted
        tags {
          key
          value
        }
      }
    }
  }
}
FilterLedgerEntries Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "entriesFilter": {
    "tag": {
      "equalTo": {
        "key": "user_id",
        "value": "user-1"
      }
    }
  }
}

Use the in operator to return Ledger Entries that have any of the specified tags:

FilterLedgerEntries Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "entriesFilter": {
    "tag": {
      "in": [{
        "key": "user_id",
        "value": "user-1"
      },{
        "key": "user_id",
        "value": "user-2"
      }]
    }
  }
}

f. Ledger Entry Groups

#

See Group Ledger Entries for more information about how to use Ledger Entry Groups.

Lookup#

Use the ledger.ledgerEntryGroup expansion to lookup a group by key and value.

GetLedgerEntryGroup
query GetLedgerEntryGroup(
  $ledger: LedgerMatchInput!
  $entryGroup: EntryGroupMatchInput!
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroup(ledgerEntryGroup: $entryGroup) {
      key
      value
      created
    }
  }
}
GetLedgerEntryGroup Variables
{
  "entryGroup": {
    "key": "withdrawal",
    "value": "12345"
  },
  "ledger": {
    "ik": "quickstart-ledger"
  }
}

The response is:

GetLedgerEntryGroup Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroup": {
        "key": "withdrawal",
        "value": "12345",
        "created": "1969-06-16T13:32:00.000Z"
      }
    }
  }
}
List#
ListLedgerEntryGroups
query ListLedgerEntryGroups(
  $ledger: LedgerMatchInput!
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroups {
      nodes {
        key
        value
        created
      }
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
    }
  }
}
ListLedgerEntryGroups Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  }
}

The response is:

ListLedgerEntryGroups Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroups": {
        "nodes": [
          {
            "key": "withdrawal",
            "value": "12345",
            "created": "1969-06-16T13:32:00.000Z"
          },
          {
            "key": "withdrawal",
            "value": "54321",
            "created": "1969-06-21T02:56:05.000Z"
          }
        ],
        "pageInfo": {
          "endCursor": null,
          "hasNextPage": false,
          "hasPreviousPage": false,
          "startCursor": null
        }
      }
    }
  }
}
Filter#

You can filter groups by key and/or created

ListLedgerEntryGroups
query ListLedgerEntryGroups(
  $ledger: LedgerMatchInput!
  $filter: LedgerEntryGroupsFilterSet,
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroups(filter: $filter) {
      nodes {
        key
        value
        created
      }
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
    }
  }
}
ListLedgerEntryGroups Variables
{
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "key": {
      "equalTo": "withdrawal"
    },
    "created": {
      "before": "1969-06-20T00:00:00.000Z"
    }
  }
}

The response is:

ListLedgerEntryGroups Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroups": {
        "nodes": [
          {
            "key": "withdrawal",
            "value": "12345",
            "created": "1969-06-16T13:32:00.000Z"
          }
        ],
        "pageInfo": {
          "endCursor": null,
          "hasNextPage": false,
          "hasPreviousPage": false,
          "startCursor": null
        }
      }
    }
  }
}

h. External Accounts

#
Lookup#

Use the externalAccount query to retrieve an ExternalAccount by its ID at your external system:

GetExternalAccountByExternalIdAndLinkId
query GetExternalAccount(
  $externalId: ID!
  $linkId: ID!
) {
  externalAccount(
    externalAccount: {
      externalId: $externalId
      linkId: $linkId
    }
  ) {
    name
    link {
      __typename
      id
      name
    }
  }
}
GetExternalAccountByExternalIdAndLinkId Variables
{
  "externalId": "<External ID>",
  "linkId": "<Link ID>"
}

Or by its FRAGMENT ID:

GetExternalAccountByFragmentID
query GetExternalAccount($id: ID!) {
  externalAccount(externalAccount: { id: $id }) {
    name
    externalId
    linkId
    link {
      __typename
      id
      name
    }
  }
}
GetExternalAccountByFragmentID Variables
{
  "id": "<Fragment External Account ID>"
}
Txs#

Use the externalAccount.txs query to list Txs synced to an External Account:

ListExternalAccountTxs
query ListExternalAccountTxs(
  $externalAccount: ExternalAccountMatchInput!
  $after: String
  $first: Int
  $before: String
) {
  externalAccount(
    externalAccount: $externalAccount
  ) {
    externalId
    link {
      __typename
      id
      name
    }
    txs(
      after: $after
      first: $first
      before: $before
    ) {
      nodes {
        externalId
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}

You may optionally specify before and after to paginate the results and first to specify the page size.

ListExternalAccountTxs Variables
{
  "externalAccount": {
    "externalId": "<External ID>",
    "linkId": "<Link ID>"
  },
  "after": "2023-07-01T00:00:00.000Z",
  "before": "2023-07-30T23:59:59.999Z",
  "first": 20
}

The response is a paginated list of Txs.

Linked Accounts#

Use the externalAccount.ledgerAccounts query to list Ledger Accounts linked to this External Account:

GetExternalAccountLinkedAccounts
query GetExternalAccountLinkedAccounts(
  $externalAccount: ExternalAccountMatchInput!
) {
  externalAccount(
    externalAccount: $externalAccount
  ) {
    externalId
    name
    link {
      __typename
      id
      name
    }
    ledgerAccounts {
      nodes {
        path
        name
        type
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}
GetExternalAccountLinkedAccounts Variables
{
  "externalAccount": {
    "externalId": "<External ID>",
    "linkId": "<Link ID>"
  }
}

The response is a paginated list of Ledger Accounts.

i. Txs

#
Lookup#

Use the tx query to retrieve a Tx by its ID and Account ID at your external system:

GetTxByExternalIds
query GetTx(
  $externalId: ID!
  $externalAccountId: ID!
  $linkId: ID!
) {
  tx(
    tx: {
      externalId: $externalId
      externalAccountId: $externalAccountId
      linkId: $linkId
    }
  ) {
    id
    description
    amount
    currency {
      code
    }
    externalId
    link {
      id
    }
    externalAccount {
      id
      externalId
    }
  }
}
GetTxByExternalIds Variables
{
  "externalAccountId": "<External Account ID>",
  "external": "<External Tx ID>",
  "linkId": "<Link ID>"
}

Or by its FRAGMENT ID:

GetTxByFragmentID
query GetTx(
  $id: ID!
) {
  tx(
    tx: {
      id: $id
    }
  ) {
    id
    description
    amount
    currency {
      code
    }
    externalId
    link {
      id
    }
    externalAccount {
      id
      externalId
    }
  }
}
GetTxByFragmentID Variables
{
  "id": "<Fragment ID>"
}
Unreconciled#

Use the ledgerAccount.unreconciledTx query to list a Ledger Account's unreconciled Txs:

GetUnreconciledTxs
query GetUnreconciledTxs(
  $ledgerAccount: LedgerAccountMatchInput!
  $after: String
  $first: Int
  $before: String
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    path
    unreconciledTxs(
      after: $after
      first: $first
      before: $before
    ) {
      nodes {
        id
        description
        amount
        currency {
          code
        }
        externalId
        link {
          id
        }
        externalAccount {
          id
          externalId
        }
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
    }
  }
}

You may optionally specify before and after to paginate the results and first to specify the page size.

GetUnreconciledTxs Variables
{
  "ledgerAccount": {
    "path": "assets/bank/user-cash",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  },
  "after": "2023-07-01T00:00:00.000Z",
  "before": "2023-07-30T23:59:59.999Z",
  "first": 20
}

The response is a paginated list of Txs.

j. Schemas

#
Lookup#

Use the schema query to retrieve a Schema by its key:

GetSchema
query GetSchema($schema: SchemaMatchInput!) {
  schema(schema: $schema) {
    key
    name
    latestVersion: version {
      created
      version
    }
    firstVersion: version(version: 1) {
      created
      version
    }
  }
}

When retrieving a Schema, use the version argument to query a specific version of the Schema. By default, the latest version is returned:

GetSchema Variables
{
  "schema": {
    "key": "quickstart-schema"
  }
}

The JSON of a Schema version can be retrieved by querying the json field:

GetLatestSchemaJSON
query GetLatestSchemaJSON(
  $schema: SchemaMatchInput!
) {
  schema(schema: $schema) {
    key
    name
    version {
      created
      version
      json
    }
  }
}
GetLatestSchemaJSON Variables
{
  "schema": {
    "key": "quickstart-schema"
  }
}
List versions#

Use the schema.versions query to query all the versions of your Schema:

ListSchemaVersions
query ListSchemaVersions(
  $schema: SchemaMatchInput!
) {
  schema(schema: $schema) {
    key
    name
    versions {
      nodes {
        created
        version
        json
      }
    }
  }
}

The response is a paginated list of your Schema's versions:

ListSchemaVersions Variables
{
  "schema": {
    "key": "quickstart-schema"
  }
}
List Ledgers#

Use the schema.ledgers query to list the Ledgers created off a Schema:

ListSchemaLedgers
query ListSchemaLedgers(
  $schema: SchemaMatchInput!
) {
  schema(schema: $schema) {
    key
    name
    ledgers {
      nodes {
        ik
        name
        created
        balanceUTCOffset
      }
    }
  }
}
ListSchemaLedgers Variables
{
  "schema": {
    "key": "quickstart-schema"
  }
}
Migration status#

FRAGMENT asynchronously migrates Ledgers when their Schema is updated. The current status of a Ledger's migration can be queried using the API, via version.migrations:

GetLedgerMigrationStatus
query GetLedgerMigrationStatus(
  $schema: SchemaMatchInput!
) {
  schema(schema: $schema) {
    key
    name
    latestVersion: version {
      migrations {
        nodes {
          ledger {
            ik
            name
          }
          status
        }
      }
    }
  }
}
ListSchemaLedgers Variables
{
  "schema": {
    "key": "quickstart-schema"
  }
}
J Generate reports#

FRAGMENT supports queries for generating common financial reports.

a. Balance sheet

#

A balance sheet reports the net worth of a business at the end of a reporting period.

Balance sheet query
query GetBalanceSheet(
  $ledgerIk: SafeString!
  $balanceAtEndOf: LastMoment!
  $accountsFilter: LedgerAccountsFilterSet!
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts(filter: $accountsFilter) {
      nodes {
        id
        name
        type
        balance(at: $balanceAtEndOf)
      }
    }
  }
}
Balance sheet parameters
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "type": {
      "in": ["asset", "liability"]
    }
  },
  "balanceAtEndOf": "1969"
}

Generate a balance sheet by querying balance on all asset and liability Ledger Accounts.

Providing a LastMoment to the at parameter on balance returns the balance at the end of that period. Values provided to in operators are OR'd, so both asset and liability accounts are returned.

b. Income statement

#

An income statement reports how a business's net worth changed over the course of a reporting period.

Income statement query
query GetIncomeStatement(
  $ledgerIk: SafeString!
  $balanceChangeDuring: Period!
  $accountsFilter: LedgerAccountsFilterSet!
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts(filter: $accountsFilter) {
      nodes {
        path
        name
        type
        balanceChange(period: $balanceChangeDuring)
      }
    }
  }
}
Income statement parameters
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "type": {
      "in": ["income", "expense"]
    }
  },
  "balanceChangeDuring": "1969"
}

Generate an income statement by querying balanceChange on all income and expense Ledger Accounts.

Providing a Period to the period parameter on balanceChange retrieves the difference in the Ledger Account's balance between the start and end of that period.

c. Account statement

#

An account statement reports how a Ledger Account changed over the course of a reporting period. It contains a Ledger Account's starting balance, ending balance and all Ledger Lines posted to it.

Monthly account statement query
query GetAccountStatement(
  $accountMatch: LedgerAccountMatchInput!
  $startingBalanceAtEndOf: LastMoment!
  $endingBalanceAtEndOf: LastMoment!
  $linesFilter: LedgerLinesFilterSet!
) {
  ledgerAccount(ledgerAccount: $accountMatch) {
    path
    name
    type
    startingBalance: balance(at: $startingBalanceAtEndOf)
    endingBalance: balance(at: $endingBalanceAtEndOf)
    lines(filter: $linesFilter) {
      nodes {
        id
        key
        posted
        description
        amount
        ledgerEntryId
      }
    }
  }
}
Monthly account statement variables
{
  "accountMatch": {
    "ledger": {
      "ik": "ik-used-to-create-ledger"
    },
    "path": "liabilities/customer-deposits/customer:123"
  },
  "linesFilter": {
    "posted": {
      "after": "1969-07-01T00:00:00.000Z",
      "before": "1969-07-30T23:59:59.999Z"
    }
  },
  "startingBalanceAtEndOf": "1969-06",
  "endingBalanceAtEndOf": "1969-07"
}

Generate an account statement by querying for balance and lines on a Ledger Account.

Get the starting balance by passing a DateTime to the at parameter on balance. Use a GraphQL alias to make multiple balance queries within one request.

To get all Ledger Lines that were posted during the reporting period, use the filter parameter on lines. The after and before filters are inclusive, so use timestamps for the first and last moments of the reporting period.

d. Journal export

#

A journal export lists all Ledger Entries posted to a Ledger during a reporting period.

Journal export query
query GetJournalExport(
  $ledgerIk: SafeString!
  $entriesFilter: LedgerEntriesFilterSet!
  $entriesCursor: String
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerEntries(filter: $entriesFilter, after: $entriesCursor) {
      nodes {
        id
        type
        posted
        description
        lines {
          nodes {
            id
            description
            account {
              name
              path
              type
            }
            amount
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
Journal export variables
{
  "ledgerIk": "ik-used-to-create-ledger",
  "entriesFilter": {
    "posted": {
      "after": "1969-01-01T00:00:00.000Z",
      "before": "1969-03-31T23:59:59.999Z"
    }
  },
  "entriesCursor": "{{data.ledger.ledgerEntries.pageInfo.endCursor}}"
}

Generate a journal export by listing Ledger Entries. For each Ledger Entry, include its Ledger Lines and their Ledger Accounts.

This can be a long list, so query pageInfo to get a pagination cursor. It can be passed to the after parameter on ledgerEntries to page through the results.

K Configure Consistency#

To ensure correctness at scale, FRAGMENT's consistency mode lets you build guardrails without sacrificing performance.

You can configure consistency within your Schema to make granular tradeoffs between throughput and consistency.

a. Ledger Configuration

#

You can configure the consistency of the Ledger Entries list query in your Ledger. To do this, set consistencyConfig at the top level of your Schema:

  • Use entries: eventual for Ledgers that require high throughput but can tolerate a stale entry list
  • Use entries: strong for Ledgers that have lower throughput but require strong consistency, such as those powering reconcilation dashboards
Configuring consistency
{
  "consistencyConfig": {
    "entries": "strong"
  },
  "chartOfAccounts": [...]
}

By default, all Ledgers use eventual consistency.

b. Ledger Account Configuration

#

You can configure the consistency of balances, as well as the Ledger Lines list query, in your Ledger Account.

To configure an account's balance consistency, set consistencyConfig.ownBalanceUpdates within a Ledger Account's definition:

  • Use ownBalanceUpdates: eventual for Ledger Accounts that require high throughput but can tolerate stale balances, such as those used for reporting
  • Use ownBalanceUpdates: strong for Ledger Accounts that have lower throughput but require strong consistency, such as those used to authorize transactions

Similarly, to configure the consistency of an account's lines, set consistencyConfig.lines:

  • Use lines: eventual for Ledger Accounts that require high throughput but can tolerate a stale line list
  • Use lines: strong for Ledger Accounts that have lower throughput but require strong consistency, such as those powering transaction histories displayed to end users
Configuring consistency
{
  "accounts": [
    {
      "key": "user-balance",
      "template": true,
      "type": "asset",
      "consistencyConfig": {
        "ownBalanceUpdates": "strong",
        "lines": "eventual"
      }
    }
  ]
}

By default, all Ledger Accounts use eventual for both properties.

For low-throughput applications, setting all Ledger Accounts as strong may make implementation easier. To do this, set defaultConsistencyConfig on chartOfAccounts:

Configuring default consistency
{
  "chartOfAccounts": {
    "defaultConsistencyConfig": {
      "ownBalanceUpdates": "strong",
      "lines": "strong"
    },
    "accounts": [...]
  }
}

Strongly consistent Ledger Accounts generally won't have children, but in all cases child Ledger Accounts inherit the parent's consistencyConfig setting.

c. Balance queries

#

To query a strongly consistent ownBalance, set consistencyMode to strong when querying the Ledger Account:

Setting consistency on ownBalance reads
query GetOwnBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalance(consistencyMode: strong)
    ownBalances(consistencyMode: strong) {
      nodes {
        amount
        currency {
          code
        }
      }
    }
  }
}

By default, balance queries on all Ledger Accounts are eventually consistent.

Restrictions:

  • Only ownBalance supports strongly consistent reads; balance and childBalance support only eventually consistent reads
  • Querying a strongly consistent balance on an eventually consistent Ledger Account throws a GraphQL error
  • You cannot combine at with consistencyMode to query strongly consistent historical balances

d. Entry conditions

#

Entry conditions are rules defined in your Schema to manage concurrency and enforce correctness within your Ledger.

Conditions are evaluated when a Ledger Entry is posted. If a condition is not met, the Ledger Entry is not posted and the mutation returns a BadRequestError with code conditional_request_failed.

Use precondition when your application reads a balance and needs to guarantee that it hasn't changed before posting the Ledger Entry.

Using precondition

{
  "type": "pay-employee",
  "lines": [...],
  "conditions": [
    {
      "account": {
        "path": "bank-account"
      },
      "precondition": {
        "ownBalance": {
          "eq": "{{current_balance}}"
        }
      }
    }
  ]
}

Use postcondition to guarantee that a write never puts a Ledger Account's balance in an undesirable state.

Using postcondition

{
  "type": "pay-employee",
  "lines": [...],
  "conditions": [
    {
      "account": {
        "path": "bank-account"
      },
      "postcondition": {
        "ownBalance": {
          "gte": "0"
        }
      }
    }
  ]
}

Restrictions:

  • Entry conditions apply to ownBalance, which changes only for Ledger Accounts directly posted to in the Ledger Entry.
  • Ledger Accounts referenced in Entry conditions must have consistencyConfig.ownBalanceUpdates set to strong
  • Linked Ledger Accounts do not support Entry conditions
L Handle currencies#

Use multi-currency Ledgers to easily build products that track currencies, stocks and inventories.

A Ledger can contain Ledger Accounts of different currencies, like USD and GBP bank accounts. Ledger Accounts can also be multi-currency, like one representing a stock portfolio, with a balance for each symbol.

You can post Ledger Entries with multiple currencies. It must follow the Accounting Equation per currency, so you'll need at least four Ledger Lines.

FRAGMENT includes a list of common currencies. You can also add your own custom currencies.

a. Track exposure

#

Multi-currency ledgers often reflect transitory states: a company accepts payment in one currency intending to convert it to another currency. Between accepting and converting the money, the exchange rate could change. Tracking the potential gain or loss from this change is called exposure.

To track exposure, use a Change Ledger Account that has multiple balances, one for each currency. Here's an example that tracks exposure between USD and EUR:

In this example,

  • User 1 starts out with 100 USD, balanced by 100 USD in the platform's bank.
  • User 1 pays User 2, but they have different wallet currencies, so we record the exchange rate they were given in an Exposure account. The exposure account balance is 100 USD - 90 EUR.
  • The platform now owes User 2 EUR, so it exchanges 100 USD for 95 EUR. In this case, due to exchange rate fluctuation, they got a different rate. The exposure account balance records the difference in exchange rates: 5 EUR.

b. Ledger Accounts

#

To create a multi-currency Ledger, set defaultCurrencyMode to multi and unset defaultCurrency:

Multi-currency Ledger
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {...},
      {...}
    ]
  },
  "ledgerEntries": {...}
}

For Ledger Accounts in a single currency, such as bank accounts, set currencyMode and currency on the account directly:

Single-currency Ledger Account
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {
        "key": "bank-account",
        "currencyMode": "single",
        "currency": {
          "code": "USD"
        }
      },
      {...}
    ]
  },
  "ledgerEntries": {...}
}

Like other Ledger Account properties, currencyMode and currency are inherited by child Ledger Accounts unless they are overridden.

c. Ledger Entries

#

You can define multi-currency Ledger Entries types in your Schema in the same way as single-currency Ledger Entries.

Multi-currency Ledger Accounts accept Ledger Lines in any currency, so Ledger Entries that post to them must specify the currency of each Ledger Line:

Multi-currency Ledger Entry
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {
        "type": "asset",
        "key": "bank-account"
      },
      {...}
    ]
  },
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_usd",
        "description": "Fund {{funding_amount}} USD",
        "lines": [
          {
            "key": "funds_arrive_in_bank",
            "account": { 
              "path": "bank-account"
            },
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "USD"
            }
          },
          {...other line}
        ]
      }
    ]
  }
}

You can parameterize currency to make your Ledger Entries more reusable:

Parameterized multi-currency Ledger Entry
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {
        "type": "asset",
        "key": "bank-account"
      },
      {...}
    ]
  },
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Fund {{funding_amount}} {{currency}}",
        "lines": [
          {
            "key": "funds_arrive_in_bank",
            "account": { 
              "path": "bank-account"
            },
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "{{currency}}"
            }
          },
          {...other line}
        ]
      }
    ]
  }
}

You can also post multi-currency Ledger Entries to Ledger Account templates which parameterize currency. This is useful for creating Linked Ledger Accounts if you have multiple bank accounts in different currencies in a multi-currency Ledger.

Parameterized Ledger Account templates
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {
        "key": "bank-accounts",
        "template": true,
        "currencyMode": "single",
        "currency": {
          "code": "{{currency}}"
        }
      },
      {...}
    ]
  },
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Fund {{funding_amount}} {{currency}}",
        "lines": [
          {
            "key": "funds_arrive_in_bank",
            "account": { 
              "path": "bank-accounts:{{currency}}"
            },
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "{{currency}}"
            }
          },
          {...other line}
        ]
      }
    ]
  }
}

Ledger Entry Conditions against multi-currency Ledger Accounts need to specify the currency the condition applies to:

Parameterized Ledger Account templates
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {...},
      {...}
    ]
  },
  "ledgerEntries": {
    "types": [
      {
        "type": "p2p_transfer",
        "description": "Move {{funding_amount}} {{currency}}",
        "lines": [
          {...},
          {...}
        ],
        "conditions": [
          {
            "account": {
              "path": "liabilities/users:{{from_user_id}}"
            },
            "currency": {
              "code": "{{currency}}"
            },
            "postcondition": {
              "ownBalance": {
                "gte": "0"
              }
            }
          }
        ]
      }
    ]
  }
}

d. Read balances

#

Balances on multi-currency Ledger Accounts are lists of currency and amount, as opposed to just a single amount. To read all balances in all currencies, query the plural versions of the singular balance field.

Latest#

You can read the latest balance in a specific currency or list the latest balance in all currencies.

Multi-currency Ledger Accounts have three balance lists:

  • ownBalances, the sum of all Ledger Lines in the Ledger Account per currency, excluding Ledger Lines in child Ledger Accounts
  • childBalances, the sum of all Ledger Lines in child Ledger Accounts per currency
  • balances, the sum of all Ledger Lines, including child Ledger Accounts per currency

To read a specific currency's balance, pass in the currency argument:

Get USD balance read
query GetUSDBalance(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalance(currency: { code: USD })
  }
}
GetUSDBalance variables
{
  "ledgerAccount": {
    "path": "assets/bank/user-cash",
    "ledger": {
      "ik": "multi-currency-ledger"
    }
  }
}

To read all the balances for a multi-currency Ledger Account, use ownBalances:

Get all balances read
query GetAllBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalances {
      nodes {
        currency {
          code
        }
        amount
      }
    }
  }
}
Aggregated#

To read aggregated balances for multi-currency Ledger Accounts, pass in the currency argument, or query childBalances and balances:

Get aggregated balances read
query GetBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {

    childBalance(currency: { code: USD })
    childBalances {
      nodes {
        currency {
          code
        }
        amount
      }
    }

    balance(currency: { code: USD })
    balances {
      nodes {
        currency {
          code
        }
        amount
      }
    }

  }
}

If any Ledger Account has a descendant that is a multi-currency Ledger Account or if it has descendants of different currencies, it has childBalances and balances.

Consistent#

To read consistent balances for multi-currency Ledger Accounts, pass in the consistencyMode argument:

Get strongly-consistent ownBalances read
query GetOwnBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalance(
      consistencyMode: strong
      currency: { code: USD }
    )
    ownBalances(consistencyMode: strong) {
      nodes {
        currency {
          code
        }
        amount
      }
    }
  }
}
Historical#

To read historical balances for multi-currency Ledger Accounts, pass in the at argument:

Get historical balances read
query GetOldBalances(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    ownBalance(at: "1969", currency: { code: USD })
    ownBalances(at: "1969") {
      nodes {
        currency {
          code
        }
        amount
      }
    }
  }
}
Balance changes#

Multi-currency Ledger Accounts support reading balance changes:

  • ownBalanceChanges, how much ownBalances changed
  • childBalanceChanges, how much childBalances changed
  • balanceChanges, how much balances changed
Balance change queries
query GetBalanceChanges(
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    
    ownBalanceChange(
      period: "1969"
      currency: { code: USD }
    )
    ownBalanceChanges(period: "1969") {
      nodes {
        currency {
          code
        }
        amount
      }
    }

    childBalanceChange(
      period: "1968-12"
      currency: { code: USD }
    )
    childBalanceChanges(period: "1968-12") {
      nodes {
        currency {
          code
        }
        amount
      }
    }

    balanceChange(
      period: "1968-12-25"
      currency: { code: USD }
    )
    balanceChanges(period: "1968-12-25") {
      nodes {
        currency {
          code
        }
        amount
      }
    }
      
  }
}

Balance change queries require you to specify a period. This can be a year, quarter, month, day or hour.

e. Custom currencies

#

You can define your own currencies to track any type of value, like rewards points, stocks or physical items.

To create a custom currency, call the createCustomCurrency mutation:

createCustomCurrency mutation
mutation CreateCustomCurrency (
    $customCurrency: CreateCustomCurrencyInput!,
) {
  createCustomCurrency(
    customCurrency: $customCurrency
  ) {
    ... on CreateCustomCurrencyResult {
      customCurrency {
        code
        customCurrencyId
        precision
        name
        customCode
      }
    }
    ... on Error {
      code
      message
    }
  }
}
createCustomCurrency variables
{
  "customCurrency": {
    "customCurrencyId": "blue-gems",
    "precision": 0,
    "name": "Blue Gems",
    "customCode": "BLUE"
  }
}

To use a custom currency, set the customCurrencyId on the currency field of a Ledger Account and Ledger Line:

Custom currency Ledger Account
{
  "key": "...",
  "chartOfAccounts": {
    "defaultCurrencyMode": "multi",
    "accounts": [
      {
        "key": "gems-issued",
        "currencyMode": "single",
        "currency": {
          "code": "CUSTOM",
          "customCurrencyId": "blue-gems"
        }
      },
      {...}
    ]
  },
  "ledgerEntries": {
    "types": [
      {
        "type": "issue-blue-gems",
        "description": "Issue blue gems",
        "lines": [
          {
            "key": "increase-pool",
            "account": { 
              "path": "gems-issued"
            },
            "amount": "{{amount}}",
            "currency": {
              "code": "CUSTOM",
              "customCurrencyId": "blue-gems"
            }
          },
          {...other line}
        ]
      }
    ]
  }
}
M Group Ledger Entries#

A Ledger Entry Group is a collection of related Ledger Entries that occur at different points in time. Each Group tracks the net change to each Ledger Account balance it affects.

Use Ledger Entry Groups to tie together Ledger Entries that are part of the same funds flow, such as a deposit, settlement or invoice. To store metadata, use tags instead.

a. Configuring Groups

#

Groups for a Ledger Entry Type are defined as a list of key/value pairs in the Schema:

Ledger Entry Type with Groups
{
  "type": "user_initiates_withdrawal",
  "description": "{{user_id}} initiates withdrawal",
  "lines": [
    {
      "account": {
        "path": "liabilities/users:{{user_id}}/available"
      },
      "key": "decrease_user_balance",
      "amount": "-{{withdraw_amount}}"
    },
    {...other line}
  ],
  "groups": [
    {
      "key": "withdrawal",
      "value": "{{withdrawal_id}}"
    }
  ]
}

Ledger Entry Groups have the following limitations:

  • You can specify up to 10 Groups for any Ledger Entry type.
  • Parameters can be used in the value of a Group, but not the key.
  • Group keys must be unique per Ledger Entry, and you can only provide one value for each key.

b. Querying balances

#

Use the ledgerEntryGroup.balances expansion to get the net change per Ledger Account balance from all Ledger Entries in a Group. Group balances are eventually consistent.

GetLedgerEntryGroupBalances
query GetLedgerEntryGroupBalances(
  $ledger: LedgerMatchInput!
  $entryGroup: EntryGroupMatchInput!,
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroup(ledgerEntryGroup: $entryGroup) {
      balances {
        nodes {
          account {
            path
          }
          ownBalance
        }
      }
    }
  }
}
GetLedgerEntryGroupBalances Variables
{
  "entryGroup": {
    "key": "withdrawal",
    "value": "some-withdrawal-id"
  },
  "ledger": {
    "ik": "quickstart-ledger"
  }
}

The response for a Ledger Entry Group with a settled withdrawal:

GetLedgerEntryGroupBalances Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroup": {
        "balances": {
          "nodes": [
            {
              "account": {
                "path": "asset-root/bank"
              },
              "ownBalance": "-50000"
            },
            {
              "account": {
                "path": "liability-root/user:user-id/available"
              },
              "ownBalance": "-50000"
            },
            {
              "account": {
                "path": "liability-root/user:user-id/pending"
              },
              "ownBalance": "0"
            }
          ]
        }
      }
    }
  }
}
Filtering balances#

Balances in a Group may be filtered by account, currency, and ownBalance.

QueryLedgerEntryGroupBalances
query QueryLedgerEntryGroupBalances(
  $ledger: LedgerMatchInput!
  $entryGroup: EntryGroupMatchInput!,
  $filter: LedgerEntryGroupBalanceFilterSet,
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroup(ledgerEntryGroup: $entryGroup) {
      balances(filter: $filter) {
        nodes {
          account {
            path
          }
          ownBalance
          currency {
            code
          }
        }
      }
    }
  }
}
QueryLedgerEntryGroupBalances Variables
{
  "entryGroup": {
    "key": "withdrawal",
    "value": "12345"
  },
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "currency": {
      "equalTo": { "code": "USD" }
    },
    "ownBalance": {
      "gte": "-1000",
      "lte": "1000"
    },
    "account": {
      "path": {
        "equalTo": "liability-root/user:user-id/pending"
      }
    }
  }
}

The response is:

QueryLedgerEntryGroupBalances Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroup": {
        "balances": {
          "nodes": [
            {
              "account": {
                "path": "liability-root/user:user-id/pending"
              },
              "ownBalance": "0",
              "currency": {
                "code": "USD"
              }
            }
          ]
        }
      }
    }
  }
}
Filter by template#

Group balances support filtering account paths using '*' in place of a template variable.

QueryLedgerEntryGroupBalances
query QueryLedgerEntryGroupBalances(
  $ledger: LedgerMatchInput!
  $entryGroup: EntryGroupMatchInput!,
  $filter: LedgerEntryGroupBalanceFilterSet,
) {
  ledger(ledger: $ledger) {
    ledgerEntryGroup(ledgerEntryGroup: $entryGroup) {
      balances(filter: $filter) {
        nodes {
          account {
            path
          }
          ownBalance
        }
      }
    }
  }
}
QueryLedgerEntryGroupBalances Variables
{
  "entryGroup": {
    "key": "withdrawal",
    "value": "12345"
  },
  "ledger": {
    "ik": "quickstart-ledger"
  },
  "filter": {
    "account": {
      "path": {
        "matches": "liability-root/user:*/pending"
      }
    }
  }
}

The response is:

QueryLedgerEntryGroupBalances Response
{
  "data": {
    "ledger": {
      "ledgerEntryGroup": {
        "balances": {
          "nodes": [
            {
              "account": {
                "path": "liability-root/user:user-id/pending"
              },
              "ownBalance": "0"
            },
            {
              "account": {
                "path": "liability-root/user:another-user-id/pending"
              },
              "ownBalance": "2500"
            }
          ]
        }
      }
    }
  }
}
N Export Data#

a. S3 Export

#

FRAGMENT supports exporting your Ledger data to AWS S3. This makes your data available for analytics, regulatory compliance and ingestion into third-party systems.

Export format#

Data exports are delivered approximately every 5 minutes. Each data export contains the Ledger data created or updated since the previous export.

Exports are divided by data type into three files. Each file contains newline-separated JSON, where each line represents an individual instance of the respective data type: 'LedgerEntry', 'LedgerLine' or 'LedgerAccount'.

The files use a naming scheme of {File Prefix}/{type}/day={day}/hour={hour}/{partition}.part.

Example files:

Onboarding#

To enable data export, first create an S3 bucket. No special settings are required, but take note of the bucket name and AWS region.

Once the bucket exists, navigate to the Settings -> S3 Exports subsection of the FRAGMENT dashboard, and follow the instructions to create a new export. You need to provide:

  • S3 Bucket Name, the name of the S3 bucket in AWS.
  • S3 Bucket Region, the AWS region of the bucket created in the previous step (for example, us-east-1).
  • Export Name, a name for this export. This is for display purposes only.
  • File Prefix (optional), the path where exports are stored. If no file prefix is provided, exports are stored in the bucket root.

The instructions in the dashboard include applying a S3 bucket policy to your bucket. Depending on how you manage your infrastructure, this may take some time, so the onboarding flow in the dashboard can be restarted at any time.

Testing#

Once you've onboarded your data export, the dashboard lets you test the connection by writing a sample file to test/testLedgerEntry.part in your bucket. The test result can be:

  • Policy Not Applied, FRAGMENT received an authorization error. This typically means the provided resource policy has not yet been applied within your AWS account. This may also mean the provided bucket name or region differs from the one created in the initial setup.
  • Invalid Bucket Name, the provided bucket name does not exist in AWS.
  • Incorrect AWS Region, the provided bucket exists in a different region than provided.
  • Verified Permissions, the file was successfully written.

FRAGMENT does not remove the test file after verifying its existence.

b. Retool

#

You can add FRAGMENT as a GraphQL resource in Retool.

  1. Create an API client in the FRAGMENT dashboard.
  2. Add a new GraphQL Resource in Retool.
  3. Set the Base URL to your API URL.
  4. Add the Authorization Header with the value Bearer OAUTH2_TOKEN. Retool will replace OAUTH2_TOKEN with the actual token at runtime.
  5. Set Authentication to OAuth 2.0
  6. Enable Use Client Credentials Flow
  7. Set the following values:
    • Set Access Token URL to OAuth URL
    • Set the Client ID and Secret
    • Set Scopes to OAuth Scope
    • Set Prompt to consent
  8. Save the resource.

Note: Testing the connection in Retool may fail even when the resource is configured correctly. To check, use the resource in an app by running the query.

O Under the hood#

FRAGMENT is the database we wish we had at Stripe and Robinhood. It's the abstraction we want at the performance we need.

a. Performance

#

On February 13, 2024, we ran a load test to simulate traffic using Grafana K6. A total of 19,622,609 requests were made over a 15 minute period. 6,769,966 of them were reads and 12,852,643 of them were writes. We observed:

  • 14,578 ledger entries per second average write throughput
  • 7,489 balance reads per second average read throughput
  • 33ms p95 read latency
  • 69ms p95 write latency

By comparison, at Robinhood it took 18 months of dedicated engineering effort to get even remotely close to these numbers.

b. Architecture

#

To achieve this performance, FRAGMENT is built on top of two distributed databases:

  • Transactional storage on AWS DynamoDB
  • Indexed storage on ElasticSearch

A two-tier system, as opposed to one built on a single Postgres instance, is harder to build and maintain. But by having separate write-optimized and read-optimized systems, we can tune each system independently and use the best tool for each job.

c. Write path

#

When customers hit any of our GraphQL mutations, all data is synchronously written into DynamoDB then asynchronously indexed on ElasticSearch.

d. Scaling writes

#

We optimize DynamoDB by:

  • Storing data in many small partitions. Each ledger entry is its own Dynamo partition. This scales horizontally since each partition can be served from a different DynamoDB server.
  • Using a single-table design with a dataloader. The dataloader batches several reads in a single application tick into one request to DynamoDB, minimizing the number of requests made to DynamoDB.

e. Read path

#

Depending on the query, GraphQL queries API requests are served from either DynamoDB or ElasticSearch.

  • DynamoDB serves ledger account balances, single item lookups, and low-volume list queries
  • ElasticSearch serves list queries that may use filtering and pagination

f. Scaling reads

#

Our ElasticSearch strategy is based on the idea that each query should only hit a single server. When a list query comes in, it gets routed to a single server, which uses sorting to cut down the search space, applies additional filters on indexed attributes, then returns the results. The results are fully hydrated so the FRAGMENT API can return data directly from ElasticSearch without hitting DynamoDB again.

This strategy is opposite to Elasticsearch's default where docs are thrown onto random servers and queries map out to every server in the cluster. Our strategy works well for a highly structured search with a high hit rate: filtering data in a Ledger. The default strategy is better for a fuzzy search with a low hit rate, like searching for a string across millions of documents.