Documentation

A Introduction#

FRAGMENT's main surfaces are a hosted API, dashboard and a Schema designer you run locally with a CLI.

Don't have a FRAGMENT workspace? Get Access.

B Quickstart#

In this Quickstart, you will build a Ledger for a basic peer-to-peer payments app.

This app has users who can fund their account and transfer funds to other users. Behind the scenes there is a bank account that collectively pools all of the user's cash

To create the Ledger, you will:

  1. Store a Schema that models the flow of funds
  2. Create a Ledger using the Schema
  3. Post a Ledger Entry
  4. Query a Ledger Account balance
Quickstart Animation

a. API Explorer

#

To call the API, you will use the API Explorer in the dashboard. This is like Postman, but for GraphQL.

FRAGMENT is a GraphQL API, so every request has two parts: a GraphQL query and variables passed to the query. For each API call, you will need to copy-paste both the query and variables into the API Explorer. Then, click the play button to call the API and see the response.

b. Store a Schema

#

In FRAGMENT, you use a Schema to model the product's flow of funds. A Schema defines the structure of the Ledger, and will be specific to a given use case.

Call the storeSchema mutation to store a Schema for the app.

storeSchema query
mutation QuickstartStoreSchema($schema: SchemaInput!) {
  storeSchema(schema: $schema) {
    ... on StoreSchemaResult {
      schema {
        key
        name
        version {
          version
          created
          json
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}

The Schema defines Ledger Accounts for the users and the bank, and Ledger Entries for events in the product, like user's funding their account or making p2p transfers.

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"
                }
              }
            }
          ]
        },
        {
          "type": "withdrawal",
          "description": "{{user_id}} withdraws {{withdrawal_amount}}",
          "lines": [
            {
              "account": { "path": "assets/banks/user-cash" },
              "key": "funds_leave_bank",
              "amount": "{{withdrawal_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "key": "decrease_user_balance",
              "amount": "{{withdrawal_amount}}"
            }
          ],
          "conditions": [
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "postcondition": {
                "ownBalance": {
                  "gte": "0"
                }
              }
            }
          ]
        },
        {
          "type": "withdrawal_with_fee",
          "description": "{{user_id}} withdraws {{withdrawal_amount}} instant with {{rtp_fees}} fee.",
          "lines": [
            {
              "account": { "path": "assets/banks/user-cash" },
              "key": "funds_leave_bank",
              "amount": "-{{withdrawal_amount}} + {{rtp_fees}}"
            },
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "key": "decrease_user_balance",
              "amount": "-{{withdrawal_amount}}"
            },
            {
              "account": { "path": "income/rtp-fees" },
              "key": "book_income",
              "amount": "{{rtp_fees}}"
            }
          ],
          "conditions": [
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "postcondition": {
                "ownBalance": {
                  "gte": "0"
                }
              }
            }
          ]
        }
      ]
    }
  }
}
 
Chart of Accounts#

The chartOfAccounts field defines the Ledger Accounts that will be created for this Ledger. It has two types of accounts:

  • a bank account for user's cash held by the company. This is an asset account because it is money the platform holds.
  • a tree of accounts for each user on the platform. These are liability accounts since the platform owes the user these funds. They are also marked as template: true, to indicate that these accounts will be created for each user.
Ledger Entries#

The ledgerEntries field defines the types of Ledger Entries that can be posted to the Ledger once it's created. Think of these like a store procedure. In this Schema, there are two types of Ledger Entries:

  • user_funds_account: this takes in a user_id, the ID of the user from your system, and funding_amount, the amount the user is funding. It creates a Ledger Entry that increases the user's available balance and increases the bank's cash. We're simplifying a bit here: in reality funding may happen in two steps.
  • p2p_transfer. this takes in two user IDs from_user_id and to_user_id, and a transfer_amount. It creates a Ledger Entry that decreases the available balance of the from_user_id and increases the available balance of the to_user_id.

c. View your Schema

#

