Documentation
a. Accounting
#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.
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.
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.
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.
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.
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
#Links represent an external system that moves money. Links can be any store of value be it banks, brokerages, card processors, crypto wallets, rewards points, or loan issuers.
Links contain External Accounts, which represent a single account at the external system. External Accounts have a transaction store, a log of all transactions in the external account.
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.
Native Links implement a full two-way sync between FRAGMENT and the external system. Transactions are automatically imported into the transaction store.
With Native Links, the reconciliation workflow is:
- External accounts and transactions are imported automatically by FRAGMENT
- Create Ledger Accounts that are linked to External Accounts
- Track unreconciled transactions
- Reconcile imported transactions into a ledger
Custom Links provide APIs for you to integrate with any external system and import transactions into FRAGMENT. These external systems are typically 3rd party payment processors (banks, card issuers, etc.) that you work with. In certain use cases, it's also valuable to model a 1st party system you maintain as a Custom Link to ensure it and FRAGMENT stay in sync.
With a Custom Link, the reconciliation workflow is:
- Import accounts and transactions from the external system via FRAGMENT's API
- Create Ledger Accounts that are linked to Custom Accounts
- Track unreconciled transactions
- Reconcile imported transactions into a ledger
c. 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.
d. Multi-Currency
#Ledger accounts in FRAGMENT support the single
or multi
currency mode. When you create an account in the multi
currency mode, you can post ledger entries with lines in different currencies to the same account.
FRAGMENT natively supports all ISO-4217 currencies and a list of common crypto currencies. You can also add your own custom currencies to build brokerages, track rewards points or make sure pre-tax and post-tax money don't mingle.
When an account is in currency mode multi
or has children with balances in multiple currencies, the account will have multiple balances. You can think of the multiple balances on a ledger account as an equation:
100 GBP - 100 USD + 100 EUR
To solve this equation:
- Pick a currency as the target currency and look up its exchange rates
- For each balance in a non-target currency multiply it by the exchange rate to the target currency
- 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.
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.
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.
a. GraphQL API
#FRAGMENT exposes a GraphQL API hosted at:
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:
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. IK
s 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 theisIkReplay
flag set to true. - expect stable payloads. If you resend a mutation with the same
IK
but a different payload, you'll get aBadRequestError
. - 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 theisIkReplay
flag will be set totrue
in the response. If you send a request with the sameIK
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. - must be SafeStrings
When generating idempotency keys, your goal should be to ensure that every operation against the FRAGMENT API has a stable identifier.
Additionally, some IK
's (and their derivatives such as ikPath
) are used as secondary unique indexes for querying. An idempotency key should be a one-way function of the underlying business-logic event. You don't need to store FRAGMENT ID's in order to query Ledgers and Ledger Accounts in FRAGMENT.
c. Authentication
#FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The flow is:
- Get a fresh access token from the token endpoint:
https://auth.fragment.dev/oauth2/token
using your API client's credentials - Use that token in an
Authorization
header with the valueBearer {{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.
Name | Value |
---|---|
grant_type | client_credentials |
scope | https://api.fragment.dev/* |
client_id | client ID from dashboard |
Your payload should be in the x-www-form-urlencoded
format:
grant_type=client_credentials&scope=https://api.fragment.dev/*&client_id={{client_id}}
Name | Value |
---|---|
Content-Type | application/x-www-form-urlencoded |
Authorization | Basic {{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.
{
"access_token": "<access token>",
"expires_in": 3600,
"token_type": "Bearer"
}
Here are some code snippets that show how to hit the https://auth.fragment.dev/oauth2/token
endpoint.
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'
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"))
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:
- Query the
__typename
field to see if the operation was successful. - 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).
- Handle error type 200 responses. This is always a
BadRequestError
orInternalError
. - Handle success types (i.e.
AddLedgerEntryResult
).
a. Ledger Creation
#The createLedger
mutation creates a new Ledger.
mutation CreateLedger (
$ledger: CreateLedgerInput!,
$ik: SafeString!
) {
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
{
"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.
{
"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.
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:
ledger
is aLedgerMatchInput
and specifies the ledger the account will be created inledgerAccounts
is aCreateLedgerAccountInput[]
and specifies the account details.
{
"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.
{
"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.
{
"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, first decide if it's a single or multi currency account. If a single currency account, the account only accepts ledger lines in that currency. If a multi-currency account, the account accepts lines in multiple currencies.
To create a multi-currency account, create an account with currencyMode
set to multi
:
{
"ledger": {
"id": <ledgerId from createLedger call>
},
"ledgerAccounts": [
{
"ik": "operating-account",
"name": "Operating Account",
"currencyMode": "multi",
"type": "asset"
},
]
}
To create a single currency account, set currencyMode
to single
and set the currency
field to specify the currency, This is a CurrencyCode
enum type which contains: fiat currencies, common cryptocurrencies, LOGICAL
, and CUSTOM
.
{
"ledger": {
"id": <ledgerId from createLedger call>
},
"ledgerAccounts": [
{
"ik": "gbp-currency-cash-balance",
"name": "GBP Operating Accounts",
"type": "asset",
"currencyMode": "single",
"currency": {
"code": "GBP"
},
}
]
}
LOGICAL
is used for accounts that contain multiple currencies via children. You typically wouldn't post ledger entries to these accounts.
{
"ledger": {
"id": <ledgerId from createLedger call>
},
"ledgerAccounts": [
{
"ik": "multi-currency-cash-balance",
"name": "Global Operating Accounts",
"type": "asset",
"currencyMode": "single",
"currency": {
"code": "LOGICAL"
},
"childLedgerAccounts": [
{
"ik": "usd-operating-account",
"name": "USD Operating Account",
"currencyMode": "single",
"currency": {
"code": "USD"
},
},
{
"ik": "gbp-operating-account",
"name": "GBP Operating Account",
"currencyMode": "single",
"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.
{
"ledger": {
"id": <ledgerId from createLedger call>
},
"ledgerAccounts": [
{
"ik": "customer-123-portfolio",
"name": "Customer 123 Portfolio",
"type": "liability",
"currencyMode": "single",
"currency": {
"code": "LOGICAL"
},
"childLedgerAccounts": [
{
"ik": "customer-123-appl-shares",
"name": "Customer 123 AAPL",
"currencyMode": "single",
"currency": {
"code": "CUSTOM",
"customCurrencyId": "AAPL"
},
},
{
"ik": "customer-123-tsla-shares",
"name": "Customer 123 TSLA",
"currencyMode": "single",
"currency": {
"code": "CUSTOM",
"customCurrencyId": "TSLA"
},
},
{
"ik": "customer-123-usd",
"name": "Customer 123 USD",
"currencyMode": "single",
"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
.
{
"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.
To create a ledger account linked to a Native Link, use the account ID from the external system API as the externalId
. FRAGMENT will set the currency
automatically.
Once linked you can:
- Make payments that automatically reconcile and post ledger entries as they process
- Track unreconciled transactions imported by FRAGMENT
- Post ledger entries that reconcile imported transactions
To create a ledger account linked to a Custom Link, use the externalId
you passed into syncCustomAccounts
, which ideally is also the account ID at the external system. The Linked Ledger Account will inherit its currency
from the Custom Account.
Once linked you can:
- Track unreconciled transactions imported via
syncCustomTxs
- Post ledger entries that reconcile imported transactions
a. Making Payments
#You can initiate a payment and post its associated ledger entry in the same API call:
mutation MakeBankTransfer ($bankTransfer: MakeBankTransferInput!, $ik: SafeString!) {
makeBankTransfer(bankTransfer: $bankTransfer, ik: $ik) {
__typename
... on MakeBankTransferResult {
bankTransfer {
id
description
status
}
isIkReplay
}
... on Error {
code
message
}
}
}
{
"ik": "some-ik",
"description": "Pay Loan #123 Principal",
"bankTransfer": {
"amount": "-100000",
"payment": {
"type": "wire",
"destination": {
"routingNumber": "021000021",
"accountNumber": "123456789"
},
"details": {
"increase": {
"wire": {
"beneficiaryName": "wire_beneficiary"
}
}
},
},
"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.
You can query which transactions still need to be reconciled to a Linked Ledger Account using the unreconciledTxs
query:
query GetUnreconciledTxs ($ledgerAccountId: ID!) {
ledgerAccount(id: $ledgerAccountId) {
id
unreconciledTxs {
nodes {
id
description
amount
externalId
externalAccountId
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
{
"id": "<linked ledger account id>"
}
unreconciledTxs
returns a TxsConnection object which is a paginated list of transactions
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.
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:
{
"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
anddescription
matches the external transaction - A line in the other specified ledger account where the
description
matches the external transaction, but theamount
is negative
You can also do more complex reconciliation where the linked ledger line is balanced by multiple other lines and override descriptions:
{
"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"
}
]
}
}
You can specify the transaction to reconcile using the ID from the external system API instead of the FRAGMENT ID:
{
"entry": {
"lines": [
{
"account": {
"id": "linked-ledger-account-id"
},
"tx": {
"externalId": "increase-api-tx-id"
}
},
{
"account": {
"id": "offset-ledger-account-id"
}
}
]
}
}
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
- 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.
mutation AddLedgerEntry($entry: LedgerEntryInput!, $ik: SafeString!) {
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.
{
"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. If posted to an account with currencyMode
set to MULTI
, the currency of the line must be specified.
{
"ik": "some-ik",
"entry": {
"posted": "2020-01-01",
"description": "example multi currency ledger entry",
"lines": [
{
"amount": "100",
"currency": {
"code": "USD",
},
"account": {
"id": <multi-currency-account-1>
}
},
{
"amount": "100",
"currency": {
"code": "USD",
},
"account": {
"id": <multi-currency-account-2>
}
},
{
"amount": "105",
"currency": {
"code": "GBP",
},
"account": {
"id": <multi-currency-account-1>
}
},
{
"amount": "105",
"currency": {
"code": "GBP",
},
"account": {
"id": <multi-currency-account-2>
}
}
]
}
}
a. Updating Ledgers
#Use the updateLedger
mutation to update properties on an existing Ledger. Currently, only name
is supported.
mutation UpdateLedger($ledger: LedgerMatchInput!, $update: UpdateLedgerInput!) {
updateLedger(ledger: $ledger, update: $update) {
__typename
... on UpdateLedgerResult {
ledger {
id
name
}
}
... on Error {
code
message
}
}
}
{
"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.
mutation UpdateLedgerAccount(
$ledgerAccount: LedgerAccountMatchInput!
$update: UpdateLedgerAccountInput!
) {
updateLedgerAccount(ledgerAccount: $ledgerAccount, update: $update) {
__typename
... on UpdateLedgerAccountResult {
ledgerAccount {
id
name
}
}
... on Error {
code
message
}
}
}
{
"ledgerAccount": {
"id": <ledgerAccount.id from createLedgerAccounts call>
},
"update": {
"name": "Fresh new name"
}
}
a. Creating a Custom Link
#To integrate with external systems that FRAGMENT does not natively support, you can programmatically import transactions and external accounts using a Custom Link.
To create a Custom Link, use the createCustomLink
mutation.
mutation NewCustomLink($name: String!, $ik: SafeString!) {
createCustomLink(name: $name, ik: $ik) {
__typename
... on CreateCustomLinkResult {
link {
id
name
}
isIkReplay
}
... on Error {
code
message
}
}
}
{
"name": "Column",
"ik": "column-fragment"
}
Each Link represents an instance of an external system and could have one or many accounts under it.
b. Syncing Custom Accounts
#Once you've created a Custom Link, you can create accounts under it using the syncCustomAccounts
mutation. Each Custom Account is an immutable, single-entry view of all the transactions in the external account.
mutation SyncBankAccounts(
$link: LinkMatchInput!
$accounts: [CustomAccountInput!]!
) {
syncCustomAccounts(link: $link, accounts: $accounts) {
__typename
... on SyncCustomAccountsResult {
accounts {
id
externalId
name
currency {
code
customCurrencyId
}
}
}
... on Error {
code
message
}
}
}
{
"link": {
"id": <linkId from createCustomLink call>
},
"accounts": [
{ "externalId": "bank-account-1", "name": "Operational Account" },
{ "externalId": "bank-account-2", "name": "Reserve Account" }
]
}
To set the currency of a Custom External Account, pass in the currency
and currencyMode
field. If not specified, the default workspace account currency will be used. This supports the same multi and single currency account semantics as creating ledger accounts.
{
"link": {
"id": <linkId from createCustomLink call>
},
"accounts": [
{
"externalId": "bank-account-1",
"name": "Operational Account",
"currencyMode": "single",
"currency": {
"code": "GBP"
}
},
{
"externalId": "bank-account-2",
"name": "Multi currency holding account",
"currencyMode": "multi",
}
]
}
externalId
is used as the idempotency key, within the scope of its Custom Link- Calling
syncCustomAccounts
multiple times with the sameexternalId
will not create a new account - Most attributes on the account can't be updated. Attempting to do so will return a
BadRequestError
. The exception isname
, which can be updated since it's only used for display purposes
externalId
cannot contain,
,"
,'
,#
,/
,(
, or)
- You can sync up to 100 Custom Accounts in one API call
c. Syncing Custom Transactions
#You can create transactions under a Custom Account using syncCustomTxs
.
mutation SyncTransactions($link: LinkMatchInput!, $txs: [CustomTxInput!]!) {
syncCustomTxs(link: $link, txs: $txs) {
__typename
... on SyncCustomTxsResult {
txs {
id
externalId
amount
date
description
}
}
... on Error {
code
message
}
}
}
{
"link": { "id": <linkId from createCustomLink call> },
"txs": [
{
"account": {
"id": <accountId from syncCustomAccounts call>
},
"externalId": "tx-1",
"description": "Buy apple",
"amount": "-100",
"posted": "2022-03-31"
},
{
"account": {
"linkId": <linkId from createCustomLink call>,
"externalId": "bank-account-2"
},
"externalId": "tx-2",
"description": "Sold pen",
"amount": "100",
"posted": "2021-01-01T16:45:00.000Z"
}
]
}
If syncing transactions to a custom account with currency mode MULTI
, the currency of the tx must be provided:
{
"link": { "id": <linkId from createCustomLink call> },
"txs": [
{
"account": {
"id": <accountId from syncCustomAccounts call>
},
"externalId": "tx-1",
"description": "Buy apple",
"amount": "-100",
"posted": "2022-03-31",
"currency": {
"code": 'USD'
}
},
]
}
Once you've imported transactions, you can use the reconcileTx
mutation to add
them to a ledger via the linked ledger account.
externalId
is used as the idempotency key, within the scope of its account- Calling
syncCustomTxs
multiple times with the sameexternalId
will not create a new transaction - Most attributes on the transaction can't be updated. Attempting to do so will return a
BadRequestError
. The exception isdescription
, which can be updated since it's only used for display purposes
externalId
cannot contain,
,"
,'
,#
,/
,(
, or)
- You can sync up to 100 Custom Transactions in one API call
a. Supported Integrations
#Currently, FRAGMENT only supports a Native Link with Increase, a Banking As a Service (BaaS) provider.
To onboard Increase, go to the dashboard. Once onboarded, FRAGMENT will periodically sync all accounts and transactions from your Increase instance and set up a webhook to receive updates.
With a Native Link to Increase you can:
- Create Ledger Accounts that are linked to Increase bank accounts
- Make payments and account for them in a single API call
- Track payments made outside of FRAGMENT and reconcile them
b. Bank Transfers
#You can quickly get started making bank transfers using the makeBankTransfer
API. This mutation is syntactic sugar over the createOrder
mutation below, so we recommend that advanced users use Orders. Idempotency keys are shared between the two mutations.
mutation MakeBankTransfer ($bankTransfer: MakeBankTransferInput!, $ik: SafeString!) {
makeBankTransfer(bankTransfer: $bankTransfer, ik: $ik) {
__typename
... on MakeBankTransferResult {
bankTransfer {
id
description
status
}
isIkReplay
}
... on Error {
code
message
}
}
}
It has one parameter, bankTransfer
, which is a MakeBankTransferInput
. See below for examples.
This mutation will:
- Create a bank transfer at the external system
- Track when the transfer completes
- Import the transaction associated with the transfer
- Post a ledger entry that reconciles the transaction
Since this mutation talks to an external system, the result could be either a settled or submitted bankTransfer
, depending on whether the bank could synchronously process the transfer.
If a bankTransfer
is waiting on a transfer to settle at the bank, FRAGMENT will wait until the transaction posts at the bank account to post the ledger entry.
To initiate an ACH credit or wire from your bank account you need to include:
- The ledger account linked to that bank account
- A negative amount to indicate that money is leaving the account
- A payment with a destination and a type of either
ach
orwire
- An offsetting entry
{
"ik": "some-ik-not-previously-used-in-create-order",
"bankTransfer": {
"amount": "-100000",
"description": "Initiate customer withdrawal",
"payment": {
"type": "wire",
"source": {
"accountNumber": <bank-account-number>,
"routingNumber": <bank-routing-number>
}
},
"linkedLedgerAccount": {
"id": <your-bank-account-id-in-fragment>
},
"offset": {
"lines": [
{
"amount": "100000",
"account": {
"id": <leger-account-id-in-fragment>
}
}
]
}
}
}
To initiate an ACH debit, you need to include:
- The ledger account linked to that bank account
- A positive amount to indicate that money should be entering the account
- A payment with a source and a type of
ach
- An offsetting entry
{
"ik": "some-ik-not-previously-used-in-create-order",
"bankTransfer": {
"amount": "100000",
"description": "Customer payment",
"payment": {
"type": "ach",
"source": {
"accountNumber": <customer-account-number>,
"routingNumber": <customer-routing-number>
}
},
"linkedLedgerAccount": {
"id": <your-cash-account-id-in-fragment>
},
"offset": {
"lines": [
{
"amount": "-100000",
"account": {
"id": <your-income-account-id-in-fragment>
}
}
]
}
}
}
You can send money from one bank account to another within the same link by specifying the destination bank account and a negative amount.
{
"ik": "some-ik-not-previously-used-in-create-order",
"bankTransfer": {
"amount": "-1000",
"payment": {
"type": "internal",
"destination": {
"bankAccount": {
"id": "fragment bank account id"
}
},
},
"offset": {
"lines": [
{
"amount": "1000",
"account": {
"id": <ledger-account-id>
}
}
]
},
"linkedLedgerAccount": {
"id": <your-cash-account-id-in-fragment>,
},
"description": "Pay out $10 to platform accounts",
}
You can also pull money into a linked bank account from another at the same bank
by specifying a source
and positive amount
:
{
"ik": "some-ik-not-previously-used-in-create-order",
"bankTransfer": {
"amount": "1000",
"payment": {
"type": "internal",
"destination": {
"bankAccount": {
"id": "fragment bank account id"
}
},
},
"offset": {
"lines": [
{
"amount": "1000",
"account": {
"id": <ledger-account-id>
}
}
]
},
"linkedLedgerAccount": {
"id": <your-cash-account-id-in-fragment>,
},
"description": "Pay in $10 to platform accounts",
}
You cannot currently do an internal transfer between two linked ledger accounts when both ledger accounts are linked to the same ledger.
If the linked ledger accounts are in different ledgers then the transaction in the non-initiating ledger account will appear as an unreconciled transaction. You can retrieve the otherTxId
in the response to createOrder
and pass it to reconcileTx
.
c. Orders
#Orders let you do accounting and payments with a single API call.
BankTransfer Orders have the following states:
- Submitted: If the payment is created successfully, the Order begins in a submitted state.
- Failed: If the payment isn't created successfully, the Order begins in a failed state. This is a terminal state.
- Settled: Once the payment clears, the Order transitions to a Settled state. During the payment reversal window (usually 60 days), this is a non-terminal state. Once the payment can no longer reverse, this becomes a terminal state.
- Returned: If the payment returns after settlement, the Order transitions to this state. For ACH, a return can happen up to 60 days after the original transaction. This is a terminal state.
You can use the createOrder
mutation to create an Order.
mutation CreateOrder ($order: CreateOrderInput!, $ik: SafeString!) {
createOrder(order: $order, ik: $ik) {
__typename
... on CreateOrderResult {
order {
stateMachine {
bankTransfer {
status
transitionLog {
transition
effect {
entry {
id
description
lines {
id
date
amount
}
}
}
}
}
}
}
isIkReplay
}
... on Error {
code
message
}
}
}
It has one parameter, order
of type CreateOrderInput
, which defines the state machine of the bank transfer being made. The BankTransferStateMachineTransitionsInput
type allows you to define the side-effects of each state transition. Currently, the only side-effect supported is posting a ledger entry.
Here's an example input that provides automatic accounting for payment reversals:
{
"ik": <some-ik>,
"order": {
"stateMachine": {
"bankTransfer": {
"config": {
"transitions": {
"submitted_to_settled": {
"entry": {
"description": "hello",
"lines": [
{
"amount": "10000",
"account": {
"id": <linked-ledger-account-id>
},
"payment": {
"type": "ach",
"destination": {
"accountNumber": "987654321",
"routingNumber": "021000021"
}
}
},
{
"amount": "10000",
"account": {
"id": <receivables-loan-123-due>
}
}
]
}
},
"settled_to_returned": {
"entry": {
"description": "olleh (reversed hello)",
"lines": [
{
"amount": "10000",
"account": {
"id": <linked-ledger-account-id>
}
},
{
"amount": "10000",
"account": {
"id": <collections-loan-123-pending>
}
}
]
}
}
}
}
}
}
}
}
You're required to define the submitted_to_settled
transition when creating an order, the others are optional.
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.
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.
ledgers
List ledgers in a workspace.ledger(ledger: LedgerMatchInput)
Get a ledger.ledgerAccount(ledgerAccount: LedgerAccountMatchInput)
Get a ledger account.ledgerEntry(ledgerEntry: LedgerEntryMatchInput)
Get a ledger entry.ledgerLine(ledgerLine: LedgerLineMatchInput)
Get a ledger line.order(order: OrderMatchInput)
Get an Order.tx(tx: TxMatchInput)
Get a transaction.
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. ThepageInfo
property contains cursors pointing to the next page and previous pages - You can send a cursor to the
after
orbefore
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:
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
}
}
}
}
{
"ledgerId": "example-ledger-id-1",
"first": 2
}
The response looks like this:
{
"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:
{
"ledgerId": "ledger-id-1",
"after": <some-end-cursor>
}
The response looks like this:
{
"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:
{
"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
orTxTypeFilter
)DateFilter
DateTimeFilter
ExternalAccountFilter
LedgerAccountFilter
To filter by the type on an Entity, send a filter argument in your query with an
equalTo
operator:
query GetLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: { id: $ledgerId}) {
ledgerAccounts(filter: $filter) {
nodes {
id
name
type
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"type": {
"equalTo": "asset"
}
}
}
To search for multiple types, you can use the in
operator:
{
"filter": {
"type": {
"in": ["asset", "liability"]
}
}
}
DateFilters
work similarly to TypeFilters
and allow you to query for a specific date:
query GetAccountLinesOnDate($ledgerAccountId: ID!, $filter: LedgerLinesFilterSet) {
ledgerAccount(ledgerAccount: {id: $ledgerAccountId}) {
lines(filter: $filter) {
nodes {
id
posted
date
amount
description
ledgerEntryId
}
}
}
}
{
"ledgerAccountId": <ledger-account-id>,
"filter": {
"date": {
"equalTo": "1969-07-21"
}
}
}
To specify a set of dates:
{
"filter": {
"date": {
"in": ["1969-07-20", "1969-07-21"]
}
}
}
To search for a date range use a 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:
query GetLedgerEntries($ledgerAccountId: ID!, $filter: LedgerLinesFilterSet) {
ledgerAccount(ledgerAccount: {id: $ledgerAccountId}) {
lines(filter: $filter) {
nodes {
id
posted
date
amount
description
ledgerEntryId
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"posted": {
"before": "1969-07-21T02:56:00.000Z"
}
}
}
You can also search using the 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:
{
"filter": {
"posted": {
"after": "1969-01-01"
"before": "1969-12-31"
}
}
}
To filter ledger accounts by their linked accounts, send a filter argument in your query with an equalTo
, in
, or isLinkedAccount
operator:
query GetLinkedLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: {id: $ledgerId}) {
id
ledgerAccounts(filter: $filter) {
nodes {
id
name
linkedAccount {
id
name
}
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"isLinkedAccount": true
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"linkedAccount": {
"equalTo": {
"id": <external account id>
}
}
}
}
To search for multiple parents you can use the in
operator:
{
"ledgerId": "ledger-id-1",
"filter": {
"linkedAccount": {
"in": [
{ "id": <external account id 1> },
{ "id": <external account id 2> }
]
}
}
}
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:
query GetLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: {id: $ledgerId}) {
id
ledgerAccounts(filter: $filter) {
nodes {
id
name
parentLedgerAccountId
parentLedgerAccount {
id
name
}
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"hasParentLedgerAccount": true
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"parentLedgerAccount": {
"equalTo": {
"id": "<ledger account id>"
}
}
}
}
To search across multiple parents you can use the in
operator:
{
"ledgerId": "ledger-id-1",
"filter": {
"parentLedgerAccount": {
"in": [{ "id": "<parent ledger id 1>" }, { "id": "<parent ledger id 2>" }]
}
}
}
You can combine filters by adding multiple components to the filter block. Results are ANDed. Example:
{
"ledgerId": "ledger-id-1",
"filter": {
"hasParentLedgerAccount": true,
"type": {
"in": ["asset", "liability"]
}
}
}
e. Match Inputs
#FRAGMENT requires you to provide MatchInputs
instead of traditionally requiring you to provide an ID. This gives you flexibility to use multiple unique identifiers to query for an entity.
Each entity has a FRAGMENT-generated id
which is used in all queries. However, for Ledgers, it's recommended to query with the IK's you've provided to FRAGMENT instead so you don't need to store the FRAGMENT ID in your system.
{
"ledger": {
"ik": "ledger-ik"
}
}
LedgerAccounts support something similar, ikPath
, which is a forward-slash-delimited string containing the IK of an account and all its direct ancestors. When querying with ikPath
, you'll also need to provide a LedgerMatchInput to identify which Ledger the account belongs to.
{
"ikPath": "parent-ik/child-ik/grandchild-ik",
"ledger": {
"ik": "ledger-ik"
}
}
f. Balances
#FRAGMENT supports querying a ledger account for its balances, historical balances, and balance changes.
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 currencybalance
The sum of all ledger lines, including child ledger accounts in the account's currency.ownBalances
The sum all ledger lines in the account in all currencies, excluding ledger lines in child ledger accounts..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.
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:
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.
You can also query the net change on a ledger account over a specific period:
ownBalanceChange
How muchownBalance
changed.childBalanceChange
How muchchildBalance
changed.balanceChange
How muchbalance
changed.ownBalanceChanges
How muchownBalances
changed.childBalanceChanges
How muchchildBalances
changed.balanceChanges
How muchbalances
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, as well as make multiple queries using aliases:
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
}
}
}
}
}
}
Balance queries will respect the ledger's balanceUTCOffset
when specifying periods
and times:
- If a ledger has an offset of
-08:00
, then queryingbalance(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.
g. 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.
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.
query GetBalanceSheet ($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: {id: $ledgerId}) {
ledgerAccounts(filter: $filter) {
nodes {
id
name
type
balance
}
}
}
}
{
"ledgerId": <some-ledger-id>,
"filter": {
"type": {
"in": ["asset", "liability"]
}
}
}
To generate an Income Statement, query balance changes on Change accounts.
query GetIncomeStatement ($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: {id: $ledgerId}) {
ledgerAccounts(filter: $filter) {
nodes {
id
name
type
balanceChange(period: "1969")
}
}
}
}
{
"ledgerId": <some-ledger-id>,
"filter": {
"type": {
"in": ["income", "expense"]
}
}
}
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.
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")
}
}
}
}
{
"ledgerId": <some-ledger-id>,
"filter": {
"type": {
"in": ["asset", "liability"]
}
}
}
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:
key: {export name}/LedgerEntry/day={day}/hour={hour}/{partition}.part
key: {export name}/LedgerLine/day={day}/hour={hour}/{partition}.part
key: {export name}/LedgerAccount/day={day}/hour={hour}/{partition}.part
2023-01-23 | Additional Payment Details We now support additional payment details that can be passed through to the payment provider. As part of this change, we've added support for providing beneficiary info on outgoing wires. Check out the PaymentInput type's |
2022-12-27 | Querying by IKs You can now use Ledger IKs in LedgerMatchInput in addition to Furthermore, we are introducing a new field on LedgerAccount, |
2022-12-22 | SafeString IKs IKs must now be SafeStrings. Ensure that all |
2022-12-20 | Multiple currency ledger accounts and custom links You can now create multi currency ledger accounts and custom links! A multi currency ledger account lets you post ledger lines of multiple currencies to the same account. This can be used for a wide variety of use cases: brokerages, crypto wallets, and international treasury to name a few.
To create a multi currency ledger account, call createLedgerAccounts with |
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 |
2022-10-11 | Backwards Pagination Connections now return |