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

#
Tap Homebrew Repo#
brew tap fragment-dev/tap
Install CLI#
brew install fragment-dev/tap/fragment-cli
  1. 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.

  2. Create your first Schema.

    fragment init

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

  3. 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
  4. Open a text editor to edit fragment.jsonc. On line 14, add a second child Ledger Account to assets-root.

    2nd child asset Ledger Account
       {
             "key": "reserve-bank",
             "name": "Reserve Bank",
             "children": []
       },
  5. Post a Ledger Entry from the visualizer to see how balance changes are reflected in your Chart of Accounts.

  6. Create a Ledger with your Schema.

    fragment store-schema

    This stores your Schema in FRAGMENT. Subsequent calls create a new version if the Schema changes.

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

    This creates a Ledger with your Schema. In the dashboard, you should see the created Ledger.

  7. Post a Ledger Entry via the API.

    fragment add-ledger-entry --ik "entry-ik-1" --ledger.ik "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:

  • Learn more about the FRAGMENT data model, starting with Accounting.
  • Get started developing with the API Overview.
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 is hosted at:

https://api.us-west-2.fragment.dev/graphql

And you can download the GraphQL API schema at:

https://api.us-west-2.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-mutation. If you send the same IK as input to two different mutations, FRAGMENT will execute both.
  • are also scoped per-ledger. If you send multiple requests with the same IK and same mutation operating on a single ledger, only one mutation will execute, and the isIkReplay flag will be set to true in the response. If you send a request with the same IK and same mutation that operate separately on two different ledgers, both mutations execute.
  • are valid for 30 days. If you resend an IK after 30 days, it could be executed as a fresh request.
  • Idempotency Keys 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: https://auth.fragment.dev/oauth2/token using your API client's credentials
  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 OAuth 2 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
scopehttps://api.fragment.dev/*
client_idclient ID from dashboard

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

Token request body
grant_type=client_credentials&scope=https://api.fragment.dev/*&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 https://auth.fragment.dev/oauth2/token endpoint.

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'

payload = f'grant_type=client_credentials&scope=https%3A%2F%2Fapi.fragment.dev%2F*&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.fragment.dev")
headers = {
  'Authorization': f'Basic {encoded_creds}',
  'Content-Type': 'application/x-www-form-urlencoded',
}
conn.request("POST", "/oauth2/token", 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 getToken = async () => {
  const auth = btoa(`${clientId}:${secret}`);
  const data = new URLSearchParams();
  data.append("grant_type", "client_credentials");
  data.append("scope", "https://api.fragment.dev/*");
  data.append("client_id", clientId);

  const response = await axios.request({
    method: "POST",
    url: 'https://auth.fragment.dev/oauth2/token',
    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

Here's an example query that uses pagination to retrieve 2 Ledger Accounts at a time:

GetLedgerAccounts query with pagination
query GetLedgerAccounts(
    $ledgerId: ID!
    $after: String
    $first: Int
    $before: String
  ) {
    ledger(ledger: { id: $ledgerId }) {
      id
      name
      ledgerAccounts(
        after: $after
        first: $first
        before: $before
      ) {
        nodes {
          id
          name
          type
        }
        pageInfo {
          hasNextPage
          endCursor
          hasPreviousPage
          startCursor
        }
      }
    }
  }
GetLedgerAccounts variables
{
  "ledgerId": "example-ledger-id-1",
  "first": 2
}

The response looks like this:

GetLedgerAccounts first page response
{
  "data": {
    "ledger": {
      "id": "example-ledger-id-1",
      "ledgerAccounts": {
        "nodes": [
          {
            "id": "example-ledger-account-id-1",
            "name": "Test 1",
            "type": "asset"
          },
          {
            "id": "example-ledger-account-id-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 argument:

GetLedgerAccounts next page variables
{
  "ledgerId": "ledger-id-1",
  "after": <some-end-cursor>
}

The response looks like this:

GetLedgerAccounts second page response
{
  "data": {
    "ledger": {
      "id": "example-ledger-id-1",
      "ledgerAccounts": {
        "nodes": [
          {
            "id": "example-ledger-account-id-3",
            "name": "Test 3",
            "type": "asset"
          },
          {
            "id": "example-ledger-account-id-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 argument:

GetLedgerAccounts previous page variables
{
  "ledgerId": "ledger-id-1",
  "before": <some-start-cursor>
}

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
  • ExternalAccountFilter
  • LedgerAccountFilter
TypeFilters#

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

GetLedgerAccounts query
query GetLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
  ledger(ledger: { id: $ledgerId}) {
    ledgerAccounts(filter: $filter) {
      nodes {
        id
        name
        type
      }
    }
  }
}
GetLedgerAccounts variables, TypeFilter equalTo operator
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "type": {
      "equalTo": "asset"
    }
  }
}

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

TypeFilter variables, in operator
{
  "filter": {
    "type": {
      "in": ["asset", "liability"]
    }
  }
}
DateFilter#

DateFilters work similarly to TypeFilters and allow you to query for a specific date:

GetAccountLinesOnDate query
query GetAccountLinesOnDate($ledgerAccountId: ID!, $filter: LedgerLinesFilterSet) {
  ledgerAccount(ledgerAccount: {id: $ledgerAccountId}) {
    lines(filter: $filter) {
      nodes {
        id
        posted
        date
        amount
        description
        ledgerEntryId
      }
    }
  }
}
GetAccountLinesOnDate variables, DateFilter equalTo operator
{
  "ledgerAccountId": <ledger-account-id>,
  "filter": {
    "date": {
      "equalTo": "1969-07-21"
    }
  }
}

To specify a set of dates:

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

To search for a date range use a DateTimeFilter

DateTimeFilter#

You can filter with ISO 8601 timestamps as a filter.

To find entities that were created or posted before a certain timestamp use the before operator:

GetLedgerEntries query
query GetLedgerEntries($ledgerAccountId: ID!, $filter: LedgerLinesFilterSet) {
  ledgerAccount(ledgerAccount: {id: $ledgerAccountId}) {
    lines(filter: $filter) {
      nodes {
        id
        posted
        date
        amount
        description
        ledgerEntryId
      }
    }
  }
}
GetLedgerEntries variables, DateTimeFilter before operator
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "posted": {
      "before": "1969-07-21T02:56:00.000Z"
    }
  }
}

You can also search using the after operator:

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

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

DateTimeFilter range query
{
  "filter": {
    "posted": {
      "after": "1969-01-01"
      "before": "1969-12-31"
    }
  }
}
ExternalAccountFilter#

To filter Ledger Accounts by their linked accounts, send a filter argument in your query with an equalTo, in, or isLinkedAccount operator:

ExternalAccountFilter GraphQL Query
query GetLinkedLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
  ledger(ledger: {id: $ledgerId}) {
    id
    ledgerAccounts(filter: $filter) {
      nodes {
        id
        name
        linkedAccount {
          id
          name
        }
      }
    }
  }
}
GetLinkedLedgerAccounts variables
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "isLinkedAccount": true
  }
}
ExternalAccountFilter, equalTo operator
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "linkedAccount": {
      "equalTo": {
        "id": <external account id>
      }
    }
  }
}

To search for multiple parents you can use the in operator:

ExternalAccountFilter, in operator
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "linkedAccount": {
      "in": [
        { "id": <external account id 1> },
        { "id": <external account id 2> }
      ]
    }
  }
}
LedgerAccountFilter#

To filter Ledger Accounts by the presence of a parent or a specific parent, send a filter argument in your query with an equalTo, in, or hasParentLedgerAccount operator:

LedgerAccountFilter GraphQL Query
query GetLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
  ledger(ledger: {id: $ledgerId}) {
    id
    ledgerAccounts(filter: $filter) {
      nodes {
        id
        name
        parentLedgerAccountId
        parentLedgerAccount {
          id
          name
        }
      }
    }
  }
}
LedgerAccountFilter, hasParentLedgerAccount
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "hasParentLedgerAccount": true
  }
}
LedgerAccountFilter, equalTo operator
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "parentLedgerAccount": {
      "equalTo": {
        "id": "<Ledger Account id>"
      }
    }
  }
}

