Documentation
a. Overview
#This Quickstart will introduce you to the FRAGMENT development cycle.
b. Prerequisites
#- A FRAGMENT Workspace
- Node version 16 or higher installed locally
- Homebrew
c. Steps
#-
Setup the CLI
Tap Homebrew Repo
brew tap fragment-dev/tap
Install CLI
brew install fragment-dev/tap/fragment-cli
-
Authenticate your local environment to your Workspace.
fragment login
Create a new API Client. Paste the
Client ID
,Secret Key
,API URL
, andOAuth URL
into the CLI. -
Create your first Schema.
fragment init
This will write an example Schema to
./fragment.jsonc
-
Start the local development environment. This will open a schema visualizer in your browser. The visualizer automatically updates every time the file changes.
fragment start
-
Open a text editor to edit
fragment.jsonc
. Under the Ledger Account with keyassets-root
, add a second child Ledger Account adjacent to the Ledger Account with keybank
:2nd child asset Ledger Account{ "key": "reserve-bank", "name": "Reserve Bank", "children": [] }
-
Post a Ledger Entry from the localhost Schema visualization to see how balance changes are reflected in your Chart of Accounts. This is an in-memory playground you can use to iterate quickly on your Schema.
-
Store your Schema in FRAGMENT. The
storeSchema
mutation is an upsert, only creating a new version if the Schema changes. Additionally, it will throw aBadRequestError
if you attempt to make backwards incompatible Schema changes, such as removing a Ledger Account.fragment store-schema
-
Create a Ledger with your Schema. You can have multiple Ledgers using the same Schema. When you update your Schema with the
storeSchema
mutation, every Ledger associated with it is automatically migrated to the latest version.fragment create-ledger \ --name "sample ledger" \ --ik "ledger-1"
In the dashboard, you should see the created Ledger.
-
Post a Ledger Entry via the API.
fragment add-ledger-entry \ --ik "entry-1" \ --ledger.ik "ledger-1" \ --type sell_something \ --param "sales_before_tax=10000" \ --param "tax_payable=500"
This posts an entry of type
sell_something
by calling theaddLedgerEntry
mutation. The other mutations that post Ledger Entries arereconcileTx
,createOrder
, andmakeBankTransfer
.
Next:
- Learn about Ledger Account Templates.
- Get started developing with the API Overview.
a. Workflow
#The FRAGMENT workflow is:
- Ledger: a data structure modeling funds for a single entity. Ledgers are created using the
createLedger
mutation. The FRAGMENT CLI provides a convenient wrapper around this mutation. If your use case requires you to manage multiple Ledgers, call thecreateLedger
mutation directly from your application. - API: data is created in a Ledger via the GraphQL API. Use
addLedgerEntry
to post logical Ledger Entries,reconcileTx
to reconcile transactions from an external system to your Ledger, andcreateOrder
ormakeBankTransfer
to make payments. See Accounting, Reconciliation, and Payments to learn more about the API. - Schema: a JSON-based declarative source of truth for the structure of your Ledger. In your CI or deployment system, use the CLI to run
fragment store-schema
. This creates or updates your Schema like a database migration. When this is run, FRAGMENT automatically migrates all Ledgers using the Schema with the updated Ledger Accounts and Ledger Entry types.
The API URL for your environment can be found in the API Clients page of the Dashboard.
You can find the GraphQL schema hosted at:
https://api.fragment.dev/schema.graphql
You can use this schema as input to auto-generate an SDK to use to call the FRAGMENT API. The GraphQL Foundation maintains an updated list of tools. We recommend that you do this as part of your CI, to ensure that you always have the latest schema.
b. No Decimals
#There are only integer amounts in the API. This forces you to use the minor units in the appropriate currency. For USD, a single unit is 1 cent. To represent $1.25, you'd pass in 125.
c. Idempotency
#In financial systems, it's important to achieve exactly-once processing. For example, when debiting a customer's bank account, only one ACH Debit should be made. FRAGMENT's API is idempotent, so your system needs to only handle at-least-once processing when calling the FRAGMENT API.
To achieve idempotency, mutations in the FRAGMENT API require a unique and stable idempotency key (IK
). These let you confidently retry operations without them running twice. If you execute a mutation more than once with the same IK
and variables, FRAGMENT will ignore your request and return the original response with the isIkReplay
flag set to true. IK
s in FRAGMENT:
- are scoped per-Ledger. If you send multiple requests with the same
IK
to a single ledger, only one mutation will execute withisIkReplay: false
, while the rest will return the original response withisIkReplay: true
. If you send multiple requests with the sameIK
to multiple different ledgers, all mutations will execute and returnisIkReplay: false
. - are valid for 30 days. If you resend an
IK
after 30 days, it could be executed as a fresh request. - are only valid for 1 hour when initiating transfers at Increase through the
makeBankTransfer
orcreateOrder
mutations. This is so we match Increase's idempotency window. All other FRAGMENT mutations support IKs for 30 days.
d. Authentication
#FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The flow is:
- Get a fresh access token from the token endpoint using your API client's credentials. You can find the endpoint URL and OAuth scope in the Dashboard's API Clients page.
- 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 OAuth2 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 | {{scope}} from dashboard |
client_id | {{client_id}} from dashboard |
Your payload should be in the x-www-form-urlencoded
format:
grant_type=client_credentials&scope={{scope}}&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 endpoint.
Your specific OAuth URL
and scope
may vary depending on which region your workspace is in.
You can find these values in the the dashboard once you have created your API Client.
curl --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'
scope = 'https://api.fragment.dev/*
auth_url = 'https://auth.fragment.dev/oauth2/token'
payload = f'grant_type=client_credentials&scope={scope}&client_id={client_id}'
client_credentials = f'{client_id}:{client_secret}'
encoded_creds = (
b64encode(client_credentials.encode('ascii'))
.decode('ascii')
)
conn = http.client.HTTPSConnection(auth_url)
headers = {
'Authorization': f'Basic {encoded_creds}',
'Content-Type': 'application/x-www-form-urlencoded',
}
conn.request("POST", endpoint, payload, headers)
data = conn.getresponse().read()
print(data.decode("utf-8"))
import axios from "axios";
const btoa = (str) => Buffer.from(str).toString("base64");
const clientId = "2h4t0cv7qv5c9r0qgs1o3erkuk";
const secret = "superSecretClientSecret";
const scope = "https://api.fragment.dev/*";
const authUrl = "https://auth.fragment.dev/oauth2/token";
const getToken = async () => {
const auth = btoa(`${clientId}:${secret}`);
const data = new URLSearchParams();
data.append("grant_type", "client_credentials");
data.append("scope", scope);
data.append("client_id", clientId);
const response = await axios.request({
method: "POST",
url: authUrl,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${auth}`,
Accept: "*/*",
},
data,
});
if (!response.data.access_token) {
throw new Error(
"didn't get an access token from auth endpoint"
);
}
return response.data.access_token;
};
e. Match Inputs
#When referencing an entity in FRAGMENT, the API requires you to provide a MatchInputs
. This gives you the flexibility to match for an entity in multiple ways.
Each entity has a FRAGMENT-generated id
which can be used in all queries.
For Ledgers, it's recommended to query with the IK's you've provided to FRAGMENT instead. This lets you not store the FRAGMENT ID of the Ledger in your system.
{
"ledger": {
"ik": "ledger-ik"
}
}
You can also match LedgerAccounts via their path
. The path
is a forward-slash-delimited string containing the IK of a Ledger Account and all its direct ancestors. When querying with path
, you'll also need to provide a LedgerMatchInput to identify which Ledger the Ledger Account belongs to.
{
"path": "parent-ik/child-ik/grandchild-ik",
"ledger": {
"ik": "ledger-ik"
}
}
f. Error Handling
#Errors are first-class citizens of our API. All mutations return a union type that represents either an error or a successful result. For example, the response type of the addLedgerEntry
mutation is as follows:
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
).
g. Querying
#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.
FRAGMENT allows you to query relationships between entities via Connection
types. These expansions are documented alongside the queries in the API Reference.
ledgerAccounts
via Ledger. List Ledger Accounts in a Ledger.ledgerEntries
via Ledger. List Ledger Entries in a Ledger.lines
via LedgerAccount. List Ledger Lines in a Ledger Account.lines
via LedgerEntry. List Ledger Lines in a ledger entry.ledgerLines
via Tx. List Ledger Lines reconciled to a transaction.ledgerEntries
via Tx. List Ledger Entries reconciled to a transaction.
Using these connections, you can construct complex queries over the data graph stored in FRAGMENT.
h. Pagination
#Fields that return lists support cursor-based pagination.
- Results are returned under a
nodes
property as an array. 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
- Results are returned in a deterministic order. This is generally reverse chronological; newest first. This sort is by posted date for Ledger Lines and Ledger Entries, or creation date for other entities.
query GetLedgerAccounts(
$ledgerIk: SafeString!
$after: String
$first: Int
$before: String
) {
ledger(ledger: { ik: $ledgerIk }) {
ledgerAccounts(
after: $after
first: $first
before: $before
) {
nodes {
path
name
type
}
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
}
}
}
{
"ledgerIk": "ik-used-to-create-ledger",
"first": 2
}
This query uses pagination to retrieve 2 Ledger Accounts at a time.
{
"data": {
"ledger": {
"ledgerAccounts": {
"nodes": [
{
"path": "assets/test-assets/test:1",
"name": "Test 1",
"type": "asset"
},
{
"path": "assets/test-assets/test:2",
"name": "Test 2",
"type": "asset"
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "<some-end-cursor>",
"hasPreviousPage": false,
"startCursor": null
}
}
}
}
}
This would be the response.
{
"ledgerIk": "ik-used-to-create-ledger",
"after": "<some-end-cursor>"
}
To retrieve the next page, send the same query but with the after
parameter set on ledgerAccounts
.
{
"data": {
"ledger": {
"ledgerAccounts": {
"nodes": [
{
"path": "assets/test-assets/test:3",
"name": "Test 3",
"type": "asset"
},
{
"path": "assets/test-assets/test:4",
"name": "Test 4",
"type": "asset"
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null,
"hasPreviousPage": true,
"startCursor": "<some-start-cursor>"
}
}
}
}
}
The response looks like this.
{
"ledgerIk": "ik-used-to-create-ledger",
"before": "<some-start-cursor>"
}
To retrieve the previous page of results, send the same query but with the before
parameter set on ledgerAccounts
. The response will be the first page of results.
i. Filtering
#You can filter result sets via the filter
parameter on Connection
types when performing list queries. The API Reference documents this in detail.
The API has several filter types built-in:
TypeFilters
(e.g.LedgerAccountTypeFilter
orTxTypeFilter
)DateFilter
DateTimeFilter
Boolean
ExternalAccountFilter
LedgerAccountFilter
query GetLedgerAccounts(
$ledgerIk: SafeString!
$accountsFilter: LedgerAccountsFilterSet!
) {
ledger(ledger: { ik: $ledgerIk }) {
ledgerAccounts(filter: $accountsFilter) {
nodes {
id
name
type
}
}
}
}
{
"ledgerIk": "ik-used-to-create-ledger",
"accountsFilter": {
"type": {
"equalTo": "asset"
}
}
}
To filter by the type on an Entity, send a filter argument in your query with an
equalTo
operator.
{
"accountsFilter": {
"type": {
"in": ["asset", "liability"]
}
}
}
To search for multiple types, you can use the in
operator:
DateFilters
work similarly to TypeFilters
.
query GetAccountLinesOnDate(
$accountMatch: LedgerAccountMatchInput!
$linesFilter: LedgerLinesFilterSet!
) {
ledgerAccount(ledgerAccount: $accountMatch) {
lines(filter: $linesFilter) {
nodes {
id
posted
date
amount
description
ledgerEntryId
}
}
}
}
{
"accountMatch": {
"ledger": {
"ik": "ik-used-to-create-ledger"
},
"path": "liabilities/customer-deposits/customer:123"
},
"linesFilter": {
"date": {
"equalTo": "1969-07-21"
}
}
}
Use the equalTo
operator to query for a specific date.
{
"linesFilter": {
"date": {
"in": ["1969-07-20", "1969-07-21"]
}
}
}
Use the in
operator to specify a set of dates.
To search for a date range use a DateTimeFilter.
query GetLedgerEntries(
$accountMatch: LedgerAccountMatchInput!
$linesFilter: LedgerLinesFilterSet
) {
ledgerAccount(ledgerAccount: $accountMatch) {
lines(filter: $linesFilter) {
nodes {
id
posted
date
amount
description
ledgerEntryId
}
}
}
}
{
"accountMatch": {
"ledger": {
"ik": "ik-used-to-create-ledger"
},
"path": "liabilities/customer-deposits/customer:123"
},
"linesFilter": {
"posted": {
"before": "1969-07-21T02:56:00.000Z"
}
}
}
Use the before
operator to find entities that were created or posted before a certain timestamp.
{
"linesFilter": {
"posted": {
"after": "1969-07-21T02:56:00.000Z"
}
}
}
You can also search using the after
operator.
{
"linesFilter": {
"posted": {
"after": "1969-01-01"
"before": "1969-12-31"
}
}
}
To specify a range supply both before
and after
. You can also use shortened dates.
query GetLinkedLedgerAccounts(
$ledgerIk: SafeString!
$accountsFilter: LedgerAccountsFilterSet!
) {
ledger(ledger: { ik: $ledgerIk }) {
ledgerAccounts(filter: $accountsFilter) {
nodes {
path
name
linkedAccount {
externalId
name
}
}
}
}
}
{
"ledgerIk": "ik-used-to-create-ledger",
"accountsFilter": {
"isLinkedAccount": true
}
}
Use the isLinkedAccount
filter to find all Ledger Accounts that are linked to any External Account.
query GetLedgerAccounts(
$ledgerIk: SafeString!
$accountsFilter: LedgerAccountsFilterSet!
) {
ledger(ledger: { ik: $ledgerIk }) {
ledgerAccounts(filter: $accountsFilter) {
nodes {
path
name
parentLedgerAccount {
path
name
}
}
}
}
}
{
"ledgerIk": "ik-used-to-create-ledger",
"accountsFilter": {
"hasParentLedgerAccount": true
}
}
To filter Ledger Accounts by the presence of a parent Ledger Account use the hasParentLedgerAccount
filter.
Use ExternalAccountFilter
to filter Ledger Accounts by their linked External Accounts. It supports the equalTo
and in
operators.
{
"ledgerIk": "ik-used-to-create-ledger",
"accountsFilter": {
"linkedAccount": {
"equalTo": {
"linkId": "link-id",
"externalId": "<external account id"
}
}
}
}
Use the equalTo
operator on the linkedAccount
filter to find all Ledger Accounts that are linked to a specific External Account. The External Account can be identifed using an ExternalAccountMatchInput
.
{
"ledgerIk": "ik-used-to-create-ledger",
"accountsFilter": {
"linkedAccount": {
"in": [
{ "id": <external account id 1> },
{ "id": <external account id 2> }
]
}
}
}
Use the in
operator to find all Ledger Accounts that are linked to any of the specified External Accounts. The External Accounts can be identified using their Fragment ID instead of the external system ID.
To filter Ledger Accounts by a specific parent, send a parentLedgerAccount
filter.
query GetLedgerAccounts(
$ledgerIk: SafeString!
$accountsFilter: LedgerAccountsFilterSet!
) {
ledger(ledger: { ik: $ledgerIk }) {
ledgerAccounts(filter: $accountsFilter) {
nodes {
path
name
parentLedgerAccount {
path
name
}
}
}
}
}
{
"ledgerIk": "ik-used-to-create-ledger",
"filter": {
"parentLedgerAccount": {
"equalTo": {
"ledger": {
"ik": "ik-used-to-create-ledger"
},
"path": "liabilities/customer-deposits/customer:123"
}
}
}
}
Use the equalTo
operator on the parentLedgerAccount
filter to find all Ledger Accounts that have a specific parent. The parent can be identified using a LedgerAccountMatchInput
.
{
"ledgerIk": "ik-used-to-create-ledger",
"filter": {
"parentLedgerAccount": {
"in": [
{ "id": "<parent ledger account id 1>" },
{ "id": "<parent ledger account id 2>" }
]
}
}
}
Use the in
operator to find all Ledger Accounts that have any of the specified parents. The parents can be identified using their Fragment ID instead of their path
.
{
"ledgerIk": "ik-used-to-create-ledger",
"accountsFilter": {
"hasParentLedgerAccount": true,
"type": {
"in": ["asset", "liability"]
}
}
}
You can combine filters by adding multiple components to the filter block. Results are ANDed.
a. Ledger
#A Ledger is a data structure that implements double entry accounting. Ledgers have a perspective; they track the money movements for a single business entity. In FRAGMENT, you model your Ledger using a Schema.
b. Ledger Account
#The core primitive in a Ledger is the Ledger Account. A Ledger Account tracks movements of money. Each Ledger Account encodes the entity's relationship with other entities such as their bank, customers, and vendors.
{
"chartOfAccounts": {
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset",
},
{
"key": "debt-root",
"name": "Debt",
"type": "liability",
},
]
}
}
Within a schema, chartOfAccounts
models the Ledger Accounts that will be created in your Ledger. The above Schema would create two Ledger Accounts: assets-root
and debt-root
.
A Ledger Line records a single change to a Ledger Account. The balance of a Ledger Account is the sum of Ledger Lines posted to it.
Ledger Accounts can be split into two layers, State and Change.
Within the State and Change layer, Ledger Accounts have specific types. These types map directly to common financial reports like Balance Sheets and Income Statements.
State
asset Assets: what you own
liability Liabilities: what you owe
Change
income Income: what you've earned
expense Expense: what you've spent
{
"chartOfAccounts": {
"defaultCurrencyMode": "single",
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset"
}
]
},
}
In a Ledger Account Schema, use the type
field to set the Ledger Account's type. A Ledger Account can only have one type and it cannot change once the Ledger Account is created.
In a double-entry Ledger, the total balance of all Ledger Accounts in the State layer equals the total balance of all Ledger Accounts in the Change layer.
- The State layer tracks the entity's net worth. The total of all the Ledger Account balances in the State layer adds up to the current net worth of the entity.
- The Change layer tracks updates to net worth. When the total net worth changes, Ledger Accounts in the Change layer track the reason for that update.
This principle is captured in FRAGMENT's accounting equation:
By design, FRAGMENT doesn't provide an Equity account type. Liability and Equity are equivalent in the Accounting Equation, so having both types would be redundant. You can model Equity on your Ledger as a Ledger Account with type Liability. The sample Schema in the Quickstart demonstrates this.
In FRAGMENT, Ledger Accounts exist in a tree hierarchy. This enables you to incorporate the dimensionality of your data directly into the Ledger. You will typically record data at the leaf nodes, such that their balances will propagate up the tree.
{
"chartOfAccounts": {
"accounts": [
{
"key": "Customer 1",
"type": "liability",
"children": [
{
"key": "Available"
},
{
"key": "Unsettled",
},
{
"key": "Blocked"
},
]
}
]
}
The Chart of Accounts lets you encode this hierarchy using the children
field. You might use it to maintain multiple balances for a given user of your product, representing the states their funds are in. The hierarchy goes up to 10 levels deep.
c. Ledger Entry
#A Ledger Entry is a single update to a Ledger. A Ledger Entry consists of two or more Ledger Lines. The sum of the amounts of the Ledger Lines must follow the Accounting Equation.
This is a simple Ledger Entry containing two Ledger Lines. One Ledger Line posts to an Asset account and the other posts to an Income account.
{
"ledgerEntries": {
"types": [
{
"type": "buy_materials",
"lines": [
{
"key": "bank",
"account": { "path": "assets-root/bank" },
"amount": "-{{materials_cost}}"
},
{
"key": "materials_expense",
"account": { "path": "expense-root/materials" },
"amount": "{{materials_cost}}"
}
]
}
]
}
}
A Ledger Entry is configured with a type
and can optionally define the set of Ledger Lines that it creates. The amounts are parameterized {{variables}}
that you must provide to the API at runtime.
{
"type": "buy_materials",
"ledger": {
"ik": "my-ledger-ik"
},
"parameters": {
"materials_cost": "1500"
}
}
To post this Ledger Entry, use the LedgerEntryInput
type. This type is shared across all the mutations that post Ledger Entries: addLedgerEntry(), reconcileTx(), makeBankTransfer(), and createOrder(). The CLI also provides fragment add-ledger-entry
, a convenient wrapper around addLedgerEntry
.
{
"ledgerEntries": {
"types": [
{
"type": "sell_something",
"lines": [
{
"key": "sales_to_bank",
"account": { "path": "assets-root/bank" },
"amount": "{{sales_before_tax}} + {{tax_payable}}"
},
{
"key": "income_from_sales_before_tax",
"account": { "path": "income-root/sales" },
"amount": "{{sales_before_tax}}"
},
{
"key": "tax_payable",
"account": { "path": "debt-root/tax_payables" },
"amount": "{{tax_payable}}
}
]
}
]
}
}
For more complex Ledger Entries, you can encode arithmetic in the Schema to configure Ledger Line amounts. Currently, only +
and -
are supported.
d. Ledger Account Templates
#The Chart of Accounts can include Ledger Account templates. A template is a repeated subtree of Ledger Accounts that are configured in the Schema. Templates can be useful when your Ledger models financial relationships with a repeated entity, such as users of your product.
{
"chartOfAccounts": {
"accounts": [
{
"key": "user",
"type": "liability",
"template": true,
"children": [
{
"key": "available"
},
{
"key": "unsettled"
},
{
"key": "blocked"
}
]
},
{
"key": "bank",
"type": "asset",
"template": false
}
]
}
}
The template
field in the Schema signifies a Ledger Account template. When the Schema is applied, this subtree is not instantiated. Instead, accounts in this subtree are created just-in-time when you first post a Ledger Entry to this part of the tree.
{
"ledgerEntries": {
"types": [
{
"type": "user_funding",
"lines": [
{
"key": "funds_arrive_in_bank",
"account": {
"path": "bank"
},
"amount": "-{{funding_amount}}"
},
{
"key": "user_balance",
"account": {
"path": "user:{{user_id}}/available"
},
"amount": "{{funding_amount}}"
}
]
}
]
}
}
Ledger Entries that posts to Ledger Account templates specify an identifier within the Ledger Account path
. When the Ledger Entry is posted, the path is constructed with the parameters
provided to the API. The Ledger Account subtree gets created if necessary.
In this example, the path
for the available
account for user 1234
would be liabilities-root/user:1234/available
.
The templated path
syntax lets you use identifiers from your system to refer to Ledger Accounts in FRAGMENT. It is also used when querying.
e. Currencies
#Ledger Accounts in FRAGMENT support the single
or multi
currency mode. A Ledger Account in the single
currency mode only accepts Ledger Lines of the same currency as the Ledger Account. In the multi
currency mode, Ledger Lines of any currency are allowed.
FRAGMENT natively supports all ISO-4217 currencies in addition to a list of common cryptocurrencies. You can also add your own custom currencies to build brokerages, track rewards points, or ensure pre-tax and post-tax money don't mingle.
{
"chartOfAccounts": {
"defaultCurrency": {
"code": "USD"
},
"defaultCurrencyMode": "single",
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset"
}
]
}
}
In most use cases, all Ledger Accounts in a Ledger will share the same currency
and currencyMode
. For convenience, you can set these as defaults within Schema.
{
"chartOfAccounts": {
"defaultCurrency": {
"code": "USD"
},
"defaultCurrencyMode": "single",
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset",
"currency": {
"code": "CAD"
}
}
]
}
}
To override the default for a specific Ledger Account, you can set currency
and currencyMode
directly on that account.
{
"ledgerEntries": {
"types": [
{
"type": "buy_materials",
"lines": [
{
"key": "bank",
"account": { "path": "assets-root/bank" },
"amount": "-{{materials_cost}}",
"currency": {
"code": "{{expense_currency}}"
}
},
{
"key": "materials_expense",
"account": { "path": "expense-root/materials" },
"amount": "{{materials_cost}}",
"currency": {
"code": "{{expense_currency}}"
}
}
]
}
]
}
}
You can also parameterize the currency for a Ledger Line. This is required when posting Ledger Entries to Ledger Accounts with the multi
currency mode. For more on how you might use the multi
currency mode, see Multi-currency.
f. Logical Clock
#In Accounting, Ledger Entries are recorded at the time the money movement event happened. However, since there is latency in software systems, you can't post a Ledger Entry exactly when the corresponding event happens. To solve this, Ledgers use a logical clock.
Ledger Entries have two time timestamps:
posted
: the logical time for the Ledger Entrycreated
: the system time the Ledger Entry was recorded in FRAGMENT
You provide posted
in LedgerEntryInput when you call the API. FRAGMENT automatically generates created
based off internal system time.
You can post Ledger Entries at any point in time, including the future. You can also query balances at any timestamp. Posting a Ledger Entry will update all balances from the posted
time into the future.
g. Balances
#FRAGMENT supports querying a Ledger Account for its balances, historical balances, and balance changes.
Each Ledger Account has six balances that can be queried:
ownBalance
The sum of all Ledger Lines in the Ledger Account, excluding Ledger Lines in child Ledger Accounts.childBalance
The sum of all Ledger Lines in child Ledger Accounts in the same currency as this Ledger Account.balance
The sum of all Ledger Lines, including child Ledger Accounts in the same currency as this Ledger Account.ownBalances
The sum of all Ledger Lines in the Ledger Account in all currencies, excluding Ledger Lines in child Ledger Accounts.childBalances
The sum of all Ledger Lines in child Ledger Accounts in all currencies.balances
The sum of all Ledger Lines, including child Ledger Accounts in all currencies.
query GetBalances ($ledgerIk: SafeString!) {
ledger(ledger: {ik: $ledgerIk}) {
ledgerAccounts {
nodes {
id
name
type
ownBalance
childBalance
balance
childBalances {
nodes {
currency {
code
}
amount
}
}
balances {
nodes {
currency {
code
}
amount
}
}
}
}
}
}
For a Ledger with a single currency you will only use balance
and childBalance
. When tracking multiple currencies in the same Ledger, use balances
and childBalances
.
To query the balance at a particular point in time use the at
argument:
query GetBalances ($ledgerIk: SafeString!) {
ledger(ledger: {ik: $ledgerIk}) {
ledgerAccounts {
nodes {
id
name
type
end_of_year: balance(at: "1969")
end_of_month: balance(at: "1969-07")
end_of_day: balance(at: "1969-07-21")
end_of_hour: balance(at: "1969-07-21T02")
}
}
}
}
If you don't specify at
, you'll get the latest balance. at
is supported for all
balances and works down to the hour granularity.
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, and make multiple queries using aliases:
query GetBalanceChanges($ledgerIk: SafeString!) {
ledger(ledger: { ik: $ledgerIk }) {
ledgerAccounts {
nodes {
id
name
type
ownBalanceChange(period: "1969")
childBalanceChange(period: "1969")
balanceChange(period: "1969")
currentYear: balanceChange(period: "1969")
lastYear: balanceChange(period: "1968")
lastYearQ4: balanceChange(period: "1968-Q4")
lastYearDecember: balanceChange(period: "1968-12")
lastYearChristmas: balanceChange(period: "1968-12-25")
lastYearLastHour: balanceChange(period: "1968-12-31T23")
childBalanceChanges(period: "1969") {
nodes {
currency {
code
}
amount
}
}
balanceChanges(period: "1969") {
nodes {
currency {
code
}
amount
}
}
}
}
}
}
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: "1969-01-31")
will return the balance at midnight PT on that date, or 8am on 1969-02-01 UTC. - Querying
balanceChange(period: "1969")
will return the net change between 8am on 1969-01-01 UTC and 8am on 1969-01-01 UTC. This gives you the net change between midnights local time. - Daylight savings is ignored, so every day throughout the year has exactly 24 hours. Trust us on this one.