Documentation

A Introduction#

a. Accounting

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

Ledger Accounts#

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

A Ledger Account has a balance. A Ledger Line records a single change to an account's balance. Thus, the sum of the lines in an account is the account's balance. An account's balance can be zero, positive or negative.

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

The State layer tracks the entity's net worth. The total of all the 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, accounts in the Change layer track the reason for that update.

Ledger Entries#

In double entry accounting, updates to the ledger must be balanced. These updates are called Ledger Entries: accounting movements that net to 0. Every ledger entry must

  • contain ledger lines that add up to no net change to the State layer
  • contain ledger lines that add up to an equal change in both the State and Change layers

For example, when selling a widget, the Ledger Entry would contain lines that offset the increase in a State account Bank with the increase in a Change account Widget Sales.

There can also be entries that only exist in the State layer. For example, when taking out a loan, the loaned cash in the State account Bank is offset against another State account called Loans account.

To encode more complex movements of money, Ledger Entries can contain more than two lines.

Account Types#

Within the State and Change layer, Ledger accounts have specific account types.

State

asset Assets: what you own

liability Liabilities: what you owe

Change

income Income: what you've earned

expense Expense: what you've spent

These account types enable more granular tracking than just State and Change. They are common terms used by accountants and map directly to common financial reports like Balance Sheets and Income Statements.

Account Hierarchies#

In Fragment, ledger accounts can exist in a tree hierarchy called a Chart of Accounts:

  • A parent account's balance contains the sum of all of its children's balances
  • You can post ledger entries at any layer in the tree. Fragment will update the balances of accounts higher up in the hierarchy
  • You can query for balances on any ledger account and choose whether or not to include its children

As your product evolves, the dimensionality of your data increases. With a flat list of accounts, you might add more database tables, fields, and foreign key relationships to model this complexity. Instead, with a tree data structure, you bake it into the ledger and extend your account hierarchy.

Logical Clock#

Ledgers use a logical clock, so you can post ledger entries at any point in time, including the future. You can also query balances at any point in time. Posting a ledger entry in the past will update all balances from that point in time into the future.

b. Reconciliation

#
Linked Ledger Accounts#

Linked Ledger Accounts are ledger accounts that are tied to an external account.

Any ledger line that posts to these accounts must match 1:1 with a transaction in the external account.

Transactions in the external account that aren't yet tied to a ledger entry are unreconciled transactions.

c. Payments

#
Initiating Payments#

For supported integrations, you can make payments directly from the ledger. Fragment creates transfers at the external system and updates the ledger as they're processed. Reconciliation is automatic.

Payment Orchestration#

Orders are finite-state machines that orchestrate payments. An Order's state tracks the state of its payment. An Order can be setup to post a ledger entry on each state transition. An Order can be used to handle transit accounting, payment failures and returns.

d. Currencies

#
Multiple currencies in one ledger#

To support multi-currency payments in Fragment, you can:

Fragment natively supports all ISO-4217 currencies and a list of common crypto currencies. You can add your own custom currencies for things like building brokerages, tracking rewards points or making sure pre-tax and post-tax money don't mingle.

Balance Equations#

Each ledger account has a single currency. Parent accounts with child accounts in different currencies have multiple balances, one for every currency of any of their descendant accounts.

You can think of the multiple balances on a ledger account as an equation:

100 GBP - 100 USD + 100 EUR

To solve this equation:

  1. Pick a currency as the target currency and look up its exchange rates
  2. For each balance in a non-target currency multiply it by the exchange rate to the target currency
  3. Add up the resulting balances

As you post multi-currency entries into the ledger, all the balance equations update. As exchange rates fluctuate, the solutions to balance equations update.

Tracking all stores of value#

You can create State accounts to track assets and liabilities in the currency they're denominated in. You might have several bank accounts in different currencies and have stock, crypto and private investment portfolios. Similarly, for liabilities, you might have an invoice or loan you have to pay in a specific currency, or customer portfolios on your platform across a range of assets. You can track all of these stores of value in the same ledger.

State account balance equations track the net worth of an entity, like single currency balances. Solving the balance equation gives you the current net worth of the entity in the target currency.

Exposure accounts#

Multi-currency ledger entries follow double-entry rules per currency.

To create valid ledger entries, you'll need to use Change accounts. Let's call these accounts Exposure accounts. Their purpose is to allow you to convert between currencies by tracking forex exposure.

