A Introduction#

FRAGMENT is a toolkit for building products that move and track money. It includes an API and Dashboard for designing, implementing, and operating your Ledger.

Don't have a FRAGMENT workspace? Get access.

B Design your Ledger#

A Ledger uses a Schema to define functionality for a specific product and use case. A Schema may be shared across multiple Ledgers. Updating a Schema will trigger migrations to update each Ledger. Use the Ledger designer in the Dashboard to model and store your Schema.

Ledgers track money using:

  • Ledger Accounts, balances that represent the financial state of a business
  • Ledger Entries, financial events that update Ledger Accounts

a. Ledger Accounts

#

A Ledger Account has a balance. Changes to a Ledger Account's balance are called Ledger Lines.

There are four types of Ledger Accounts, split into two layers:

State

Assets: what you own
Liabilities: what you owe

Change

Income: what you've earned
Expense: what you've spent

State Ledger Accounts track your product's financial relationships with your bank, payment systems and users. Balance Sheets are a report of State Ledger Account balances.

Change Ledger Accounts track when and how your product makes a profit or loss. They produce Income Statements.

Within a Schema, the chartOfAccounts key contains a nested tree of Ledger Accounts, up to a maximum depth of 10:

Ledger Accounts that share a parent require a unique key, but the same key can be used in different parts of the tree.

For some Ledger Accounts, you must set additional properties:

  • linkedAccount enables reconciliation with an external system
  • template allows multiple instances to be created on demand
  • currencyMode configures the account's currency mode, single or multi
  • consistencyConfig configures the whether the Ledger Account's balances and Ledger Lines are strongly or eventually consistent

b. Ledger Entries

#

A Ledger Entry is a single update to a Ledger. Define a Ledger Entry type in your Schema for every financial event in your product and bank.

A Ledger Entry must be balanced, which means it follows the Accounting Equation:


Assets - Liabilities
=
Income - Expenses

How you balance a Ledger Entry depends upon its net effect to the Ledger's balances.

When the net change to the State Ledger Accounts is zero, the financial event being recorded did not change the net worth of the business. In this example, an increase to an asset account is balanced by an increase in a liability account:

When the net change to the State Ledger Accounts is non-zero, the financial event being recorded made a profit or loss. In this example, a difference in asset and liability accounts is balanced by an increase in an income account.

C Deploy your Ledger#

Once you've designed your Ledger, you can deploy it using the dashboard, the API, or by embedding our CLI in your CI.

a. Dashboard

#

You can edit and store your Schema and create Ledgers directly from the FRAGMENT Dashboard. This is useful during development, but is not recommended for production workflows.

b. API

#

You can call the API to store your Schema and create Ledgers. This is useful if you want to automate your Ledger deployment or have multiple environments which you want to keep in-sync. If you are creating many Schemas and Ledgers, you can also call the storeSchema and createLedger APIs directly from your product.

Call storeSchema to store a Schema. Depending on your use case, you may share one Schema for all your users, or create a Schema per user.