To search across multiple parents you can use the in operator:

LedgerAccountFilter, in operator
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "parentLedgerAccount": {
      "in": [{ "id": "<parent ledger id 1>" }, { "id": "<parent ledger id 2>" }]
    }
  }
}
Combined Filters#

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

Combined filters
{
  "ledgerId": "ledger-id-1",
  "filter": {
    "hasParentLedgerAccount": true,
    "type": {
      "in": ["asset", "liability"]
    }
  }
}
C Accounting#

a. Ledgers

#

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 Accounts

#

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.

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.

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.

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

Default Currencies
{
  "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.

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.

c. Ledger Entries

#

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.

LedgerEntrySchema
{
  "type": "buy_materials",
  "lines": [
    {
      "key": "credit_bank_for_materials",
      "account": { "path": "assets-root/bank" },
      "amount": "-{{materials_cost}}"
    },
    {
      "key": "debit_materials_expense",
      "account": { "path": "expense-root/materials" },
      "amount": "{{materials_cost}}"
    }
  ]
}

A Ledger Entry is configured with a type and defines 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
{
  "type": "sell_something",
  "lines": [
    {
      "key": "debit_bank_from_sales",
      "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. Chart of Accounts

#

The Chart of Accounts is a data structure that models the structure of your Ledger and organizes the complexities in your flow of funds.

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.

The Chart of Accounts can also 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 an multiple semantically similar entities, such as users of your product.

Ledger Account Template
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "Customer",
        "type": "liability",
        "template": true,
        "children": [
          {
            "key": "Available"
          },
          {
            "key": "Unsettled",
          },
          {
            "key": "Blocked"
          },
        ]
      }
    ]
  }

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
{
  "type": "credit_user",
  "lines": [
    {
      "key": "funds_arrive_in_bank",
      "account": {
        "path": "assets-root/bank"
      },
      "amount": "-{{funding_amount}}"
    },
    {
      "key": "credit_user_balance",
      "account": {
        "path": "liabilities-root/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
{
  "type": "buy_materials",
  "lines": [
    {
      "key": "credit_bank_for_materials",
      "account": { "path": "assets-root/bank" },
      "amount": "-{{materials_cost}}",
      "currency": {
        "code": "{{expense_currency}}"
      }
    },
    {
      "key": "debit_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 ($ledgerId: ID!) {
  ledger(ledger: {id: $ledgerId}) {
    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.