Solving the balance equation for an Exposure Account calculates the forex gain or loss in the target currency, if you were to liquidate at the exchange rates used.

B API Basics#

a. GraphQL API

#

Fragment exposes a GraphQL API hosted at:

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

An API Explorer is available in the dashboard. This lets you hit our GraphQL API like you would using cURL or Postman for a REST API.

You can download the GraphQL API schema at:

Fragment API Schema
https://api.us-west-2.fragment.dev/schema.graphql

You can use this schema as input to various GraphQL tools for your language 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. 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 an idempotency key (or IK, for short) so that you can confidently retry operations without them running twice. IKs in Fragment:

  • ensure mutations are processed exactly-once. 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.
  • expect stable payloads. If you resend a mutation with the same IK but a different payload, you'll get a BadRequestError.
  • 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.

When generating idempotency keys, your goal should be to ensure that every operation against the Fragment API has a stable identifier. An idempotency key should be a one-way function of the underlying business-logic event.

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

d. Errors

#

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:

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).
C Setup#

a. Ledger Creation

#

The createLedger mutation creates a new Ledger.

createLedger GraphQL mutation
mutation CreateLedger (
    $ledger: CreateLedgerInput!,
    $ik: ID!
) {
  createLedger(
    ik: $ik,
    ledger: $ledger
  ) {
    __typename
    ... on CreateLedgerResult {
      ledger {
        id
        name
      }
      isIkReplay
    }
    ... on Error {
      code
      message
    }
  }
}

It has one parameter, ledger, which is a CreateLedgerInput

createLedger GraphQL query variables
{
  "ik": "some-ik",
  "ledger": {
    "name": "Api test ledger"
  }
}

The success response from the API is a CreateLedgerResult.

b. Ledger Timezones

#

To specify a timezone for queries to your ledger, pass in the optional field balanceUTCOffset to the ledger parameter on createLedger. The default timezone is UTC.

CreateLedgerInput with balanceUTCOffset
{
  "ik": "some-ik",
  "ledger": {
    "name": "Test ledger",
    "balanceUTCOffset": "-08:00"
  }
}

In this example, the ledger's day starts at 8am UTC. Balance queries against this ledger will consider the start of each local day to be at 8am the next day in UTC. See balances for more info on querying balances.

All whole-number UTC offsets (-11:00 to +12:00) are supported. Here are a few common examples:

  • -08:00 for Pacific Time
  • -07:00 for Mountain Time
  • -06:00 for Central Time
  • -05:00 for Eastern Time
  • +00:00 for UTC / GMT

c. Account Creation

#

You can use the createLedgerAccounts mutation to create ledger accounts.

createLedgerAccounts GraphQL mutation
mutation CreateLedgerAccounts (
  $ledger: LedgerMatchInput!,
  $ledgerAccounts: [CreateLedgerAccountsInput!]!,
) {
  createLedgerAccounts(
    ledger: $ledger,
    ledgerAccounts: $ledgerAccounts
  ) {
    __typename
    ... on CreateLedgerAccountsResult {
      ledgerAccounts {
        id
        name
        type
        ik
        currency {
          code
          customCurrencyId
        }
      }
      ikReplays {
        ik
        isIkReplay
      }
      
    }
    ... on Error {
      code
      message
    }
  }
}

It has two parameters:

createLedgerAccounts GraphQL query variables
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "ledgerAccounts": [
    {
      "ik": "assets",
      "name": "All assets",
      "type": "asset"
    },
    {
      "ik": "liabilities",
      "name": "All liabilities",
      "type": "liability"
    }
  ]
}

The type can be asset, liability, income, or expense.

Unlike other mutations that take a single IK, createLedgerAccounts accepts an IK for each of the ledger accounts in the request payload. This is so you can recover in the case of a partial failure. You can use the IK to map accounts in the mutation input to their counterparts in the response.

The API creates up to 200 accounts in a single request.

d. Ledger Account Hierarchies

#

Ledger accounts are organized in a tree-based hierarchy. Unlike a flat-list of ledger accounts, this tree structure lets you encode the dimensionality of your product's data directly into the ledger. Account balances automatically aggregate up the hierarchy so that each node contains the balances of its subtree. See Querying Balances.

You can create hierarchies of ledger accounts in a single API call during a call to createLedgerAccounts by passing in the childLedgerAccounts field.


