Cards

Ledgers tutorial for cards

Below are two different integration strategies for creating a digital wallet using Ledgers. Event Handlers allow you to stream information to be ledgered based on previously-created handler templates. The Transactions API gives you full control over when and how you ledger activity.

TAB-Event Handlers

Overview

The creation of the LT requires two objects to exist before we begin translating. The first is the:

Ledger Event Handler

POST /api/ledger_event_handlers

Example Request

POST /api/ledger_event_handlers

{
  "name": "Card Swipe",
  "description": "Handles card transactions",
  "variables": {
    "card_account": {
      "type": "ledger_account",
      "query": {
        "field": "metadata",
        "operator": "contains",
        "value": {
          "card_id": "{{ledgerable_event.custom_data.card_id}}"
        }
      }
    },
    "receivable_account": {
      "type": "ledger_account",
      "query": {
        "field": "id",
        "operator": "equals",
        // Fixed LA ID
        "value": "dfba9ebc-4c29-4894-93a1-9338218a2d76"
      }
    }
  },
  "ledger_transaction_template": {
    "description": "My Description",
    "status": "{{ledgerable_event.custom_data.status}}",
    "ledger_entries": [
      {
        "ledger_account_id": "{{variables.card_account.id}}",
        "amount": "{{ledgerable_event.custom_data.amount}}",
        "direction": "credit"
      },
      {
        "ledger_account_id": "{{variables.receivable_account.id}}",
        "amount": "{{ledgerable_event.custom_data.amount}}",
        "direction": "debit"
      }
    ]
  },
  "conditions": {
    "field": "ledgerable_event.name",
    "operator": "equals",
    "value": "Card Swipe"
  },
  "metadata": {
    "my_event_handler": "is_the_best"
  }
}

Example Response

{
  "id": "a6a314a8-b4f9-4684-a456-63f66e3cbdcd",
  "object": "ledger_event_handler",
  "name": "Card Swipe",
  "description": "Handles card transactions",
  "variables": {
    "card_account": {
      "type": "ledger_account",
      "query": {
        "field": "metadata",
        "operator": "contains",
        "value": {
          "card_id": "{{ledgerable_event.custom_data.card_id}}"
        }
      }
    },
    "receivable_account": {
      "type": "ledger_account",
      "query": {
        "field": "id",
        "operator": "equals",
        // Fixed LA ID
        "value": "d1f69141-0668-4cf4-9157-b80174fb2f0a"
      }
    }
  },
  "ledger_transaction_template": {
    "description": "My Description",
    "status": "{{ledgerable_event.custom_data.status}}",
    "ledger_entries": [
      {
        "ledger_account_id": "{{variables.card_account.id}}",
        "amount": "{{ledgerable_event.custom_data.amount}}",
        "direction": "credit"
      },
      {
        "ledger_account_id": "{{variables.receivable_account.id}}",
        "amount": "{{ledgerable_event.custom_data.amount}}",
        "direction": "debit,
      },
    ],
  },
  "conditions": {
    "field": "ledgerable_event.name",
    "operator": "equals",
    "value": "Card Swipe"
  },
  "metadata": {
    "my_event_handler": "is_the_best"
  }
  "live_mode": true,
  "created_at": "2023-01-01T12:00:00Z",
  "updated_at": "2023-01-01T12:00:00Z"
}

The Event Handler dictates the formatting of the Ledgerable Event to the Ledger Transaction via the ledger_transaction_template.

Clients would need to build the ledger_transaction object within the ledger_transaction_template object. They have two additional forms of data injection beyond raw input:

  1. Ledgerable Event fields may be referenced via the {{ledgerable_event.field_name}} syntax. Nested fields can be accessed using . delimiters (e.g. { foo: { bar: baz } } }, {{ledgerable_event.foo.bar}} would yield baz)
  2. Outside of the ledger_transaction_template there is the variables key. Variables are a name => object mapping, currently only Ledger Accounts are supported as an object. Ledger Accounts may be fetched and referenced from the ledger_transaction_template by writing a query to the ledger accounts table. Any field that exists in the Ledger Account object may be accessed the same way.

Logic is not evaluated, and we will error in the case where there are either mixed {{field_name}} and raw string inputs (e.g. {{custom_data.foo}} and more stuff here}}).

The conditions field dictates whether we apply a Event Handler to a Ledgerable Event. We currently only support specifying the value field in the request. This is to guarantee matching only one ledgerable event to one event handler.

Ledgerable Event

POST /api/ledgerable_events

Example Request

{
  "name": "Card Swipe",
  "description": "Card swipe from Alice",
  "custom_data": {
    "amount": "1500",
    "status": "posted",
    "card_id": "<unique_card_id>"
  },
  "metadata": { "external_transaction_id": "123456" }
}

Example Response

{
  "id": "c155f488-8835-480a-92cc-5ee09c254225",
  "object_type": "ledgerable_event",
  "name": "Card Swipe",
  "description": "Card swipe from Alice",
  "custom_data": {
    "amount": "1500",
    "status": "posted",
    "card_id": "<unique_card_id>"
  },
  "metadata": { "external_transaction_id": "123456" },
  "live_mode": true,
  "created_at": "2023-01-01T12:00:00Z",
  "updated_at": "2023-01-01T12:00:00Z"
}