storeSchema query
mutation QuickstartStoreSchema($schema: SchemaInput!) {
  storeSchema(schema: $schema) {
    ... on StoreSchemaResult {
      schema {
        key
        name
        version {
          version
          created
          json
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
storeSchema variables
{
  "schema": {
    "key": "quickstart-schema",
    "name": "Quickstart Schema",
    "chartOfAccounts": {
      "defaultCurrency": {
        "code": "USD"
      },
      "defaultCurrencyMode": "single",
      "accounts": [
        {
          "key": "assets",
          "type": "asset",
          "children": [
            {
              "key": "banks",
              "children": [
                {
                  "key": "user-cash"
                }
              ]
            }
          ]
        },
        {
          "key": "liabilities",
          "type": "liability",
          "children": [
            {
              "key": "users",
              "template": true,
              "consistencyConfig": {
                "ownBalanceUpdates": "strong"
              },
              "children": [
                {
                  "key": "available"
                },
                {
                  "key": "pending"
                }
              ]
            }
          ]
        },
        {
          "key": "income",
          "type": "income"
        },
        {
          "key": "expense",
          "type": "expense",
          "children": []
        }
      ]
    },
    "ledgerEntries": {
      "types": [
        {
          "type": "user_funds_account",
          "description": "Funding {{user_id}} for {{funding_amount}}.",
          "lines": [
            {
              "account": { "path": "assets/banks/user-cash" },
              "key": "funds_arrive_in_bank",
              "amount": "{{funding_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "key": "increase_user_balance",
              "amount": "{{funding_amount}}"
            }
          ]
        },
        {
          "type": "p2p_transfer",
          "description": "P2P of {{transfer_amount}} from {{from_user_id}} to {{to_user_id}}.",
          "lines": [
            {
              "account": {
                "path": "liabilities/users:{{from_user_id}}/available"
              },
              "key": "decrease_from_user",
              "amount": "-{{transfer_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{to_user_id}}/available" },
              "key": "increase_to_user",
              "amount": "{{transfer_amount}}"
            }
          ],
          "conditions": [
            {
              "account": {
                "path": "liabilities/users:{{from_user_id}}/available"
              },
              "postcondition": {
                "ownBalance": {
                  "gte": "0"
                }
              }
            }
          ]
        }
      ]
    }
  }
}
 

Create a Ledger using the createLedger mutation.

createLedger
mutation QuickstartCreateLedger(
  $ik: SafeString!
  $ledger: CreateLedgerInput!
  $schema: SchemaMatchInput
) {
  createLedger(
    ik: $ik,
    ledger: $ledger,
    schema:$schema
  ) {
    ... on CreateLedgerResult {
      ledger {
        ik
        name
        created
        schema {
          key
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}

The schema.key field is set to the key from the storeSchema API call.

createLedger variables
{
  "ik": "quickstart-ledger",
  "ledger": {
    "name": "Quickstart Ledger"
  },
  "schema": {
    "key": "quickstart-schema"
  }
}

c. CLI

#

The FRAGMENT CLI can be installed in your CI and used to store your Schema.

Here's an example of how you can use the CLI in a Github Action workflow:

Fragment CLI in Github Actions
- name: Set up Homebrew
  id: set-up-homebrew
  uses: Homebrew/actions/setup-homebrew@master
- name: Install Fragment CLI
  run: |
    brew tap fragment-dev/tap
    brew install fragment-dev/tap/fragment-cli
    echo "Fragment CLI installed"
- name: Authenticate with Fragment
  run: |
    fragment login \
      --client-id ${{ vars.FRAGMENT_CLIENT_ID }} \
      --client-secret ${{ vars.FRAGMENT_CLIENT_SECRET }} \
      --api-url ${{ vars.FRAGMENT_API_URL }} \
      --oauth-url ${{ vars.FRAGMENT_OAUTH_URL }} \
      --oauth-scope ${{ vars.FRAGMENT_OAUTH_SCOPE }}
- name: Store Schema
  run: |
    fragment store-schema --path my-schema.jsonc
 

Read the CLI Command Reference to learn more about the FRAGMENT CLI.

D Install the SDK#

FRAGMENT publishes SDKs in TypeScript, Go, and Ruby. The SDKs are open-source, implement authentication, and come with predefined GraphQL queries to help you get started.

a. Setup the CLI

#

The FRAGMENT CLI is used to generate GraphQL queries specific to your Schema. The SDKs use the generated queries to give you a strongly-typed interface for posting Ledger Entries defined in your Ledger.

Install the CLI using Homebrew:

brew tap fragment-dev/tap &&\
  brew install fragment-dev/tap/fragment-cli

Authenticate the CLI to your FRAGMENT workspace, run the login command:

fragment login

b. TypeScript

#

The TypeScript Node SDK is available at @fragment-dev/node-client.

Using npm
npm install --save @fragment-dev/node-client
Using yarn
yarn add @fragment-dev/node-client

Create an API client in the FRAGMENT dashboard. Initialize the SDK using the credentials:

import {
  createFragmentClient
} from '@fragment-dev/node-client';

const fragment = createFragmentClient({
  params: {
    clientId: "<Client ID>",
    clientSecret: "<Client Secret>",
    apiUrl: "<API URL>",
    authUrl: "<OAuth URL>",
    scope: "<OAuth Scope>",
  },
});

// Verify the SDK is authenticated by retrieving
// the workspace
const { workspace } = await fragment.getWorkspace();
console.log('Workspace Name:', workspace.name);

Read the SDK's README for additional code examples.

Generate Queries#

This workflow is a two-step process:

  1. Use the CLI to generate GraphQL queries.
  2. Use the SDK to translate these queries to a TypeScript GraphQL client you can embed within your application.

Run the get-schema CLI command to download your Schema locally to fragment/schema.jsonc:

fragment get-schema --output=fragment/schema.jsonc

Run the gen-graphql CLI command to generate the GraphQL queries:

fragment gen-graphql \
  --path=fragment/schema.jsonc \
  --output=fragment/queries.graphql

This will create a queries.graphql file which you will use to generate the GraphQL client.

Generate Client#

Run the TypeScript codegen to generate the GraphQL client code:

Using npx
npx fragment-node-client-codegen \
  --input=fragment/queries.graphql \
  --outputFilename=fragment/fragment-client.ts
Using yarn
yarn fragment-node-client-codegen \
  --input=fragment/queries.graphql \
  --outputFilename=fragment/fragment-client.ts

Pass the getSdk function from the generated file to createFragmentClient to use the queries:

import {
  createFragmentClient
} from "@fragment-dev/node-client";

import {
  getSdk
} from './fragment/fragment-client';

const fragment = createFragmentClient({
  params: {
    clientId: "<Client ID>",
    clientSecret: "<Client Secret>",
    apiUrl: "<API URL>",
    authUrl: "<OAuth URL>",
    scope: "<OAuth Scope>",
  },
  getSdk,
});

// The returned client includes the
// pre-defined queries as well as
// the queries generated by the CLI.
await fragment.Post_YourLedgerEntry({});

c. Python

#

The Python SDK is available at github.com/fragment-dev/fragment-python.

To install:

Using pip
pip install fragment-python
Using poetry
poetry add fragment-python

Create an API client in the FRAGMENT dashboard. Initialize the client using the credentials:

from fragment.sdk.client import Client

client = Client(
  client_id="<Client ID>",
  client_secret="<Client Secret>",
  api_url="<API URL>",
  auth_url="<OAuth URL>",
  auth_scope="<OAuth Scope>",
)

async def print_workspace():
  response = await client.get_workspace()
  print(response.workspace.name)

import asyncio
loop.get_event_loop().run_until_complete(
  print_workspace())

Read the SDK's README for additional code examples.

Generate Queries#

This workflow is a two-step process:

  1. Use the CLI to generate GraphQL queries.
  2. Use the SDK to translate these queries to a Python GraphQL client you can embed within your application.

Run the get-schema CLI command to download your Schema locally to fragment/schema.jsonc:

fragment get-schema --output=fragment/schema.jsonc

Run the gen-graphql CLI command to generate the GraphQL queries:

fragment gen-graphql \
  --path=fragment_lib/schema.jsonc \
  --output=fragment_lib/queries/queries.graphql

This will create a queries.graphql file which you will use to generate the GraphQL client.

Generate Client#

Run the Python codegen to generate the Python GraphQL client code:

fragment-python-client-codegen \
  --input-dir=fragment_lib/queries \
  --target-package-name=sdk \
  --output-dir=fragment_lib

You can optionally provide the --sync flag to generate a synchronous client.

Instantiate the generated client from fragment_lib/sdk/client.py:

from .fragment_lib.sdk.client import Client

# The generated client includes the pre-defined
# queries as well as the queries generated by the CLI.
client = Client(
  client_id="<Client ID>",
  client_secret="<Client Secret>",
  api_url="<API URL>",
  auth_url="<OAuth URL>",
  auth_scope="<OAuth Scope>",
)

async def print_workspace_and_post_entry():
  response = await client.get_workspace()
  print(response.workspace.name)

  await client.post_your_ledger_entry_type(...)

import asyncio
loop.get_event_loop().run_until_complete(
  print_workspace_and_post_entry())

d. Go

#

The Go SDK is available at github.com/fragment-dev/fragment-go.

To install, run:

go get 'github.com/fragment-dev/fragment-go'

Create an API client in the FRAGMENT dashboard. Initialize the SDK using the credentials:

package main

import (
  "context"
  "fmt"

  "github.com/fragment-dev/fragment-go/auth"
  "github.com/fragment-dev/fragment-go/queries"
)

func main() {
  ctx, err := auth.GetAuthenticatedContext(
    context.Background(),
    &auth.GetTokenParams{
      ClientID: "<Client ID>",
      ClientSecret: "<Client Secret>",
      ApiUrl: "<API URL>",
      AuthUrl: "<OAuth URL>",
      Scope: "<OAuth Scope>",
    }
  )
  if err != nil {
    fmt.Println(err)
    return
  }

  // Verify the SDK is authenticated by retrieving
  // the workspace
  response, _ := queries.GetWorkspace(ctx)
}

Read the SDK's README for additional code examples.

Generate Queries#

This workflow is a two-step process:

  1. Use the CLI to generate GraphQL queries.
  2. Use the SDK to translate these queries to methods you can embed within your application.

Run the get-schema CLI command to download your Schema locally to fragment/schema.jsonc:

fragment get-schema --output=fragment/schema.jsonc

Run the gen-graphql CLI command to generate the GraphQL queries:

fragment gen-graphql \
  --path=fragment/schema.jsonc \
  --output=fragment/queries.graphql

This will create a queries.graphql file which you will use to generate the corresponding Go methods.

Generate Code#

Run the Go SDK codegen to generate methods for each GraphQL query:

go run github.com/fragment-dev/fragment-go \
  --input=fragment/queries.graphql \
  --output=fragment/generated.go \
  --package=main

You can then issue the GraphQL request from your main package:

package main

import (
  "context"
  "fmt"

  "github.com/fragment-dev/fragment-go/auth"
)

func main() {
  ctx, err := auth.GetAuthenticatedContext(
    context.Background(),
    &auth.GetTokenParams{
      ClientID: "<Client ID>",
      ClientSecret: "<Client Secret>",
      ApiUrl: "<API URL>",
      AuthUrl: "<OAuth URL>",
      Scope: "<OAuth Scope>",
    }
  )
  if err != nil {
    fmt.Println(err)
    return
  }

  response, _ := Post_YourLedgerEntryType(
    ctx,
    ...
  )
}

e. Ruby

#

The Ruby SDK is available at fragment-dev.

To install, run:

gem install fragment-dev

Create an API client in the FRAGMENT dashboard. Initialize the SDK using the credentials:

require 'fragment_client'

fragment = FragmentClient.new(
  "<Client ID>",
  "<Client Secret>",
  api_url: "<API URL>",
  oauth_url: "<OAuth URL>",
  oauth_scope: "<OAuth Scope>"
)

// Verify the SDK is authenticated by retrieving
// the workspace
workspace = fragment.get_workspace()

Read the SDK's README for additional code examples.

Generate Queries#

This workflow is a two-step process:

  1. Use the CLI to generate GraphQL queries.
  2. Use the SDK to translate these queries to methods you can embed within your application.

Run the get-schema CLI command to download your Schema locally to fragment/schema.jsonc:

fragment get-schema --output=fragment/schema.jsonc

Run the gen-graphql CLI command to generate the GraphQL queries:

fragment gen-graphql \
  --path=fragment/schema.jsonc \
  --output=fragment/queries.graphql

This will create a queries.graphql file which you will provide as input to FragmentClient.

Use custom queries#

Initialize FragmentClient with the extra_queries_filenames keyword argument set to the path of the generated queries.graphql file:

require 'fragment_client'

fragment = FragmentClient.new(
  "<Client ID>",
  "<Client Secret>",
  api_url: "<API URL>",
  oauth_url: "<OAuth URL>",
  oauth_scope: "<OAuth Scope>",
  extra_queries_filenames:
    ["path/to/queries.graphql"]
)

fragment.post_your_ledger_entry_type()

f. Other Languages

#

FRAGMENT exposes a GraphQL API to write and read your data. The latest GraphQL schema is hosted at:

https://api.fragment.dev/schema.graphql

To create an SDK in a language not listed above:

  1. Use the FRAGMENT CLI to generate GraphQL queries from your Schema
  2. Generate an SDK using a GraphQL codegen tool for your langauge
  3. Implement authentication, retry and error handling logic
Generate Queries#

Run the get-schema CLI command to download your Schema locally to fragment/schema.jsonc:

fragment get-schema --output=fragment/schema.jsonc

Run the gen-graphql CLI command to generate the GraphQL queries:

fragment gen-graphql \
  --path=fragment/schema.jsonc \
  --output=fragment/queries.graphql

Provide the optional --include-standard-queries flag to include the set of standard GraphQL queries in the output.

This will create a queries.graphql file which you will provide as input to your codegen tool.

Some codegen tools require each GraphQL query to be in a separate file. You can use the --output-file-per-query flag:

fragment gen-graphql \
  --path=fragment/schema.jsonc \
  --output=fragment/queries \
  --output-file-per-query
Generate SDK#

Use a GraphQL codegen tool to generate the SDK. A list of clients is available on the GraphQL website.

Implement logic#

You will need to implement authentication by customizing the generated SDK. FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. Read the API Authentication section to learn more.

You should also add support for handling errors and retries. See the API Errors section.

E Post Ledger Entries#

Posting a Ledger Entry to your Ledger is a two-step process:

  1. Define the structure of the Ledger Entry in your Schema.
  2. Post the Ledger Entry using the API.

a. Schema

#

Ledger Entries are defined in your Schema under the ledgerEntries.types key. Every Ledger Entry has a type that must be unique within the Schema.

Ledger Entry definition
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Fund {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "account": { 
              "path": "assets/banks/user-cash"
            },
            "key": "funds_arrive_in_bank",
            "amount": "{{funding_amount}}"
          },
          {
            "account": { 
              "path": "liabilities/users:{{user_id}}/available"
            },
            "key": "increase_user_balance",
            "amount": "{{funding_amount}}"
          }
        ]
      }
    ]
  }
}

The amounts of a Ledger Line can be parameterized using {{handlebar}} syntax and can contain basic arithmetic (+ or -):

Ledger Entry definition
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_with_fee",
        "description": "Fund {{user_id}} for {{funding_amount}} with {{fee_amount}} fee",
        "lines": [
          {
            "account": { 
              "path": "assets/banks/user-cash"
            },
            "key": "funds_arrive_in_bank",
            "amount": "{{funding_amount}}"
          },
          {
            "account": { 
              "path": "liabilities/users:{{user_id}}/available"
            },
            "key": "increase_user_balance",
            "amount": "{{funding_amount}} - {{fee_amount}}"
          },
          {
            "account": { "path": "income/funding-fees" },
            "key": "take_fee",
            "amount": "{{fee_amount}}"
          }
        ]
      }
    ]
  }
}