createLedgerAccounts with child accounts
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "ledgerAccounts": [
    {
      "ik": "assets-root",
      "name": "All assets",
      "type": "asset",
      "childLedgerAccounts": [
        {
          "ik": "assets-receivables",
          "name": "All customer receivables",
          "childLedgerAccounts": [
            {
              "ik": "assets-receivables-customer-1",
              "name": "ACME receivables"
            },
            {
              "ik": "assets-receivables-customer-2",
              "name": "Generic Corp receivables"
            }
          ]
        }
      ]
    }
  ]
}

You can nest ledger accounts up to 10 levels, and you only have to specify the type on the top level account, since all of its child accounts will be of the same type.

Top level accounts in the payload can also specify a parent ledger account to create a hierarchy under that account. Child accounts have the same account type as their parent, so type doesn't need to be set.

createLedgerAccounts parent flag
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "ledgerAccounts": [
    {
      "ik": "assets-receivables",
      "name": "All customer receivables",
      "parent": {
        "id": "assets-root"
      },
      "childLedgerAccounts": [
        {
          "ik": "assets-receivables-customer-1",
          "name": "ACME receivables"
        },
        {
          "ik": "assets-receivables-customer-2",
          "name": "Generic Corp receivables"
        }
      ]
    }
  ]
}

e. Account Currencies

#

To set the currency of a ledger account, pass in the currency field. If not specified, the default workspace account currency will be used.

createLedgerAccounts with currency
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "ledgerAccounts": [
    {
      "ik": "usd-operating-account",
      "name": "USD Operating Account",
      "currency": {
        "code": "USD"
      },
      "type": "asset"
    },
    {
      "ik": "gbp-operating-account",
      "name": "GBP Operating Account",
      "currency": {
        "code": "GBP"
      },
      "type": "asset"
    }
  ]
}

The currency field is a CurrencyCode enum type which contains:

  • fiat currencies
  • common cryptocurrencies
  • LOGICAL
  • CUSTOM

LOGICAL is used for accounts that contain multiple currencies via children. You typically wouldn't post ledger entries to these accounts.

createLedgerAccounts LOGICAL
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "ledgerAccounts": [
    {
      "ik": "multi-currency-cash-balance",
      "name": "Global Operating Accounts",
      "type": "asset",
      "currency": {
        "code": "LOGICAL"
      },
      "childLedgerAccounts": [
        {
          "ik": "usd-operating-account",
          "name": "USD Operating Account",
          "currency": {
            "code": "USD"
          },
        },
        {
          "ik": "gbp-operating-account",
          "name": "GBP Operating Account",
          "currency": {
            "code": "GBP"
          },
        }
      ]
    }
  ]
}

CUSTOM lets you define a currency yourself. If currency is set to CUSTOM, customCurrencyId must be set in order to identify the currency.

createLedgerAccounts with currency
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "ledgerAccounts": [
    {
      "ik": "customer-123-portfolio",
      "name": "Customer 123 Portfolio",
      "type": "liability",
      "currency": {
        "code": "LOGICAL"
      },
      "childLedgerAccounts": [
        {
          "ik": "customer-123-appl-shares",
          "name": "Customer 123 AAPL",
          "currency": {
            "code": "CUSTOM",
            "customCurrencyId": "AAPL"
          },
        },
        {
          "ik": "customer-123-tsla-shares",
          "name": "Customer 123 TSLA",
          "currency": {
            "code": "CUSTOM",
            "customCurrencyId": "TSLA"
          },
        },
        {
          "ik": "customer-123-usd",
          "name": "Customer 123 USD",
          "currency": {
            "code": "USD"
          },
        }
      ]
    }
  ]
}

f. Linked Ledger Accounts

#

You can link ledger accounts within Fragment to accounts at an external system by passing in the linkedAccount parameter when calling createLedgerAccounts.

createLedgerAccounts linked accounts
{
  "ledger": {
    "id": "ledger id"
  },
  "ledgerAccounts": [
    {
      "ik": "assets-cash",
      "name": "Cash",
      "childLedgerAccounts": [
        {
          "ik": "bank-account-1",
          "name": "Operating account",
          "linkedAccount": {
            "linkId": "fragment increase link id",
            "externalId": "increase api bank account id"
          }
        },
        {
          "ik": "bank-account-2",
          "name": "Savings account",
          "linkedAccount": {
            "linkId": "fragment custom link id",
            "externalId": "bank account 2 id"
          }
        }
      ]
    }
  ]
}