The Ledgerable Event is the financial event occurring on the partner side. The only field required is name, otherwise it is up to clients to decide what they provide.

On creation of a Ledgerable Event, we match the name of the event to the conditions field of the Event Handler. If there is no match, we error. On a match, the event is supplied as input to the template, creating an LT at the same time.

On a successful response, there are always two objects created: the ledgerable event (in the response), and the Ledger Transaction. The Ledger Transaction has the Ledgerable Event as the Ledgerable ID.

In the example above, if we were to GET the ledger transaction with the provided ID in the response by querying the ledgerable_id, we’d receive:

{
  "id": "cc2e883a-6556-47d5-bfcc-0d2a2d76f98e",
  "object": "ledger_transaction",
  "ledgerable_type": "LedgerableEvent",
  "ledgerable_id": "c155f488-8835-480a-92cc-5ee09c254225",
  "ledger_id": "a9d970da-207e-43da-b4d6-6e9ae01ba2cc",
  "description": "Card swipe from Alice",
  "status": "posted",
  "ledger_entries": [
    {
      "id": "45067a63-6e4d-48f6-8aed-872cb3ee9c72",
      "object": "ledger_entry",
      "amount": 1500,
      "direction": "credit",
      "ledger_account_id": "8b0b2123-5443-4eb6-a4b2-7f5366a6a968",
      "ledger_account_currency": "USD",
      "ledger_account_currency_exponent": 2,
      "ledger_transaction_id": "cc2e883a-6556-47d5-bfcc-0d2a2d76f98e",
      "resulting_ledger_account_balances": null,
      "live_mode": true,
      "created_at": "2020-08-04T16:58:51Z",
      "updated_at": "2020-08-04T16:58:51Z"
    },
    {
      "id": "2a9b522e-5be3-49bc-b607-a09f9786dc3c",
      "object": "ledger_entry",
      "amount": 1500,
      "direction": "debit",
      "ledger_account_id": "d1f69141-0668-4cf4-9157-b80174fb2f0a",
      "ledger_account_currency": "USD",
      "ledger_account_currency_exponent": 2,
      "ledger_transaction_id": "cc2e883a-6556-47d5-bfcc-0d2a2d76f98e",
      "resulting_ledger_account_balances": null,
      "live_mode": true,
      "created_at": "2020-08-04T16:58:51Z",
      "updated_at": "2020-08-04T16:58:51Z"
    }
  ],
  "posted_at": null,
  "effective_at": "2021-01-01T00:00:00.000",
  "effective_date": "2021-01-01",
  "metadata": { "external_transaction_id": "123456" },
  "live_mode": true,
  "created_at": "2020-08-04T16:58:51Z",
  "updated_at": "2020-08-04T16:58:51Z"
}

Notice the bolded fields were injected from the ledgerable event and variables.

TAB-Transactions API

Overview

This guide will explain how to use the Ledgers API for a card product. Any card product needs to be able to determine what balances users can spend or have spent using their cards. The Ledgers API lets you record these balances at scale alongside other transactions in your app.

In this example, we will assume you are building a product called Swipe that allows users to deposit funds and spend those funds using a card. We will demonstrate a simple flow here with a few events:

  • The user deposits funds
  • The user swipes their card and you authorize the transaction
  • A payment is made to the card issuer

The Modern Treasury ledger will serve as a central source of truth on money movements while Swipe interacts with multiple external systems:

Setup

First, you will make a ledger for Swipe.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledgers \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Swipe Ledger",
    "description": "Represents our USD funds and user balances",
    "currency": "USD"
  }'

This will return a ledger object with an ID to be used in the following step.

{
    "id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "object": "ledger",
    "name": "Swipe Ledger",
    "description": "Represents our USD funds and user balances",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:48:05Z",
    "updated_at": "2020-08-04T16:48:05Z"
}

Next, you can create 3 ledger accounts to represent balances you need your application to track:

  • Cash in Swipe's bank account
  • Funds held for a given user, Jane Doe. When Jane signs up, you create this account and issue her a card through your card issuer.
  • Funds to be settled with the card issuer

Note that the cash account is normal_balance=debit (because it represents a balance that you hold), whereas the user wallet and issuer accounts are normal_balance=credit (because they represent balances that you owe).

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_accounts \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Cash Account",
    "description": "Tracks our cash",
    "normal_balance": "debit",
    "currency": "USD",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12"
  }'
  
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_accounts \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Jane Doe Wallet",
    "description": "Tracks balance held on behalf of Jane Doe",
    "normal_balance": "credit",
    "currency": "USD",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12"
  }'

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_accounts \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Issuer Settlement",
    "description": "Tracks balance to be paid card issuer",
    "normal_balance": "credit",
    "currency": "USD",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12"
  }'

This will return the 3 ledger accounts you've created.

