# Post Ledger Entries

Source: https://fragment.dev/guides/post-ledger-entries

[Documentation index](https://fragment.dev/llms.txt)

<a id="post-ledger-entries"></a>

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.

[Visual: postLedgerEntries. View on the HTML page.](https://fragment.dev/guides/post-ledger-entries)

<a id="schema"></a>

## Schema

Ledger Entries are defined in your Schema under the `ledgerEntries.types` key.

```json title="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 -):

```json title="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.

[Visual: accountingEquation. View on the HTML page.](https://fragment.dev/guides/post-ledger-entries)

<a id="post-to-the-api"></a>

## Post to the API

Call the `addLedgerEntry` mutation to post a Ledger Entry:

```graphql title="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`, `typeVersion` (optional), and the required `parameters`. If not provided `typeVersion` will default to 1.

```json title="addLedgerEntry variables"
{
  "ik": "add-ledger-entry",
  "entry": {
    "ledger": {
      "ik": "quickstart-ledger"
    },
    "type": "user_funds_account",
    "typeVersion": 1,
    "posted": "1234-01-01T01:01:01",
    "parameters": {
      "user_id": "testing-user",
      "funding_amount": "200"
    }
  }
}
```

<a id="post-to-the-api-minor-units"></a>

### Minor units

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

<a id="post-to-the-api-idempotency"></a>

### 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](https://fragment.dev/markdown/api-reference/ledger-overview.md#idempotency).

<a id="post-to-the-api-timestamps"></a>

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

<a id="post-to-the-api-net-amounts"></a>

### Net amounts

By default lines are aggregated and filtered before posting:

- Lines targeting the same Ledger Account and currency are summed into a single line
- If the sum is zero, the line is not posted, unless all lines in an entry are zero

This is controlled by the `postLinesAs` field on a Ledger Entry type. The available values are:

- `net_amounts`: Lines targeting the same account, currency, and `tx` are aggregated into a single line with the summed amount. Lines that sum to zero are skipped unless all lines sum to zero.
- `skip_zero_lines`: Lines are posted as-is without aggregation, but lines with a zero amount are skipped unless all lines are zero.
- `raw_lines`: Lines are posted as-is without aggregation or filtering.

<a id="linking-ledger-accounts"></a>

## Linking Ledger Accounts

Ledger Accounts can be linked to external financial systems to [reconcile](https://fragment.dev/markdown/guides/reconcile-payments.md) your Ledger with payments.

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.

```json title="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`:

```graphql title="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.

```json title="reconcileTx variables"
{
  "entry": {
    "type": "reconciliation-type",
    "ledger": {
      "ik": "quickstart-ledger"
    },
    "parameters": {
      "bank_transaction_id": "tx_1234"
    }
  }
}
```

Read more about creating and using linked Ledger Accounts in [Reconcile transactions](https://fragment.dev/markdown/guides/reconcile-payments.md).

<a id="currencies"></a>

## Currencies

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

```json title="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](https://fragment.dev/markdown/guides/handle-currencies.md).

<a id="repeated-lines"></a>

## Repeated Lines

Repeated lines allow you to post a variable number of Ledger Lines. For example, a batch payment that funds N wallets in a single entry, where N is determined at runtime.

To specify the parameter used as the source array, set `repeated.key`:

```json title="Ledger Entry with repeated lines"
{
  "type": "fund_wallets",
  "description": "Fund wallets from bank",
  "lines": [
    {
      "key": "record_incoming_top_ups",
      "account": { "path": "assets/receivables/top_up:{{top_up_id}}" },
      "amount": "{{amount}}",
      "repeated": { "key": "top_ups" }
    },
    {
      "key": "top_up_wallet",
      "account": { "path": "liabilities/wallet:{{wallet_id}}" },
      "amount": "{{amount}}",
      "repeated": { "key": "top_ups" }
    }
  ]
}
```

In this example, the `record_incoming_top_ups` and `top_up_wallet` lines will be posted once for each element in the `top_ups` array:

```json title="addLedgerEntry variables with repeated lines"
{
  "ik": "batch-fund-wallets",
  "entry": {
    "ledger": { "ik": "my-ledger" },
    "type": "fund_wallets",
    "parameters": {
      "top_ups": [
        { "wallet_id": "user-a", "top_up_id": "t-1", "amount": "100" },
        { "wallet_id": "user-b", "top_up_id": "t-2", "amount": "200" }
      ]
    }
  }
}
```

This produces four Ledger Lines: two repeated lines for each of the two items.

When repeated lines expand, multiple lines may target the same Ledger Account. For example, one line posts to a unique template account per item while the other posts to a shared account:

```json title="Repeated lines with shared accounts"
{
  "type": "batch_payout",
  "description": "Batch payout",
  "postLinesAs": "net_amounts",
  "lines": [
    {
      "key": "decrease_pool",
      "account": { "path": "assets/pool" },
      "amount": "-{{amount}}",
      "repeated": { "key": "payouts" }
    },
    {
      "key": "increase_recipient_balance",
      "account": { "path": "liabilities/recipients:{{recipient_id}}" },
      "amount": "{{amount}}",
      "repeated": { "key": "payouts" }
    }
  ]
}
```

With `postLinesAs: raw_lines` or `skip_zero_lines`, this would produce four lines. With `net_amounts`, lines targeting the same account are combined, so only three lines are posted. This lets you increase the number of items you can pass as parameters before hitting the limit of 30 Ledger Lines allowed within a ledger Entry.

When using repeated lines with [linked Ledger Accounts](https://fragment.dev/markdown/guides/reconcile-payments.md#linked-ledger-accounts), the constraint of each Ledger Line reconciling a single Tx still applies. This requires each array item to include a unique `tx_id`, when posting using `raw_lines` or `skip_zero_lines`. When posting using `net_amounts`, lines are only combined if they also share the same `tx_id`. The total amount of combined lines must equal the Tx amount.

Restrictions:

- Only one repeated key is allowed per Ledger Entry type. All repeated lines and [repeated conditions](https://fragment.dev/markdown/guides/configure-consistency.md#repeated-conditions) must share the same key.
- Each repeated key group must have at least 2 lines that balance
- All elements in the array must have the same set of keys and cannot overlap with top-level parameters

<a id="using-conditions"></a>

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

```json title="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": {
        "totalBalance": {
          "gte": "0"
        }
      }
    }
  ]
}
```

Read more about using Ledger Entry Conditions in [Configure consistency](https://fragment.dev/markdown/guides/configure-consistency.md).

<a id="using-tags"></a>

## Using Tags

Ledger Entry tags provide a way to store arbitrary key-value pairs on your Ledger Entries. You can define tags in your Schema or add them at runtime when posting entries.

```json title="Ledger Entry Type with Tags"
{
  "type": "user_funds_account",
  "description": "Fund {{user_id}}",
  "lines": [
    {
      "account": {
        "path": "liabilities/users:{{user_id}}/available"
      },
      "key": "increase_user_balance",
      "amount": "{{funding_amount}}"
    }
  ],
  "tags": [
    {
      "key": "user",
      "value": "{{user_id}}"
    },
    {
      "key": "deposit_flow",
      "value": "{{deposit_flow_id}}"
    }
  ]
}
```

Read more about using and managing tags in [Store metadata](https://fragment.dev/markdown/guides/store-metadata.md).

<a id="using-groups"></a>

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

```json title="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](https://fragment.dev/markdown/guides/group-ledger-entries.md).

<a id="updating-entries"></a>

## Updating Entries

There are two ways to update a Ledger Entry after it has been posted:

1. Use `updateLedgerEntry` to modify tags and groups
2. Reverse the entry and post a new one to correct mistakes or change other attributes

To update a Ledger Entry's tags and groups use the `updateLedgerEntry` mutation. This is useful for adding metadata or associating entries with groups after they've been posted.

```graphql title="updateLedgerEntry mutation"
mutation UpdateLedgerEntry(
  $ledgerEntry: LedgerEntryMatchInput!
  $update: UpdateLedgerEntryInput!
) {
  updateLedgerEntry(
    ledgerEntry: $ledgerEntry,
    update: $update
  ) {
    __typename
    ... on UpdateLedgerEntryResult {
      entry {
        type
        ik
        tags {
          key
          value
        }
        groups {
          key
          value
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
```

Read more about [updating tags](https://fragment.dev/markdown/guides/store-metadata.md#updating-entry-tags) and [updating groups](https://fragment.dev/markdown/guides/group-ledger-entries.md#updating-entry-groups).

To correct mistakes or change other attributes of a Ledger Entry (like amounts, accounts, or timestamps), you can reverse the entry and post a new one.

```graphql title="reverseLedgerEntry mutation"
mutation ReverseLedgerEntry(
  $id: ID!
) {
  reverseLedgerEntry(
    id: $id
  ) {
    __typename
    ... on ReverseLedgerEntryResult {
      reversingLedgerEntry {
        ik
        id
        type
        posted
        created
      }
      reversedLedgerEntry {
        ik
        id
        type
        posted
        created
        reversedAt
      }
    }
    ... on Error {
      code
      message
    }
  }
}
```

Read more about [reversing entries](https://fragment.dev/markdown/guides/reverse-ledger-entries.md).

<a id="runtime-entries"></a>

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

```json title="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`:

```graphql title="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
    }
  }
}
```

```json title="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"
        }

    ]
  }
}
```

<a id="batch-ledger-entries"></a>

## Batch Ledger Entries

Call the `addLedgerEntries` mutation to post several Ledger Entries in one atomic
transaction. Either every Ledger Entry in the batch posts or none of them do.

```graphql title="addLedgerEntries mutation"
mutation AddLedgerEntries($entries: [AddLedgerEntryInput!]!) {
  addLedgerEntries(entries: $entries) {
    __typename
    ... on AddLedgerEntriesResult {
      results {
        isIkReplay
        entry {
          type
          created
          posted
        }
        lines {
          amount
          account {
            path
          }
        }
      }
    }
    ... on AddLedgerEntriesError {
      code
      message
      errors {
        ik
        code
        message
      }
    }
    ... on Error {
      code
      message
    }
  }
}
```

Set the `ik` for each Ledger Entry in a batch. A batch of Ledger Entries
can contain more than one Ledger Entry `type`.

```json title="addLedgerEntries variables"
{
  "entries": [
    {
      "ik": "add-ledger-entries-first",
      "entry": {
        "ledger": { "ik": "quickstart-ledger" },
        "type": "user_funds_account",
        "posted": "1969-06-16T13:32:00.000Z",
        "parameters": {
          "user_id": "testing-user",
          "funding_amount": "200"
        }
      }
    },
    {
      "ik": "add-ledger-entries-second",
      "entry": {
        "ledger": { "ik": "quickstart-ledger" },
        "type": "user_funds_account",
        "posted": "1969-06-21T02:56:05.000Z",
        "parameters": {
          "user_id": "testing-user",
          "funding_amount": "350"
        }
      }
    }
  ]
}
```

The `AddLedgerEntriesResult` response returns the list of Ledger Entries posted, in the same order as the request.

Restrictions:

- A batch can have up to 30 Ledger Lines.
- The `ik` for each Ledger Entry within a batch must be unique.

Handling errors:
- `AddLedgerEntriesError` is returned when a batch fails due to one or more invalid
  Ledger Entries. The response contains a list of `ik`s with their respective error messages.
- `addLedgerEntries` is idempotent. Retrying a failed batch with the same `ik`s is safe.

<a id="batch-ledger-entries-entry-conditions"></a>

### Entry conditions

Entry conditions are merged and evaluated when a batch of Ledger Entries is posted. If the merged
condition is not met, the batch is not posted and the mutation returns a `BadRequestError` with code
`conditional_request_failed`.

If a batch contains incompatible conditions, a `BadRequestError` with code `unsatisfiable_batch_condition`
is returned.