The linkId is the ID of the link to the external system. This can be retrieved from the Fragement dashboard.

Every line in a Linked Ledger Account must correspond to a Transaction in the External Account it's linked to.

D Operate#

a. Making Payments

#

You can initiate a payment and post its associated ledger entry in the same API call:

makeBankTransfer mutation
mutation MakeBankTransfer ($bankTransfer: MakeBankTransferInput!, $ik: ID!) {
  makeBankTransfer(bankTransfer: $bankTransfer, ik: $ik) {
    __typename
    ... on MakeBankTransferResult {
      bankTransfer {
        id
        description
        status
      }
      isIkReplay
    }
    ... on Error {
      code
      message
    }
  }
}
makeBankTransfer variables
{
  "ik": "some-ik",
  "description": "Pay Loan #123 Principal",
  "bankTransfer": {
    "amount": "-100000",
    "payment": {
      "type": "wire",
      "source": {
        "routingNumber": "021000021",
        "accountNumber": "123456789"
      }
    },
    "linkedLedgerAccount": {
      "id": "<linked ledger account id>"
    },
    "offset": {
      "lines": [
        {
          "amount": "100000",
          "account": {
            "id": "Loan #123 Balance"
          }
        }
      ]
    }
  }
}

This mutation will:

  • initiate the wire transfer at the bank
  • track the transfer's processing status
  • post a ledger entry when the transfer settles

The example above uses Bank Transfers. Bank Transfers are an abstraction over Orders that only support posting ledger entries when a payment succeeds. As a result, one caveat is that IK's are shared between makeBankTransfer and createOrder. See Idempotency for more details.

To handle payment transit accounting, failures and returns, use the createOrder mutation. Orders expose the full state machine of a payment and allow you to post ledger entries on state machine transitions.

b. Reconciling Transactions

#

To maintain a 1:1 mapping between external systems and Fragment, payments made outside of Fragment need to be reconciled.

Querying unreconciled transactions#

You can query which transactions still need to be reconciled to a Linked Ledger Account using the unreconciledTxs query:

unreconciledTxs query
query GetUnreconciledTxs ($ledgerAccountId: ID!) {
  ledgerAccount(id: $ledgerAccountId) {
    id
    unreconciledTxs {
      nodes {
        id
        description
        amount
        externalId
        externalAccountId
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}
unreconciledTxs variables
{
  "id": "<linked ledger account id>"
}

unreconciledTxs returns a TxsConnection object which is a paginated list of transactions

Reconciling a transaction#

To reconcile transactions from external payment systems, use the reconcileTx mutation. This mutation pulls in a transaction from the transaction store to the ledger and adds a balanced entry with a line matching the transaction amount to a linked account.

reconcileTx mutation
mutation ReconcileTx ($entry: LedgerEntryInput!) {
  reconcileTx(entry: $entry) {
    __typename
    ... on ReconcileTxResult {
      entry {
        id
        date
        description
      }
      lines {
        id
        date
        amount
        type
      }
    }
    ... on Error {
      code
      message
    }
  }
}

This mutation has one parameter, entry which has the same LedgerEntryInput type as other mutations. It uses the tx field to specify the transaction to reconcile:

reconcileTx variables
{
  "entry": {
    "lines": [
      {
        "account": {
          "id": "linked-ledger-account-id"
        },
        "tx": {
          "id": "tx-id-from-unreconciled-txs-query-above"
        }
      },
      {
        "account": {
          "id": "offset-ledger-account-id"
        }
      }
    ]
  }
}

The result will be a ledger entry posted at the transaction posted timestamp that contains:

  • A line in the linked ledger account that where the amount and description matches the external transaction
  • A line in the other specified ledger account where the description matches the external transaction, but the amount is negative
More complex ledger entries#

You can also do more complex reconciliation where the linked ledger line is balanced by multiple other lines and override descriptions:

reconcileTx variables
{
  "entry": {
    "lines": [
      {
        "account": {
          "id": "linked-ledger-account-id"
        },
        "tx": {
          "id": "tx-id-from-unreconciled-txs-query-above"
        },
        "amount": "10000"
      },
      {
        "account": {
          "id": "seller-2-accounts-payable"
        },
        "description": "Item #2234 sold",
        "amount": "8500"
      },
      {
        "account": {
          "id": "shipper-accounts-payable"
        },
        "description": "Item #2234 shipping fees",
        "amount": "1000"
      },
      {
        "account": {
          "id": "platform-fees-income-account"
        },
        "description": "Item #2234 platform fees",
        "amount": "500"
      }
    ]
  }
}
Using externalId to reconcile transactions#

You can specify the transaction to reconcile using the ID from the external system API instead of the Fragment ID:

reconcileTx variables
{
  "entry": {
    "lines": [
      {
        "account": {
          "id": "linked-ledger-account-id"
        },
        "tx": {
          "externalId": "increase-api-tx-id"
        }
      },
      {
        "account": {
          "id": "offset-ledger-account-id"
        }
      }
    ]
  }
}
Idempotency#

reconcileTx doesn't require an idempotency key. Internally, Fragment creates a composite IK from:

  • the ID of the transaction, and
  • ID of the ledger being reconciled
Limitations#
  • You can only reconcile a single transaction per call
  • If you are reconciling a transfer between two external accounts which are both linked to the same ledger, use a transit account in between to split the transfer into two reconcileTx calls.

c. Adding Ledger Entries

#

You can add ledger entries that aren't attached to an external transaction via the addLedgerEntry mutation. These are called "logical" ledger entries.

addLedgerEntry mutation
mutation AddLedgerEntry($entry: LedgerEntryInput!, $ik: ID!) {
  addLedgerEntry(entry: $entry, ik: $ik) {
    __typename
    ... on AddLedgerEntryResult {
      entry {
        id
        date
        description
      }
      lines {
        id
        date
        description
        amount
        type
      }
    }
    ... on Error {
      code
      message
    }
  }
}

addLedgerEntry accepts an entry of type LedgerEntryInput that contains the ledger entry's ledger lines.

addLedgerEntry variables
{
  "ik": "some-ik",
  "entry": {
    "posted": "2020-01-01",
    "description": "hello",
    "lines": [
      {
        "amount": "100",
        "account": {
          "id": "ledger-account-id"
        }
      },
      {
        "amount": "100",
        "account": {
          "id": "ledger-account-id"
        }
      }
    ]
  }
}

Ledger entries have a limit on the number of lines they can contain. The limit is dependent on the number of ledger accounts and lines within them. See Ledger Entry Size.

d. Multi-currency Entries

#

A ledger entry can contain lines across multiple currencies. It must be balanced within each currency.

LedgerEntryInput with multiple currencies
{
  "ik": "some-ik",
  "entry": {
    "posted": "2020-01-01",
    "description": "example multi currency ledger entry",
    "lines": [
      {
        "amount": "100",
        "account": {
          "id": <some-usd-ledger-account-id>
        }
      },
      {
        "amount": "100",
        "account": {
          "id": <another-usd-ledger-account-id>
        }
      }
      {
        "amount": "105",
        "account": {
          "id": <some-gbp-ledger-account-id>
        }
      },
      {
        "amount": "105",
        "account": {
          "id": <another-gbp-ledger-account-id>
        }
      }
    ]
  }
}
E Update#

a. Updating Ledgers

#

Use the updateLedger mutation to update properties on an existing Ledger. Currently, only name is supported.


updateLedger GraphQL mutation
mutation UpdateLedger($ledger: LedgerMatchInput!, $update: UpdateLedgerInput!) {
  updateLedger(ledger: $ledger, update: $update) {
    __typename
    ... on UpdateLedgerResult {
      ledger {
        id
        name
      }
    }
    ... on Error {
      code
      message
    }
  }
}

updateLedger GraphQL parameters
{
  "ledger": {
    "id": <ledgerId from createLedger call>
  },
  "update": {
    "name": "Fresh new name"
  }
}

b. Updating Ledger Accounts

#

Use the updateLedgerAccount mutation to update properties on an existing ledger account. Currently, only name is supported.


updateLedgerAccount GraphQL mutation
mutation UpdateLedgerAccount(
  $ledgerAccount: LedgerAccountMatchInput!
  $update: UpdateLedgerAccountInput!
) {
  updateLedgerAccount(ledgerAccount: $ledgerAccount, update: $update) {
    __typename
    ... on UpdateLedgerAccountResult {
      ledgerAccount {
        id
        name
      }
    }
    ... on Error {
      code
      message
    }
  }
}

updateLedgerAccount GraphQL parameters
{
  "ledgerAccount": {
    "id": <ledgerAccount.id from createLedgerAccounts call>
  },
  "update": {
    "name": "Fresh new name"
  }
}
H Limits#

a. Ledger Entry Size

#

Ledger entries are limited to 40 units, where units are calculated with the following formula:

3 base units + 1 unit per line + 2 units per ledger account

So a basic ledger entry with two lines on two different accounts would be:

3 base + 2 for lines + 4 for accounts = 9 units

If you're only affecting two ledger accounts, you can include 33 lines, but if every line is for a different ledger account, you can send only 12 lines.

b. Increase Transfers

#

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.

c. Ledger Account Creation

#

When bulk creating ledger accounts using createLedgerAccounts, you can create up to 200 accounts up to 10 levels deep in a single API call.

I Querying#

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

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

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

d. 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"]
    }
  }
}