See the Schema you created in the Schemas tab of the dashboard.

d. Create a Ledger

#

Create a Ledger for the app 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 quickstart-schema so the Ledger is created using the Schema stored by the previous API call.

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

e. Post Ledger Entries

#

Use the addLedgerEntry mutation to post Ledger Entries.

addLedgerEntry
mutation QuickstartAddLedgerEntry(
  $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
    }
  }
}

Let's start by posting a user funding Ledger Entry.

addLedgerEntry variables
{
  "ik": "fund-user-1-account",
  "entry": {
    "type": "user_funds_account",
    "posted": "1234-11-11T13:00:00.000Z",
    "ledger": {
      "ik": "quickstart-ledger"
    },
    "parameters": {
      "funding_amount": "10000",
      "user_id": "user-1"
    }
  }
}

The important fields to call out are type and parameters. For details on the rest of the fields check out (Posting Ledger Entries)[/#Post-Ledger-Entries].

  • The entry.type field is set to user_funds_account, defined in the Schema
  • The entry.parameters field contains a key/value pair of all the parameters defined in the Schema. The funding_amount parameter sets the Ledger Line amounts and the user_id parameter sets the Ledger Account path. Since the user account is a template Ledger Account, it gets created on the fly.

After funding the first user, change the parameter values and re-run the mutation to create another funded user account.

addLedgerEntry variables
{
  "ik": "fund-user-2-account",
  "entry": {
    "type": "user_funds_account",
    "posted": "1234-11-11T13:00:00.000Z",
    "ledger": {
      "ik": "quickstart-ledger"
    },
    "parameters": {
      "funding_amount": "10000",
      "user_id": "user-2"
    }
  }
}

You can then try the p2p_transfer Ledger Entry by setting type to p2p_transfer and filling in the appropriate parameters from the Schema.

addLedgerEntry variables
{
  "ik": "p2p-transfer",
  "entry": {
    "type": "p2p_transfer",
    "posted": "1234-12-11T13:00:00.000Z",
    "ledger": {
      "ik": "quickstart-ledger"
    },
    "parameters": {
      "transfer_amount": "5000",
      "from_user_id": "user-1"
      "to_user_id": "user-1"
    }
  }
}

f. Query a Balance

#

Now that we've posted some Ledger Entries, let's query a Ledger Account's balance using the ledgerAccount query.

GetBalances query
query QuickstartGetBalance (
  $account: LedgerAccountMatchInput!
) {
    ledgerAccount(ledgerAccount: $account) {
      path
      name
      type
      ownBalance(consistencyMode: use_account)
      created
      currency {
        code
      }
  }
}
GetBalances variables
{
  "account": {
    "path": "liabilities/users:user-1/available",
    "ledger": {
      "ik": "quickstart-ledger"
    }
  }
}

Try changing path to assets/banks/user-cash or liabilities/users:user-2/available to query the other Ledger Accounts.

g. View your Ledger

#

Look in the dashboard to see the Ledger, Ledger Accounts and Ledger Entries you've created.

h. Next Steps

#

Now that you've created a toy Ledger for this p2p app, the next step is to model your own product's flow of funds and begin building your own Ledger. To do that, continue to the next section, Design your Schema.

C API Overview#

a. CLI

#

The fragment CLI lets you manage your Schema as a file checked into your codebase. It includes:

  • an interactive designer to create, visualize and simulate your Schema.
  • commands to upload your Schema from your codebase and trigger Ledger migrations.
Install the CLI#
brew tap fragment-dev/tap
brew install fragment-dev/tap/fragment-cli

This will install the fragment commend.

Initialize a Schema#
fragment init

This writes an example Schema to ./fragment.jsonc.

Start the designer#
fragment start

This opens the designer in your browser and watches for changes to your fragment.jsonc file.

Login to the CLI#
fragment login

This authenticates the CLI to your workspace. In CI, pass in the credentials as parameters.

Store your Schema#
fragment store-schema

Uploads fragment.jsonc and creates a new version of the Schema. All Ledgers using that Schema will be migrated to the new version.

b. GraphQL API

#

Use the GraphQL API to read and write data from your Ledger.

The API URL for your workspace can be found in the dashboard.

You can find the GraphQL schema hosted at:

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

You can use this schema as input to auto-generate an SDK to use to call the FRAGMENT API. The GraphQL Foundation maintains an updated list of tools. We recommend that you do this as part of your CI, to ensure that you always have the latest schema.

c. No Decimals

#

There are only integer amounts in the API. This forces you to use the minor units in the appropriate currency. For USD, a single unit is 1 cent. To represent $1.25, you'd pass in 125.

d. Idempotency

#

In financial systems, it's important to achieve exactly-once processing. For example, when debiting a customer's bank account, only one ACH Debit should be made. FRAGMENT's API is idempotent, so your system needs to only handle at-least-once processing when calling the FRAGMENT API.

To achieve idempotency, mutations in the FRAGMENT API require a unique and stable idempotency key (IK). These let you confidently retry operations without them running twice. If you execute a mutation more than once with the same IK and variables, FRAGMENT will ignore your request and return the original response with the isIkReplay flag set to true. IKs in FRAGMENT:

  • are scoped per-Ledger. If you send multiple requests with the same IK to a single ledger, only one mutation will execute with isIkReplay: false, while the rest will return the original response with isIkReplay: true. If you send multiple requests with the same IK to multiple different ledgers, all mutations will execute and return isIkReplay: false.
  • are valid for 30 days. If you resend an IK after 30 days, it could be executed as a fresh request.
  • are only valid for 1 hour when initiating transfers at Increase through the makeBankTransfer or createOrder mutations. This is so we match Increase's idempotency window. All other FRAGMENT mutations support IKs for 30 days.

e. Authentication

#

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're making. 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;
};
 