{
    "id": "f1c7e474-e6d5-4741-9f76-04510c8b6d7a",
    "object": "ledger_account",
    "name": "Cash Account",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "description": "Tracks our cash",
    "normal_balance": "debit",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:54:32Z",
    "updated_at": "2020-08-04T16:54:32Z"
}

{
    "id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b",
    "object": "ledger_account",
    "name": "Jane Doe Wallet",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "description": "Tracks balance held on behalf of Jane Doe",
    "normal_balance": "credit",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:54:32Z",
    "updated_at": "2020-08-04T16:54:32Z"
}

{
    "id": "463237a4-a93d-4396-9be8-5bacb8ceead1",
    "object": "ledger_account",
    "name": "Issuer Settlement",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "description": "Tracks balance to be paid card issuer",
    "normal_balance": "credit",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:54:32Z",
    "updated_at": "2020-08-04T16:54:32Z"
}

Recording a deposit

Now that the setup is done, you can write to the ledger to record what happens in your business.

First, you'll pull funds from Jane Doe using your external payment provider. Once the payment is executed, you create a ledger transaction to record that Jane Doe has deposited $100 in her wallet. This will recognize that $100 has entered your bank account, and that $100 of funds are held on behalf of Jane.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_transactions \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Jane Doe cash deposit",
    "effective_date": "2020-08-27",
    "status": "posted",
    "external_id": "97dbb8b1-e6f2-485e-a0ec-6267e3c60718",
    "ledger_entries": [
      {
        "amount": 10000,
        "direction": "debit",
        "ledger_account_id": "f1c7e474-e6d5-4741-9f76-04510c8b6d7a"
      },
      {
        "amount": 10000,
        "direction": "credit",
        "ledger_account_id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b"
      }
    ] 
  }'

Because you control the ledger, you can allow Jane to spend these in-app funds in any number of ways. For example, you could allow her to send funds to another Swipe user. To see how this would be represented in the ledger, refer to the Quickstart for Digital Wallets.

Authorizing a card transaction

Jane can also spend funds through her card. When Jane swipes her card to buy a $50 item, you receive an authorization request from your card issuing partner.

Your will need to check Jane's balance in your ledger in order to determine whether to authorize this payment, by querying the Get Ledger Account endpoint:

curl --request GET \
  -u ORGANIZATION_ID:API_KEY \
   --url https://app.moderntreasury.com/api/ledger_accounts/61574fb6-7e8e-403e-980c-ff23e9fbd61b

This will return Jane's live wallet balance.

{
  "id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b",
  "object": "ledger_account",
  "live_mode": true,
  "name": "Jane Doe Wallet",
  "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
  "description": "Tracks balance held on behalf of Jane Doe",
  "lock_version": 2,
  "normal_balance": "credit",
  "balances": {
    "pending_balance": {
      "credits": 10000,
      "debits": 10000,
      "amount": 10000,
      "currency": "USD",
      "currency_exponent": 2
    },
    "posted_balance": {
      "credits": 10000,
      "debits": 10000,
      "amount": 10000,
      "currency": "USD",
      "currency_exponent": 2
    }
  },
  "metadata": {},
  "discarded_at": NULL,
  "created_at": "2020-08-04T16:54:32Z",
  "updated_at": "2020-08-04T17:23:12Z"
}

Based on Jane's sufficient balance and any other attributes of the card swipe you want to consider, you decide to authorize this transaction.

First, you'll write a transaction putting an authorization hold on $50 on Jane's account and noting that $50 should be settled with your card issuer. When creating this transaction, you can refer to the lock_version of Jane's account balance to ensure that her account balance has not changed since you last checked it. The transaction will fail if the balance has changed (for example, because Jane has already spent her funds in another part of your app).

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_transactions \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Jane Doe card swipe",
    "effective_date": "2020-08-29",
    "status": "pending",
    "external_id": "c006a6df-72ff-4cbf-aa4a-c18dde4b05c5",
    "ledger_entries": [
      {
        "amount": 5000,
        "direction": "credit",
        "ledger_account_id": "463237a4-a93d-4396-9be8-5bacb8ceead1"
      },
      {
        "amount": 5000,
        "direction": "debit",
        "ledger_account_id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b"
      }
    ]
  }'

If the transaction is created, you have successfully put a hold on Jane's in-app funds. You can respond to your issuer's request authorizing the card transaction.

Settling with the issuer

Finally, at the end of the day, you can settle with your card issuer. This involves sending them the funds that users have spent that day through your payment provider.

Once the payment is executed through your external provider, you can create a ledger transaction recording that $50 has left your bank account, and that $50 is no longer owed to the issuer.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_transactions \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Card issuer settlement",
    "effective_date": "2020-08-30",
    "status": "posted",
    "external_id": "a9a7fd22-8922-498a-b8b3-def4df72d5e4",
    "ledger_entries": [
      {
        "amount": 5000,
        "direction": "credit",
        "ledger_account_id": "f1c7e474-e6d5-4741-9f76-04510c8b6d7a"
      },
      {
        "amount": 5000,
        "direction": "debit",
        "ledger_account_id": "463237a4-a93d-4396-9be8-5bacb8ceead1"
      }
    ]
  }'