Documentation

A Quickstart#

a. Overview

#

This Quickstart will introduce you to the FRAGMENT development cycle.

b. Prerequisites

#
  • A FRAGMENT Workspace
  • Node version 16 or higher installed locally
  • Homebrew

c. Steps

#
  1. Setup the CLI

    Tap Homebrew Repo

    brew tap fragment-dev/tap

    Install CLI

    brew install fragment-dev/tap/fragment-cli

  2. Authenticate your local environment to your Workspace.

    fragment login

    Create a new API Client. Paste the Client ID , Secret Key, API URL, and OAuth URL into the CLI.

  3. Create your first Schema.

    fragment init

    This will write an example Schema to ./fragment.jsonc

  4. Start the local development environment. This will open a schema visualizer in your browser. The visualizer automatically updates every time the file changes.

    fragment start
  5. Open a text editor to edit fragment.jsonc. Under the Ledger Account with key assets-root, add a second child Ledger Account adjacent to the Ledger Account with key bank:

    2nd child asset Ledger Account
       {
             "key": "reserve-bank",
             "name": "Reserve Bank",
             "children": []
       }
  6. Post a Ledger Entry from the localhost Schema visualization to see how balance changes are reflected in your Chart of Accounts. This is an in-memory playground you can use to iterate quickly on your Schema.

  7. Store your Schema in FRAGMENT. The storeSchema mutation is an upsert, only creating a new version if the Schema changes. Additionally, it will throw a BadRequestError if you attempt to make backwards incompatible Schema changes, such as removing a Ledger Account.

    fragment store-schema
  8. Create a Ledger with your Schema. You can have multiple Ledgers using the same Schema. When you update your Schema with the storeSchema mutation, every Ledger associated with it is automatically migrated to the latest version.

    fragment create-ledger \
         --name "sample ledger" \
         --ik "ledger-1"

    In the dashboard, you should see the created Ledger.

  9. Post a Ledger Entry via the API.

    fragment add-ledger-entry \
         --ik "entry-1" \
         --ledger.ik "ledger-1" \
         --type sell_something \
         --param "sales_before_tax=10000" \
         --param "tax_payable=500"

    This posts an entry of type sell_something by calling the addLedgerEntry mutation. The other mutations that post Ledger Entries are reconcileTx, createOrder, and makeBankTransfer.

Next:

B API Overview#

a. Workflow

#

The FRAGMENT workflow is:

  • Ledger: a data structure modeling funds for a single entity. Ledgers are created using the createLedger mutation. The FRAGMENT CLI provides a convenient wrapper around this mutation. If your use case requires you to manage multiple Ledgers, call the createLedger mutation directly from your application.
  • API: data is created in a Ledger via the GraphQL API. Use addLedgerEntry to post logical Ledger Entries, reconcileTx to reconcile transactions from an external system to your Ledger, and createOrder or makeBankTransfer to make payments. See Accounting, Reconciliation, and Payments to learn more about the API.
  • Schema: a JSON-based declarative source of truth for the structure of your Ledger. In your CI or deployment system, use the CLI to run fragment store-schema. This creates or updates your Schema like a database migration. When this is run, FRAGMENT automatically migrates all Ledgers using the Schema with the updated Ledger Accounts and Ledger Entry types.

The API URL for your environment can be found in the API Clients page of 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.

b. 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.

c. 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.

d. Authentication

#
Flow#

FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The flow is:

  1. Get a fresh access token from the token endpoint using your API client's credentials. You can find the endpoint URL and OAuth scope in the Dashboard's API Clients page.
  2. Use that token in an Authorization header with the value Bearer {{access_token}} when calling the GraphQL API.

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.

You can create and view API Clients from the FRAGMENT dashboard. All API clients have full access to your workspace.

The call to get an access token follows the OAuth2 spec, so you can use an OAuth2 library that supports the client credentials grant to retrieve the token, or make an HTTP request.

Token Request Body#
NameValue
grant_typeclient_credentials
scope{{scope}} from dashboard
client_id{{client_id}} from dashboard

Your payload should be in the x-www-form-urlencoded format:

Token request body
grant_type=client_credentials&scope={{scope}}&client_id={{client_id}}
Token Request Headers#
NameValue
Content-Typeapplication/x-www-form-urlencoded
AuthorizationBasic {{client_credentials}} where client_credentials is the base 64 encoded version of: {{client_id}}:{client_secret}}

The response will be a JSON Object containing your access token.

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

Here are some code snippets that show how to hit the endpoint. Your specific OAuth URL and scope may vary depending on which region your workspace is in. You can find these values in the the dashboard once you have created your API Client.

Curl
curl --location --request POST 'https://auth.fragment.dev/oauth2/token' \
  --header 'Authorization: Basic Mmg0dDBjdjdxdjVjOXIwcWdzMW8zZXJrdWs6c3VwZXJTZWNyZXRDbGllbnRTZWNyZXQ=' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=https://api.fragment.dev/*' \
  --data-urlencode 'client_id=2h4t0cv7qv5c9r0qgs1o3erkuk'
 
Python
import http.client
import b64encode from base64

client_id = '2h4t0cv7qv5c9r0qgs1o3erkuk'
client_secret = 'superSecretClientSecret'
scope = 'https://api.fragment.dev/*
auth_url = 'https://auth.fragment.dev/oauth2/token'

payload = f'grant_type=client_credentials&scope={scope}&client_id={client_id}'
client_credentials = f'{client_id}:{client_secret}'
encoded_creds = (
  b64encode(client_credentials.encode('ascii'))
  .decode('ascii')
)

conn = http.client.HTTPSConnection(auth_url)
headers = {
  'Authorization': f'Basic {encoded_creds}',
  'Content-Type': 'application/x-www-form-urlencoded',
}
conn.request("POST", endpoint, payload, headers)

data = conn.getresponse().read()
print(data.decode("utf-8"))
 
Javascript
import axios from "axios";

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

const clientId = "2h4t0cv7qv5c9r0qgs1o3erkuk";
const secret = "superSecretClientSecret";
const scope = "https://api.fragment.dev/*";
const authUrl = "https://auth.fragment.dev/oauth2/token";

const getToken = async () => {
  const auth = btoa(`${clientId}:${secret}`);
  const data = new URLSearchParams();
  data.append("grant_type", "client_credentials");
  data.append("scope", scope);
  data.append("client_id", clientId);

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

e. 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.

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

f. 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).

g. 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.

h. 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.

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

C 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

asset Assets: what you own

liability Liabilities: what you owe

Change

income Income: what you've earned

expense 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 all the mutations that post Ledger Entries: addLedgerEntry(), reconcileTx(), makeBankTransfer(), and createOrder(). The CLI also provides fragment add-ledger-entry, a convenient wrapper around addLedgerEntry.

LedgerEntrySchema with math
{
  "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.
D Reconciliation#