f. Match Inputs

#

When referencing an entity in FRAGMENT, the API requires you to provide a MatchInputs. This gives you the flexibility to match for an entity in multiple ways.

Each entity has a FRAGMENT-generated id which can be used in all queries.

For Ledgers, it's recommended to query with the IK's you've provided to FRAGMENT instead. This lets you not store the FRAGMENT ID of the Ledger in your system.

LedgerMatchInput
{
  "ledger": {
    "ik": "ledger-ik"
  }
}

You can also match LedgerAccounts via their path. The path is a forward-slash-delimited string containing the IK of a Ledger Account and all its direct ancestors. When querying with path, you'll also need to provide a LedgerMatchInput to identify which Ledger the Ledger Account belongs to.

LedgerAccountMatchInput
{
  "path": "parent-ik/child-ik/grandchild-ik",
  "ledger": {
    "ik": "ledger-ik"
  }
}

g. Error Handling

#

Errors are first-class citizens of our API. All mutations return a union type that represents either an error or a successful result. For example, the response type of the addLedgerEntry mutation is as follows:

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

When calling the API, your code should:

  1. Query the __typename field to see if the operation was successful.
  2. Handle non-200 responses. This typically happens due to transient errors or if your query does not conform to FRAGMENT's GraphQL spec (i.e: omitting a required variable or selecting a nonexistent field).
  3. Handle error type 200 responses. This is always a BadRequestError or InternalError.
  4. Handle success types (i.e. AddLedgerEntryResult).

h. Querying

#
Top Level Queries#

To read data, FRAGMENT exposes several top level queries. These queries support one or more lookup parameters via *Match types. These are documented in more detail in the API Reference.

Expansions#

FRAGMENT allows you to query relationships between entities via Connection types. These expansions are documented alongside the queries in the API Reference.

  • ledgerAccounts via Ledger. List Ledger Accounts in a Ledger.
  • ledgerEntries via Ledger. List Ledger Entries in a Ledger.
  • lines via LedgerAccount. List Ledger Lines in a Ledger Account.
  • lines via LedgerEntry. List Ledger Lines in a ledger entry.
  • ledgerLines via Tx. List Ledger Lines reconciled to a transaction.
  • ledgerEntries via Tx. List Ledger Entries reconciled to a transaction.