Ledger Entries must be balanced by the Accounting Equation. If they are not, the Ledger designer throws an error.

b. Post to the API

#

Call the addLedgerEntry mutation to post a Ledger Entry:

addLedgerEntry mutation
mutation AddLedgerEntry(
  $ik: SafeString!
  $entry: LedgerEntryInput!
) {
  addLedgerEntry(
    ik: $ik,
    entry: $entry
  ) {
    __typename
    ... on AddLedgerEntryResult {
      entry {
        type
        created
        posted
      }
      lines {
        amount
        key
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 

Set the Ledger Entry's type and the required parameters.

addLedgerEntry variables
{
  "ik": "add-ledger-entry",
  "entry": {
    "ledger": {
      "ik": "ledger-ik"
    },
    "type": "user_funds_account",
    "posted": "1234-01-01T01:01:01",
    "parameters": {
      "user_id": "testing-user",
      "funding_amount": "200"
    }
  }
}
Minor Units#

All numbers in FRAGMENT are integers representing the smallest unit, encoded as strings. For example, USD $2.50 is provided as "250".

Tags#

You can specify tags on a Ledger Entry to store arbitrary key-value pairs. See Store metadata.

Idempotency#

To ensure a Ledger Entry is only posted once, provide an Idempotency Key ik to the addLedgerEntry mutation. This identifies the Ledger Entry and lets you safely retry the API call. See Integrate the API.

Timestamps#

Ledger Entries have two timestamps:

  • posted, the time the money movement event happened. You provide this to the addLedgerEntry API call.
  • created, the time at which the Ledger Entry was posted to the API. FRAGMENT auto-generates this value.

You can post entries with a posted timestamp in the past or future. Posting a Ledger Entry updates all balances from the posted time into the future.

c. Linked Accounts

#

Linked Ledger Accounts are used for reconciling your Ledger with your external financial systems.

To define a Ledger Entry to a Linked Ledger Account, you must specify the tx of the Ledger Line. This is the ID of the transaction at the external system.

Linked Ledger Account Entry
{
  "key": "linked-ledger-account-schema",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "reconciliation-type",
        "lines": [
          {
            "key": "reconciliation-line-key",
            "account": { "path": "bank" },
            "amount": "{{some_amount}}",
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          },
          {...other line}
        ]
      }
    ]
  }
}

To post Ledger Entries to a Linked Ledger Account, call reconcileTx instead of addLedgerEntry:

reconcileTx query
mutation ReconcileTx(
    $entry: LedgerEntryInput!
  ) {
    reconcileTx(entry: $entry) {
      __typename
      ... on ReconcileTxResult {
        entry {
          ik
          posted
          created
        }
        isIkReplay
        lines {
          amount
          currency {
            code
          }
        }
      }
      ... on Error {
        code
        message
      }
    }
  }
 

The query variables are the same as addLedgerEntry, except posted and ik are omitted. They are inferred from the external Tx.

reconcileTx variables
{
  "entry": {
    "type": "reconciliation-type",
    "ledger": {
      "ik": "ledger-ik"
    },
    "parameters": {
      "bank_transaction_id": "tx_1234"
    }
  }
}

Read more about creating and using Linked Ledger Accounts in Reconcile transactions.

d. Currencies

#

If a Ledger Account is in currencyMode: multi, you must specify the currency of the Ledger Lines posted to it.

Ledger Entry definition
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Funding {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "account": { "path": "assets/banks/user-cash" },
            "key": "funds_arrive_in_bank",
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "USD"
            }
          },
          {
            "account": { "path": "liabilities/users:{{user_id}}/available" },
            "key": "increase_user_balance",
            "amount": "{{funding_amount}}",
            "currency": {
              "code": "USD"
            }
          }
        ]
      }
    ]
  }
}

