Cards

Ledgers tutorial for cards

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"
      }
    ]
  }'