Using these connections, you can construct complex queries over the data graph stored in FRAGMENT.

i. 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 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 has 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.
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
}

This query uses pagination to retrieve 2 Ledger Accounts at a time.

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
        }
      }
    }
  }
}

This would be the response.

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

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

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>"
        }
      }
    }
  }
}

The response looks like this.

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

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

j. Filtering

#

You can filter result sets via the filter parameter on Connection types when performing list queries. The API Reference documents this in detail.

The API has several filter types built-in:

  • TypeFilters (e.g. LedgerAccountTypeFilter or TxTypeFilter)
  • DateFilter
  • DateTimeFilter
  • Boolean
  • ExternalAccountFilter
  • LedgerAccountFilter
TypeFilters#
GetLedgerAccounts query
query GetLedgerAccounts(
  $ledgerIk: SafeString!
  $accountsFilter: LedgerAccountsFilterSet!
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts(filter: $accountsFilter) {
      nodes {
        id
        name
        type
      }
    }
  }
}
GetLedgerAccounts variables, TypeFilter equalTo operator
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "type": {
      "equalTo": "asset"
    }
  }
}

To filter by the type on an Entity, send a filter argument in your query with an equalTo operator.

TypeFilter variables, in operator
{
  "accountsFilter": {
    "type": {
      "in": ["asset", "liability"]
    }
  }
}

To search for multiple types, you can use the in operator:

DateFilter#

DateFilters work similarly to TypeFilters.

GetAccountLinesOnDate query
query GetAccountLinesOnDate(
  $accountMatch: LedgerAccountMatchInput!
  $linesFilter: LedgerLinesFilterSet!
) {
  ledgerAccount(ledgerAccount: $accountMatch) {
    lines(filter: $linesFilter) {
      nodes {
        id
        posted
        date
        amount
        description
        ledgerEntryId
      }
    }
  }
}
GetAccountLinesOnDate variables, DateFilter equalTo operator
{
  "accountMatch": {
    "ledger": {
      "ik": "ik-used-to-create-ledger"
    },
    "path": "liabilities/customer-deposits/customer:123"
  },
  "linesFilter": {
    "date": {
      "equalTo": "1969-07-21"
    }
  }
}

Use the equalTo operator to query for a specific date.

DateFilter in operator
{
  "linesFilter": {
    "date": {
      "in": ["1969-07-20", "1969-07-21"]
    }
  }
}

Use the in operator to specify a set of dates.

DateTimeFilter#

To search for a date range use a DateTimeFilter.

GetLedgerEntries query
query GetLedgerEntries(
  $accountMatch: LedgerAccountMatchInput!
  $linesFilter: LedgerLinesFilterSet
) {
  ledgerAccount(ledgerAccount: $accountMatch) {
    lines(filter: $linesFilter) {
      nodes {
        id
        posted
        date
        amount
        description
        ledgerEntryId
      }
    }
  }
}
GetLedgerEntries variables, DateTimeFilter before operator
{
  "accountMatch": {
    "ledger": {
      "ik": "ik-used-to-create-ledger"
    },
    "path": "liabilities/customer-deposits/customer:123"
  },
  "linesFilter": {
    "posted": {
      "before": "1969-07-21T02:56:00.000Z"
    }
  }
}

Use the before operator to find entities that were created or posted before a certain timestamp.

DateTimeFilter after operator
{
  "linesFilter": {
    "posted": {
      "after": "1969-07-21T02:56:00.000Z"
    }
  }
}

You can also search using the after operator.

DateTimeFilter range query
{
  "linesFilter": {
    "posted": {
      "after": "1969-01-01"
      "before": "1969-12-31"
    }
  }
}

To specify a range supply both before and after. You can also use shortened dates.