Read more about how to implement a product that handles multiple currencies in Handle currencies.

e. Using conditions

#

Ledger Entry conditions are rules defined in your Schema used to manage concurrency and enforce correctness within your Ledger. If a condition is not met, the Ledger Entry is not posted and the mutation throws a BadRequestError.

Conditions are defined in the Schema:

Ledger Entry Template with conditions
{
  "type": "user_withdraws_funds",
  "description": "{{user_id}} withdraws for {{withdraw_amount}}",
  "lines": [
    {
      "account": { "path": "assets/banks/user-cash" },
      "key": "funds_leave_bank",
      "amount": "-{{withdraw_amount}}"
    },
    {
      "account": { "path": "liabilities/users:{{user_id}}/available" },
      "key": "decrease_user_balance",
      "amount": "-{{withdraw_amount}}"
    }
  ],
  "conditions": [
    {
      "account": {
        "path": "liabilities/users:{{user_id}}/available"
      },
      "postcondition": {
        "ownBalance": {
          "gte": "0"
        }
      }
    }
  ]
}

Read more about using Ledger Entry Conditions in Configure consistency.

f. Using Groups

#

Ledger Entry Groups provide a way to tie together related Ledger Entries. You can configure them on a Ledger Entry Type in the Schema.

Ledger Entry Type with Groups
{
  "type": "user_initiates_withdrawal",
  "description": "{{user_id}} initiates withdrawal",
  "lines": [
    {
      "account": {
        "path": "liabilities/users:{{user_id}}/available"
      },
      "key": "decrease_user_balance",
      "amount": "-{{withdraw_amount}}"
    },
    {...other line}
  ],
  "groups": [
    {
      "key": "withdrawal",
      "value": "{{withdrawal_id}}"
    }
  ]
}