e. Balances

#

Fragment supports querying a ledger account for its balances, historical balances, and balance changes.

Ledger Account Balances#

Each Ledger Account has five balances that can be queried:

  • ownBalance The sum of all ledger lines in the account, excluding ledger lines in child ledger accounts.
  • childBalance The sum of all ledger lines in child ledger accounts in the account's currency
  • balance The sum of all ledger lines, including child ledger accounts in the account's currency.
  • childBalances The sum 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
          }
        }
      }
    }

}
}

You will likely only use balance and childBalance. If you're tracking multiple currencies in the same ledger, use balances and childBalances.

You can query the balance at a particular point in time using an at argument:

Balance queries
query GetBalances ($ledgerId: ID!) {
  ledger(ledger: {id: $ledgerId}) {
    ledgerAccounts {
      nodes {
        id
        name
        type
        end_of_year: balance(at: "2021")
        end_of_month: balance(at: "2021-06")
        end_of_day: balance(at: "2021-06-15")
        end_of_hour: balance(at: "2021-06-15T06")
      }
    }
  }
}

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.

Ledger Account 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.
  • childBalanceChanges How much childBalances changed.
  • balanceChanges How much balances changed.

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

Balance change queries
query GetBalanceChanges($ledgerId: ID!) {
  ledger(ledger: {id: $ledgerId}) {
    ledgerAccounts {
      nodes {
        id
        name
        type
        ownBalanceChange(period: "2021")
        childBalanceChange(period: "2021")
        balanceChange(period: "2021")
        
        currentYear: balanceChange(period: "2021")
        lastYear: balanceChange(period: "2020")
        lastYearQ4: balanceChange(period: "2020-Q4")
        lastYearDecember: balanceChange(period: "2020-12")
        lastYearChristmas: balanceChange(period: "2020-12-25")
        lastYearLastHour: balanceChange(period: "2020-12-31T23")
        
        childBalanceChanges(period: "2021") {
          nodes {
            currency {
              code
            }
            amount
          }
        }
        balanceChanges(period: "2021") {
          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: "2021-01-31") will return the balance at midnight PT on that date, or 8am on 2021-02-01 UTC.
  • Likewise, querying balanceChange(period: "2021") will return the net change between 8am on 2021-01-01 UTC and 8am on 2022-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.

f. Generating Reports

#

Fragment supports arbitary querying with pagination and filtering, so you can use the API to pull data for custom reports as you wish. We'll walk you through how to query a ledger for two common financial reports: Balance Sheets and Income Statements.

Balance Sheets#

To generate a Balance Sheet, query the balances on State accounts. Remember, we can add the at parameter to balance to get the Balance Sheet at a given point in time.

Balance Sheet query
query GetBalanceSheet ($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
  ledger(ledger: {id: $ledgerId}) {
    ledgerAccounts(filter: $filter) {
      nodes {
        id
        name
        type
        balance
      }
    }
  }
}
Balance Sheet parameters
{
  "ledgerId": <some-ledger-id>,
  "filter": {
    "type": {
      "in": ["asset", "liability"]
    }
  }
}
Income Statements#

To generate an Income Statement, query balance changes on Change accounts.

Income Statement query
query GetIncomeStatement ($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
  ledger(ledger: {id: $ledgerId}) {
    ledgerAccounts(filter: $filter) {
      nodes {
        id
        name
        type
        balanceChange(period: "1969")
      }
    }
  }
}
Income Statement parameters
{
  "ledgerId": <some-ledger-id>,
  "filter": {
    "type": {
      "in": ["income", "expense"]
    }
  }
}
Monthly Account Statements#

To generate a monthly Account Statement, query the balances. Since the date provided to at in balance is inclusive, the last day of the previous month is used to query startingBalance. For example, the balance at the end of May, inclusive of entries posted on May 31st, should be the starting balance for June.

Monthly Account Statement query
query GetAccountStatement ($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
  ledger(ledger: {id: $ledgerId}) {
    ledgerAccounts(filter: $filter) {
      nodes {
        id
        name
        type
        startingBalance: balance(at: "1969-05-31")
        endingBalance: balance(at: "1969-06-30")
      }
    }
  }
}
Monthly Account Statement variables
{
  "ledgerId": <some-ledger-id>,
  "filter": {
    "type": {
      "in": ["asset", "liability"]
    }
  }
}
J Data Export#

Fragment supports exporting ledger data to external sources, which can then be used for analytics, regulatory compliance, and ingestion into other third party systems. Currently, Fragment only supports exporting data to Amazon S3.

a. Creating a Data Export

#

To onboard a Data Export, first create an S3 bucket within your AWS account. No special settings are required, but take note of the bucket name & AWS region for later use.

Once the bucket is prepared, navigate to the Settings -> S3 Exports subsection of the Fragment dashboard, and follow the instructions to create a new export. You will need the following information:

  • S3 Bucket Name: the name of the bucket created in the previous step (e.g. fragment-exports).
  • S3 Bucket Region: the region of the bucket created in the previous step (e.g. us-east-1).
  • Export Name: a name for this export, only used for your own identification (e.g. analytics). This name is only used for display purposes.
  • File Prefix (optional): the path within which exports will be stored. If not provided, exports will be stored in the bucket root. Typically, this is only needed if the bucket is to be used for multiple purposes. For example, if multiple workspaces will export to the same bucket, it may be useful to prefix by a workspace identifier.

Note that the instructions include applying an IAM policy to your bucket. Depending on how you manage your infrastructure, this may take some time to apply. In this case, the setup flow can be restarted at any time.

b. Testing a Data Export

#

Once the bucket name and region are provided, the dashboard setup flow allows for testing the export connection by writing a sample file to test/testLedgerEntry.part within your bucket. This may result in one of several outcomes:

  • Policy Not Applied means that Fragment received an authorization error, which typically means the provided IAM policy has not yet been applied within your AWS account. Less commonly, it may also mean the provided bucket name and/or region differ from the one created in the initial setup.
  • Invalid Bucket Name means that the provided bucket name does not exist within any AWS account.
  • Incorrect AWS Region means that the provided bucket name exists, but within a different region than provided.
  • Verified Permissions means that the file was successfully written.

Depending on your bucket usage, you may wish to remove the test file after verifying its existence. Fragment will not do this for you.

c. Configuration

#

By default, exports are delivered approximately every 5 minutes. Each export contains the ledger data created or updated over the previous period. They are divided into three types of files containing newline-separated JSON. Each line represents an individual instance of the respective data type:

LedgerEntry#

key: {export name}/LedgerEntry/day={day}/hour={hour}/{partition}.part

Download Example File

LedgerLine#

key: {export name}/LedgerLine/day={day}/hour={hour}/{partition}.part

Download Example File

LedgerAccount#

key: {export name}/LedgerAccount/day={day}/hour={hour}/{partition}.part

Download Example File

K Changelog#
2022-11-30

Ledger-specific IKs for Orders and Entries

IKs provided to createOrder, addLedgerEntry, and makeBankTransfer are now automatically scoped to their parent ledgers. See Idempotency for details.

2022-11-14

Data Export

You can now export ledger data to Amazon S3 for analytics, compliance, ingestion into other third party systems, and other use cases. To set this up, go to the settings tab in the dashboard. See Data Export for details.

2022-10-31

Querying Multiple Ledger Accounts

You can now query for multiple ledger accounts by ID in the same query. To do this, pass the ledgerAccount filter and use the in operator. See LedgerAccountsFilterSet for details.

2022-10-11

Backwards Pagination

Connections now return hasPreviousPage and startCursor. To page backwards, pass startCursor to the before argument on a list query. See Pagination for details.