Boolean#
ExternalAccountFilter GraphQL Query
query GetLinkedLedgerAccounts(
  $ledgerIk: SafeString!
  $accountsFilter: LedgerAccountsFilterSet!
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts(filter: $accountsFilter) {
      nodes {
        path
        name
        linkedAccount {
          externalId
          name
        }
      }
    }
  }
}
GetLinkedLedgerAccounts variables
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "isLinkedAccount": true
  }
}

Use the isLinkedAccount filter to find all Ledger Accounts that are linked to any External Account.

LedgerAccountFilter GraphQL Query
query GetLedgerAccounts(
  $ledgerIk: SafeString!
  $accountsFilter: LedgerAccountsFilterSet!
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts(filter: $accountsFilter) {
      nodes {
        path
        name
        parentLedgerAccount {
          path
          name
        }
      }
    }
  }
}
LedgerAccountFilter, hasParentLedgerAccount
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "hasParentLedgerAccount": true
  }
}

To filter Ledger Accounts by the presence of a parent Ledger Account use the hasParentLedgerAccount filter.

ExternalAccountFilter#

Use ExternalAccountFilter to filter Ledger Accounts by their linked External Accounts. It supports the equalTo and in operators.

ExternalAccountFilter, equalTo operator
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "linkedAccount": {
      "equalTo": {
        "linkId": "link-id",
        "externalId": "<external account id"
      }
    }
  }
}

Use the equalTo operator on the linkedAccount filter to find all Ledger Accounts that are linked to a specific External Account. The External Account can be identifed using an ExternalAccountMatchInput.

ExternalAccountFilter, in operator
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "linkedAccount": {
      "in": [
        { "id": <external account id 1> },
        { "id": <external account id 2> }
      ]
    }
  }
}

Use the in operator to find all Ledger Accounts that are linked to any of the specified External Accounts. The External Accounts can be identified using their Fragment ID instead of the external system ID.

LedgerAccountFilter#

To filter Ledger Accounts by a specific parent, send a parentLedgerAccount filter.

LedgerAccountFilter GraphQL Query
query GetLedgerAccounts(
  $ledgerIk: SafeString!
  $accountsFilter: LedgerAccountsFilterSet!
) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts(filter: $accountsFilter) {
      nodes {
        path
        name
        parentLedgerAccount {
          path
          name
        }
      }
    }
  }
}
LedgerAccountFilter, equalTo operator
{
  "ledgerIk": "ik-used-to-create-ledger",
  "filter": {
    "parentLedgerAccount": {
      "equalTo": {
        "ledger": {
          "ik": "ik-used-to-create-ledger"
        },
        "path": "liabilities/customer-deposits/customer:123"
      }
    }
  }
}

Use the equalTo operator on the parentLedgerAccount filter to find all Ledger Accounts that have a specific parent. The parent can be identified using a LedgerAccountMatchInput.

LedgerAccountFilter, in operator
{
  "ledgerIk": "ik-used-to-create-ledger",
  "filter": {
    "parentLedgerAccount": {
      "in": [
        { "id": "<parent ledger account id 1>" },
        { "id": "<parent ledger account id 2>" }
      ]
    }
  }
}

Use the in operator to find all Ledger Accounts that have any of the specified parents. The parents can be identified using their Fragment ID instead of their path.

Combined Filters#
Combined filters
{
  "ledgerIk": "ik-used-to-create-ledger",
  "accountsFilter": {
    "hasParentLedgerAccount": true,
    "type": {
      "in": ["asset", "liability"]
    }
  }
}

You can combine filters by adding multiple components to the filter block. Results are ANDed.

D Accounting#

a. Ledger

#

A Ledger is a data structure that implements double entry accounting. Ledgers have a perspective; they track the money movements for a single business entity. In FRAGMENT, you model your Ledger using a Schema.

b. Ledger Account

#

The core primitive in a Ledger is the Ledger Account. A Ledger Account tracks movements of money. Each Ledger Account encodes the entity's relationship with other entities such as their bank, customers, and vendors.