Read more about using Ledger Entry Groups in Group Ledger Entries.

g. Runtime Entries

#

You can define a Ledger Entry whose Ledger Lines are defined at runtime.

This can be useful if you're building a product where your end user, not you, defines the Ledger Entry structure. This is common for FRAGMENT users offering accounting services to their users.

To support runtime-defined Ledger Entries, omit the lines field in the Schema.

Runtime-defined Ledger Entry

{
  "type": "runtime_entry",
  "description": "Runtime-defined ledger entry"
}

Then, set the lines field when posting the Ledger Entry using addLedgerEntry or reconcileTx:

AddRuntimeLedgerEntry query
mutation AddRuntimeLedgerEntry(
  $ik: SafeString!
  $entry: LedgerEntryInput!
) {
  addLedgerEntry(
    ik: $ik,
    entry: $entry
  ) {
    ... on AddLedgerEntryResult {
      entry {
        type
        created
        posted
      }
      lines {
        amount
        key
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 
AddRuntimeLedgerEntry variables
{
  "ik": "add-arbitrary-ledger-entry",
  "entry": {
    "type": "runtime_entry",
    "lines": [
        {
          "account": { "path": "assets/banks/user-cash" },
          "key": "funds_arrive_in_bank",
          "amount": "100"
        },
        {
          "account": { "path": "liabilities/users:test-user/available" },
          "key": "increase_user_balance",
          "amount": "100"
        }

    ]
  }
}
F Reconcile Transactions#

Your Ledger should accurately track the real-world movement of money within your product. This is done by reconciling transactions from your external financial systems with your Ledger.

The overall workflow is a three-step process:

  1. Transact at your external financial system.
  2. Sync the real-world money movement to FRAGMENT.
  3. Reconcile the transaction to a Ledger Entry using the API.

This whole process happens through a FRAGMENT Link.

d. Linked Accounts

#

After you set up the Link, create Linked Ledger Accounts, accounts in your Ledger that map 1:1 with accounts at your external financial system.

To define a Linked Ledger Account, set linkedAccount on a Ledger Account in your Schema:

Linked Ledger Account with hardcoded IDs
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "children": [{
          "key": "operating",
          "name": "Operating Bank",
          "linkedAccount": {
            "linkId": "some-link-id",
            "externalId": "bank-account-1"
          }
        }]
      }
    ]
  }
}