Schema#
Chart of Accounts
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
      },
      {
        "key": "debt-root",
        "name": "Debt",
        "type": "liability",
      },
    ]
  }
}

Within a schema, chartOfAccounts models the Ledger Accounts that will be created in your Ledger. The above Schema would create two Ledger Accounts: assets-root and debt-root.

Lines and Balances#

A Ledger Line records a single change to a Ledger Account. The balance of a Ledger Account is the sum of Ledger Lines posted to it.

Types#

Ledger Accounts can be split into two layers, State and Change.

Within the State and Change layer, Ledger Accounts have specific types. These types map directly to common financial reports like Balance Sheets and Income Statements.

State

Assets: what you own
Liabilities: what you owe

Change

Income: what you've earned
Expense: what you've spent
LedgerAccountSchema
{
  "chartOfAccounts": {
    "defaultCurrencyMode": "single",
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset"
      }
    ]
  },
}

In a Ledger Account Schema, use the type field to set the Ledger Account's type. A Ledger Account can only have one type and it cannot change once the Ledger Account is created.

Double Entry#

In a double-entry Ledger, the total balance of all Ledger Accounts in the State layer equals the total balance of all Ledger Accounts in the Change layer.

  • The State layer tracks the entity's net worth. The total of all the Ledger Account balances in the State layer adds up to the current net worth of the entity.
  • The Change layer tracks updates to net worth. When the total net worth changes, Ledger Accounts in the Change layer track the reason for that update.

This principle is captured in FRAGMENT's accounting equation:

By design, FRAGMENT doesn't provide an Equity account type. Liability and Equity are equivalent in the Accounting Equation, so having both types would be redundant. You can model Equity on your Ledger as a Ledger Account with type Liability. The sample Schema in the Quickstart demonstrates this.

Hierarchies#

In FRAGMENT, Ledger Accounts exist in a tree hierarchy. This enables you to incorporate the dimensionality of your data directly into the Ledger. You will typically record data at the leaf nodes, such that their balances will propagate up the tree.

Ledger Account Hierarchy
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "Customer 1",
        "type": "liability",
        "children": [
          {
            "key": "Available"
          },
          {
            "key": "Unsettled",
          },
          {
            "key": "Blocked"
          },
        ]
      }
    ]
  }

The Chart of Accounts lets you encode this hierarchy using the children field. You might use it to maintain multiple balances for a given user of your product, representing the states their funds are in. The hierarchy goes up to 10 levels deep.

c. Ledger Entry

#

A Ledger Entry is a single update to a Ledger. A Ledger Entry consists of two or more Ledger Lines. The sum of the amounts of the Ledger Lines must follow the Accounting Equation.

This is a simple Ledger Entry containing two Ledger Lines. One Ledger Line posts to an Asset account and the other posts to an Income account.

Schema#
LedgerEntrySchema
{
  "ledgerEntries": {
    "types": [
      {
        "type": "buy_materials",
        "lines": [
          {
            "key": "bank",
            "account": { "path": "assets-root/bank" },
            "amount": "-{{materials_cost}}"
          },
          {
            "key": "materials_expense",
            "account": { "path": "expense-root/materials" },
            "amount": "{{materials_cost}}"
          }
        ]
      }
    ]
  }
}

A Ledger Entry is configured with a type and can optionally define the set of Ledger Lines that it creates. The amounts are parameterized {{variables}} that you must provide to the API at runtime.

LedgerEntryInput
{
  "type": "buy_materials",
  "ledger": {
    "ik": "my-ledger-ik"
  },
  "parameters": {
    "materials_cost": "1500"
  }
}

To post this Ledger Entry, use the LedgerEntryInput type. This type is shared across both mutations that post Ledger Entries: addLedgerEntry() and reconcileTx(). The CLI also provides fragment add-ledger-entry, a convenient wrapper around addLedgerEntry.