The linkId comes from the Link you create in the dashboard. The externalId is the ID of the account at your external financial system.

The CLI automatically replaces variables with ${ENV_VAR} syntax when running fragment store-schema. This lets you use different External Accounts per environment:

Linked Ledger Account with env var IDs
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "children": [{
          "key": "operating",
          "name": "Operating Bank",
          "linkedAccount": {
            "linkId": "${BANK_LINK_ID}",
            "externalId": "${BANK_ACCOUNT_ID}"
          }
        }]
      }
    ]
  }
}

To create a Linked Ledger Account that is part of a Ledger Account template, parameterize the linkedAccount field:

Linked Ledger Account with parameter IDs
{
  "chartOfAccounts": {
    "accounts": [
      {
        "key": "assets-root",
        "name": "Assets",
        "type": "asset",
        "children": [{
          "key": "operating",
          "name": "Operating Bank",
          "template": true,
          "linkedAccount": {
            "linkId": "{{BANK_LINK_ID}}",
            "externalId": "{{BANK_ACCOUNT_ID}}"
          }
        }]
      }
    ]
  }
}

These parameters are then required when posting a Ledger Entry to a Linked Ledger Account. This can be useful if you're creating bank accounts per customer, for example.

e. Transact externally

#

Transactions at your external financial system are initiated in one of two ways:

  • You initiate a payment by calling their API.
  • You are notified by the external system that a transaction has occurred. This can happen via webhook, by polling their API, or receiving a periodic transaction report.

f. Sync transactions

#

Once a transaction occurs, you need to sync it with your FRAGMENT Ledger.

If you're using a Native Link, FRAGMENT automatically syncs transactions. You can skip ahead to Reconcile transactions.

If you're using a Custom Link, your sync process should call syncCustomTxs. Your sync process can periodically enumerate transactions in your external system or be triggered by webhook.

You may also want to sync and reconcile when your product makes a payment. You should only sync transactions that are settled, not pending or declined.

Once you have a set of transactions to sync, call syncCustomTxs:

syncCustomTxs mutation
mutation SyncTransactions(
  $link: LinkMatchInput!
  $txs: [CustomTxInput!]!
) {
  syncCustomTxs(link: $link, txs: $txs) {
    __typename
    ... on SyncCustomTxsResult {
      txs {
        id
        externalId
        amount
        date
        description
      }
    }
    ... on Error {
      code
      message
    }
  }
}
syncCustomTxs variables
{
  "link": { "id": "some-link-id" },
  "txs": [
    {
      "account": {
        "externalId": "bank-account-1"
      },
      "externalId": "tx-1",
      "description": "Processed ACH batch",
      "amount": "-100",
      "posted": "1968-01-01"
    },
    {
      "account": {
        "externalId": "bank-account-2"
      },
      "externalId": "tx-2",
      "description": "Received RTP payment",
      "amount": "100",
      "posted": "1968-01-01T16:45:00Z"
    }
  ]
}

You should ensure that externalId is a stable and unique identifier for each transaction, within the scope of its account. This identifier enforces idempotentency. This identifier is typically the ID of the transaction at the external system. Make sure you use the lowest level transaction ID available, not the ID of a higher level construct that may be linked to multiple transactions, like a payment.

Calling syncCustomTxs with a different description for an existing externalId updates the name of the External Account. amount and posted timestamp are immutable.

You can sync transactions from different accounts in the same API call, but they must all belong to the same Custom Link.

g. Reconcile a Tx

#

To reconcile a transaction from an external system, follow the same two-step process as when posting Ledger Entries:

  1. Define the structure of the Ledger Entry in your Schema.
  2. Post the Ledger Entry using the API.

Ledger Entries posting to a Linked Ledger Account must specify the Tx from the External Account tied to the Ledger Account. This lets FRAGMENT know which transaction to reconcile:

LedgerEntrySchema with Linked Ledger Account
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_via_link",
        "description": "Funding {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "account": { "path": "assets/operating" },
            "key": "funds_arrive_in_bank",
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          },
          {
            "account": { "path": "liabilities/users:{{user_id}}/available" },
            "key": "increase_user_balance"
          }
        ]
      }
    ]
  }
}

Notes:

  • Ledger Lines posting to a Linked Ledger Account must match 1:1 with the Tx that's being reconciled.
  • bank_transaction_id represents the ID of the transaction at the external system.

Instead of calling addLedgerEntry, Linked Ledger Accounts use the reconcileTx mutation:

reconcileTx mutation
mutation ReconcileTx(
  $entry: LedgerEntryInput!
) {
  reconcileTx(
    entry: $entry
  ) {
    ... on ReconcileTxResult {
      entry {
        type
        created
        posted
      }
      lines {
        amount
        key
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 

The parameters look similar to addLedgerEntry. Specify the Ledger Entry type that you are using and provide the parameters defined in the Schema:

reconcileTx variables
{
  "entry": {
    "type": "user_funding",
    "ledger": {
      "ik": "ledger-ik"
    },
    "parameters": {
      "txId": "tx_12345",
      "customerId": "customer-1"
    }
  }
}

ik and posted are optional when posting Ledger Entries with reconcileTx:

  • ik: the Tx.externalId is used to ensure that reconciling a transaction is idempotent
  • posted: the timestamp of the Ledger Entry is taken from the Tx to ensure the Ledger mirrors the external system
Reconciling multiple Txs#

Book transfers are a common type of money movement which produce two Txs at your bank as part of one payment.

To reconcile multiple Txs using reconcileTx:

LedgerEntrySchema with multiple Txs
{
  "key": "schema-key",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account_via_link",
        "description": "Funding {{user_id}} for {{funding_amount}}",
        "lines": [
          {
            "key": "funds_arrive_in_operating_bank",
            "account": { "path": "assets/operating-bank-account" },
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          },
          {
            "key": "funds_leave_holding_bank",
            "account": { "path": "assets/holding-bank-account" },
            "tx": {
              "externalId": "{{bank_transaction_id}}"
            }
          }
        ]
      }
    ]
  }
}

Notes:

  • Reconciling multiple Txs is only permitted if they have the same posted timestamp.
  • You can only reconcile multiple Txs within the same Link.

h. Unreconciled Txs

#

Transactions synced to FRAGMENT but not reconciled to a Ledger are considered unreconciled.

You can query unreconciled transactions in a Linked Ledger Account using the unreconciledTxs field on theLedgerAccount. It is recommended to periodically call this query to ensure your Ledger stays up to date. It can also be used to build reconciliation UIs:

unreconciledTxs query
query GetUnreconciledTxs (
  $ledgerAccount: LedgerAccountMatchInput!
) {
  ledgerAccount(ledgerAccount: $ledgerAccount) {
    id
    unreconciledTxs {
      nodes {
        id
        description
        amount
        externalId
        externalAccountId
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

The variables specify the path of the Linked Ledger Account you are querying and the Ledger it belongs to.

unreconciledTxs variables
{
  "ledgerAccount": {
    "path": "assets/operating",
    "ledger": {...}
  }
}

unreconciledTxs is eventually consistent, so if you've recently transacted at a Native Link or just synced manually for a Custom Link, you may not see the transaction in the query results immediately.

i. Stripe Txs

#

Transactions synced to FRAGMENT within a Stripe Link have special handling. Every Balance Transaction at Stripe has two amount fields:

  1. The gross amount of the transaction
  2. The fee paid for the transaction, to Stripe

This doesn't neatly map 1:1 with Fragment's recon model, so FRAGMENT creates two Txs for each Balance Transaction at Stripe: one for the gross amount and the other for the fee. This allows you to account for these amounts independently in your Ledger. The external IDs for these Txs are:

  1. {{stripe_tx_id}}_gross for the gross amount Tx
  2. {{stripe_tx_id}}_fee for the fee amount Tx

For example, a Stripe Balance Transaction with ID txn_123 will result in two Txs in FRAGMENT with external IDs txn_123_gross and txn_123_fee. You can reconcile both of these in a single reconcileTx call.

G Store metadata#

You can provide up to 10 tags on Ledger Entries to store arbitrary key-value pairs, like IDs from your product.

a. Schema Entry tags

#

You can define tags on Ledger Entry types in your Schema:

Schema-defined tags
{
  "key": "...",
  "chartOfAccounts": {...},
  "ledgerEntries": {
    "types": [
      {
        "type": "user_funds_account",
        "description": "Fund {{user_id}}",
        "lines": [
          {
            "key": "increase_user_balance",
            "account": { 
              "path": "liabilities/users:{{user_id}}/available"
            },
            "amount": "{{funding_amount}}"
          },
          {...other line}
        ],
        "tags": [
          {
            "key": "user",
            "value": "{{user_id}}"
          },
          {
            "key": "deposit_flow",
            "value": "{{deposit_flow_id}}"
          },
          {
            "key": "deposit_flow_type",
            "value": "ach"
          }
        ]
      }
    ]
  }
}

You can use the same parameter for both tag values and account paths.

When posting a Ledger Entry, include tag values as parameters:

addLedgerEntry mutation
mutation AddLedgerEntry(
  $ik: SafeString!
  $entry: LedgerEntryInput!
) {
  addLedgerEntry(
    ik: $ik,
    entry: $entry
  ) {
    __typename
    ... on AddLedgerEntryResult {
      entry {
        type
        ik
        tags {
          key
          value
        }
      }
      lines {
        amount
        description
        account {
          path
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 
addLedgerEntry variables with Schema-defined tags
{
  "ik": "fund-abc",
  "entry": {
    "ledger": {
      "ik": "ledger-ik"
    },
    "type": "user_funds_account",
    "parameters": {
      "user_id": "user-1",
      "funding_amount": "200",
      "deposit_flow_id": "deposit-123"
    }
  }
}

The Ledger Entry will have the tags you defined in the Schema:

addLedgerEntry response with tags
{
  "data": {
    "addLedgerEntry": {
      "entry": {
        "type": "user_funds_account",
        "ik": "fund-abc",
        "tags": [
          {
            "key": "user",
            "value": "user-1"
          },
          {
            "key": "deposit_flow",
            "value": "deposit-123"
          },
          {
            "key": "deposit_flow_type",
            "value": "ach"
          }
        ]
      },
      "lines": [...]
    }
  }
}

b. Runtime Entry tags

#

You can define tags at runtime when posting a Ledger Entry:

addLedgerEntry variables with runtime-defined tags
{
  "ik": "add-ledger-entry",
  "entry": {
    "ledger": {
      "ik": "ledger-ik"
    },
    "type": "user_funds_account",
    "parameters": {
      "user_id": "testing-user",
      "funding_amount": "200",
      "deposit_flow_id": "abc"
    },
    "tags": [
      {
        "key": "deposit_flow_type",
        "value": "ach"
      },
      {
        "key": "operator",
        "value": "alice"
      }
    ]
  }
}

If you define tags both at runtime and in the Schema, the Ledger Entry will get the combined set of tags:

addLedgerEntry response with tags
{
  "data": {
    "addLedgerEntry": {
      "entry": {
        "type": "user_funds_account",
        "ik": "fund-abc",
        "tags": [
          {
            "key": "user",
            "value": "user-1"
          },
          {
            "key": "deposit_flow",
            "value": "deposit-123"
          },
          {
            "key": "deposit_flow_type",
            "value": "ach"
          },
          {
            "key": "operator",
            "value": "alice"
          }
        ]
      },
      "lines": [...]
    }
  }
}

You can specify the same tag key in both places only if they have the same value.

c. Updating Entry tags

#

In addition to tags defined in your Schema, you can add and update tags on a posted Ledger Entry.

updateLedgerEntry mutation
mutation UpdateLedgerEntryTags(
  $ledgerEntry: LedgerEntryMatchInput!
  $update: UpdateLedgerEntryInput!
) {
  updateLedgerEntry(
    ledgerEntry: $ledgerEntry,
    update: $update
  ) {
    __typename
    ... on UpdateLedgerEntryResult {
      entry {
        type
        ik
        tags {
          key
          value
        }
        lines {
          nodes {
            amount
            description
            account {
              path
            }
          }
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
 
updateLedgerEntry variables to add and update tags
{
  "ledgerEntry": {
    "ik": "add-ledger-entry",
    "ledger": {
      "ik": "ledger-ik"
    }
  },
  "update": {
    "tags": [
      {
        "key": "operator",
        "value": "bob"
      },
      {
        "key": "supervisor",
        "value": "eve"
      }
    ]
  }
}

This is an additive operation:

  • If you specify a tag that already exists, it will be updated
  • If you specify a new tag, it will be added
  • If you don't specify an existing tag, it will remain unchanged

You can only update a Ledger Entry a maximum of 10 times.

H Read balances#

FRAGMENT supports querying a Ledger Account for its latest balances, historical balances and balance changes.