LedgerEntrySchema with arithmetic expressions
{
  "ledgerEntries": {
    "types": [
      {
        "type": "sell_something",
        "lines": [
          {
            "key": "sales_to_bank",
            "account": { "path": "assets-root/bank" },
            "amount": "{{sales_before_tax}} + {{tax_payable}}"
          },
          {
            "key": "income_from_sales_before_tax",
            "account": { "path": "income-root/sales" },
            "amount": "{{sales_before_tax}}"
          },
          {
            "key": "tax_payable",
            "account": { "path": "debt-root/tax_payables" },
            "amount": "{{tax_payable}}
          }
        ]
      }
    ]
  }
}

For more complex Ledger Entries, you can encode arithmetic in the Schema to configure Ledger Line amounts. Currently, only + and - are supported.

d. Ledger Account Templates

#

The Chart of Accounts can include Ledger Account templates. A template is a repeated subtree of Ledger Accounts that are configured in the Schema. Templates can be useful when your Ledger models financial relationships with a repeated entity, such as users of your product.

Ledger Account Template
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "user",
        "type": "liability",
        "template": true,
        "children": [
          {
            "key": "available"
          },
          {
            "key": "unsettled"
          },
          {
            "key": "blocked"
          }
        ]
      },
      {
        "key": "bank",
        "type": "asset",
        "template": false
      }
    ]
  }
}

The template field in the Schema signifies a Ledger Account template. When the Schema is applied, this subtree is not instantiated. Instead, accounts in this subtree are created just-in-time when you first post a Ledger Entry to this part of the tree.

LedgerEntrySchema with templated Ledger Account
{
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funding",
        "lines": [
          {
            "key": "funds_arrive_in_bank",
            "account": {
              "path": "bank"
            },
            "amount": "-{{funding_amount}}"
          },
          {
            "key": "user_balance",
            "account": {
              "path": "user:{{user_id}}/available"
            },
            "amount": "{{funding_amount}}"
          }
        ]
      }
    ]
  }
}

Ledger Entries that posts to Ledger Account templates specify an identifier within the Ledger Account path. When the Ledger Entry is posted, the path is constructed with the parameters provided to the API. The Ledger Account subtree gets created if necessary.

In this example, the path for the available account for user 1234 would be liabilities-root/user:1234/available.

The templated path syntax lets you use identifiers from your system to refer to Ledger Accounts in FRAGMENT. It is also used when querying.

e. Currencies

#

Ledger Accounts in FRAGMENT support the single or multi currency mode. A Ledger Account in the single currency mode only accepts Ledger Lines of the same currency as the Ledger Account. In the multi currency mode, Ledger Lines of any currency are allowed.

FRAGMENT natively supports all ISO-4217 currencies in addition to a list of common cryptocurrencies. You can also add your own custom currencies to build brokerages, track rewards points, or ensure pre-tax and post-tax money don't mingle.

Default Currencies
{
  "chartOfAccounts": {
    "defaultCurrency": {
      "code": "USD"
    },
    "defaultCurrencyMode": "single",
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset"
      }
    ]
  }
}

In most use cases, all Ledger Accounts in a Ledger will share the same currency and currencyMode. For convenience, you can set these as defaults within Schema.

Override Currency
{
  "chartOfAccounts": {
    "defaultCurrency": {
      "code": "USD"
    },
    "defaultCurrencyMode": "single",
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "currency": {
          "code": "CAD"
        }
      }
    ]
  }
}

To override the default for a specific Ledger Account, you can set currency and currencyMode directly on that account.

LedgerEntrySchema
{
  "ledgerEntries": {
    "types": [
      {
        "type": "buy_materials",
        "lines": [
          {
            "key": "bank",
            "account": { "path": "assets-root/bank" },
            "amount": "-{{materials_cost}}",
            "currency": {
              "code": "{{expense_currency}}"
            }
          },
          {
            "key": "materials_expense",
            "account": { "path": "expense-root/materials" },
            "amount": "{{materials_cost}}",
                  "currency": {
              "code": "{{expense_currency}}"
            }
          }
        ]
      }
    ]
  }
}

You can also parameterize the currency for a Ledger Line. This is required when posting Ledger Entries to Ledger Accounts with the multi currency mode. For more on how you might use the multi currency mode, see Multi-currency.

f. Logical Clock

#

In Accounting, Ledger Entries are recorded at the time the money movement event happened. However, since there is latency in software systems, you can't post a Ledger Entry exactly when the corresponding event happens. To solve this, Ledgers use a logical clock.

Ledger Entries have two time timestamps:

  • posted: the logical time for the Ledger Entry
  • created: the system time the Ledger Entry was recorded in FRAGMENT

You provide posted in LedgerEntryInput when you call the API. FRAGMENT automatically generates created based off internal system time.

You can post Ledger Entries at any point in time, including the future. You can also query balances at any timestamp. Posting a Ledger Entry will update all balances from the posted time into the future.

g. Balances

#

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

Point-in-Time Balances#

Each Ledger Account has six balances that can be queried:

  • ownBalance The sum of all Ledger Lines in the Ledger Account, excluding Ledger Lines in child Ledger Accounts.
  • childBalance The sum of all Ledger Lines in child Ledger Accounts in the same currency as this Ledger Account.
  • balance The sum of all Ledger Lines, including child Ledger Accounts in the same currency as this Ledger Account.
  • ownBalances The sum of all Ledger Lines in the Ledger Account in all currencies, excluding Ledger Lines in child Ledger Accounts.
  • childBalances The sum of all Ledger Lines in child Ledger Accounts in all currencies.
  • balances The sum of all Ledger Lines, including child Ledger Accounts in all currencies.
GetBalances query
query GetBalances ($ledgerIk: SafeString!) {
  ledger(ledger: {ik: $ledgerIk}) {
    ledgerAccounts {
      nodes {
        id
        name
        type
        ownBalance
        childBalance
        balance
        childBalances {
          nodes {
            currency {
              code
            }
            amount
          }
        }
        balances {
          nodes {
            currency {
              code
            }
            amount
          }
        }
      }
    }
  }
}

For a Ledger with a single currency you will only use balance and childBalance. When tracking multiple currencies in the same Ledger, use balances and childBalances.

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

Balance queries
query GetBalances ($ledgerIk: SafeString!) {
  ledger(ledger: {ik: $ledgerIk}) {
    ledgerAccounts {
      nodes {
        id
        name
        type
        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 down to the hour granularity.

Balance Changes#

You can also query the net change on a Ledger Account over a specific period:

  • ownBalanceChange How much ownBalance changed.
  • childBalanceChange How much childBalance changed.
  • balanceChange How much balance changed.
  • ownBalanceChanges How much ownBalances changed.
  • childBalanceChanges How much childBalances changed.
  • balanceChanges How much balances changed.

Similar to querying point-in-time balances above, if you're doing multi-currency accounting, you'll want to use the plural 'changes' fields.

You can specify the Period as a year, quarter, month, day or hour, and make multiple queries using aliases:

Balance change queries
query GetBalanceChanges($ledgerIk: SafeString!) {
  ledger(ledger: { ik: $ledgerIk }) {
    ledgerAccounts {
      nodes {
        id
        name
        type
        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")
        childBalanceChanges(period: "1969") {
          nodes {
            currency {
              code
            }
            amount
          }
        }
        balanceChanges(period: "1969") {
          nodes {
            currency {
              code
            }
            amount
          }
        }
      }
    }
  }
}
Balances and Ledger Timezone Offsets#

Balance queries will respect the Ledger's balanceUTCOffset when specifying periods and times:

  • If a Ledger has an offset of -08:00, then querying balance(at: "1969-01-31") will return the balance at midnight PT on that date, or 8am on 1969-02-01 UTC.
  • Querying balanceChange(period: "1969") will return 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. Trust us on this one.
E Reconciliation#

c. Linked Ledger Accounts

#