blog

New API access feature for reading and writing Beancount data easily

August 28, 2023
api
new-feature
automation

There are countless accounting book software, most of which operate based on database queries. Unlike most accounting book software, plain-text accounting software such as Beancount operates based on text like this:

2023-08-28 * "BeanHub Pro Subscription"
  Expenses:Office:Supplies:SoftwareAsService     9.0 USD
  Assets:Bank:US:Mercury                        -9.0 USD

Many software engineers, like myself, love using Beancount for bookkeeping because we get used to writing code and love open source. With the idea of accounting books as code, we can track the changes easily with Git.

The screencast of Git diff of a BeanHub repository

Database-powered vs text-based

There’s a reason why most people build accounting software with databases instead of text-based files. It’s much easier to develop an accounting book software product backed by SQL database as it’s proven technology and very natural to operate and update, particularly from a machine’s perspective. On the other hand, working with text-based accounting books is much more challenging because the format is designed to be human-readable and writable.

Despite being way harder, we asked ourselves at the beginning when building BeanHub why we should make yet another SQL-based accounting book system as if its users suffer the same issues as the other 9,999 accounting apps — vendor lock-in and lack of data ownership. Surely, you can pick up any proprietary accounting book software you like on the internet, but you never know if it will live for a few years and disappear like most would eventually. People worry about losing access to their critical financial data.

Unlike proprietary database-powered accounting books, let’s say you put all your accounting books on BeanHub and assume it runs out of business tomorrow. As long as you have a local or a remote Git clone of your repository, you can still access all your data. That’s the magic of open-source text-based accounting book software. Because that’s a critical value, we insist on doing it the hard but right way, i.e., making all the operations based on text files and Git instead of a database.

Now, you can also read or write your Beancount repository via BeanHub API easily.

While operating on a Beancount accounting book repository is hard for us and will surely be hard for you. Have you ever wondered if having an easy way to read and write your Beancount repository would be excellent? With that, you can build custom automation for your accounting books with your preferred programming languages.

Yes! We heard your request and are glad to announce that the new API access feature is available to all BeanHub users today. As mentioned in our last article, you can create access tokens for accessing API on the access token management page. You can also find the API document here or the API Docs link at the footer of BeanHub web pages. You can find the interactive document here if you want to try the API out with a Swagger UI on the web.

What can you do with the API now?

Currently, we support reading entries and form reading/submissions. Here’s a cURL command example for reading entries:

curl https://api.beanhub.io/v1/repos/USERNAME/REPO_NAME/entries \
  -H "Access-Token: YOUR_ACCESS_TOKEN_HERE"

To understand more about our custom form feature, please see our past blog post about the custom form . Here’s a cURL command for submitting data to your custom form:

curl https://api.beanhub.io/v1/repos/USERNAME/REPO_NAME/forms/FORM_NAME \
  -H "Access-Token: YOUR_ACCESS_TOKEN_HERE" \
  -d '{"data": {"date": "2023-08-28", "hours": "15"}}'

The form submission API also allows you to submit batch form data and customize the Git commit message. Here’s an example:

curl https://api.beanhub.io/v1/repos/USERNAME/REPO_NAME/forms/FORM_NAME \
  -H "Access-Token: YOUR_ACCESS_TOKEN_HERE" \
  -d '{
    "data": [
      {"date": "2023-08-26", "hours": "15"},
      {"date": "2023-08-27", "hours": "13"},
      {"date": "2023-08-28", "hours": "14"}
    ],
    "commit": {
      "message": "Add project XYZ contracting hours for 2023-08-26 ~ 2023-08-28"
    }
  }'

A real-world example — generate software development contracting invoices automatically biweekly

With the custom form feature introduced a while back, we have made logging the software development contracting hours for our clients much easier at Launch Platform. But still, we need to sum up the hours spent for each client and send out invoices every other week. It’s a tedious and error-prone process. We have wanted to automate the process for a long time, and now, we can move closer to our fully automatic accounting book goal with the help of our new API feature. Here’s the sample Python code we use for generating our clients biweekly:

import datetime
import decimal
import logging
import os
import pathlib
import re
import urllib.parse

import click
import requests
# This module is our internal library for generating invoice PDF file.
# You need to make one for yourself.
# Please checkout https://py-pdf.github.io/fpdf2/ to learn how to
# generate a PDF file.
from make_invoice import ItemLine
from make_invoice import make_invoice

BEANHUB_USERNAME = os.environ["BEANHUB_USERNAME"]
BEANHUB_REPO_NAME = os.environ["BEANHUB_REPO_NAME"]
BEANHUB_ACCESS_TOKEN = os.environ["BEANHUB_ACCESS_TOKEN"]
BEANHUB_BASE_API_URL = (
    f"https://api.beanhub.io/v1/repos/{BEANHUB_USERNAME}/{BEANHUB_REPO_NAME}/"
)


def fetch_entries():
    logger = logging.getLogger(__name__)

    # Notice: ideally we should be able to filter entries based on account and the date
    #         range, or other conditions with URL parameters. But the API feature
    #         is not available yet at this moment. So let's fetch everything and filter
    #         instead for now, given the frequency of running this script is like
    #         bi-weekly, it shouldn't be too bad for now.
    entries_url = urllib.parse.urljoin(BEANHUB_BASE_API_URL, "entries")
    commit_hexsha = None
    page = 0
    while True:
        resp = requests.get(
            entries_url + f"?page={page}",
            headers={"Access-Token": BEANHUB_ACCESS_TOKEN},
        )
        resp.raise_for_status()
        payload = resp.json()
        pagination = payload["pagination"]
        current_page = pagination["page"]
        total_pages = pagination["total_pages"]
        logger.info("Processing entries page %s / %s", current_page, total_pages)
        yield from payload["entries"]
        current_commit_hex_sha = payload["commit"]["hexsha"]
        if commit_hexsha is not None and current_commit_hex_sha != commit_hexsha:
            # Ensure that we are fetching data from the same commit otherwise we may
            # see inconsistent data
            raise ValueError(
                f"Inconsistent commit hexsha {commit_hexsha} and {current_commit_hex_sha}"
            )
        if current_page >= total_pages:
            break
        commit_hexsha = current_commit_hex_sha
        page += 1


def get_account_posting(
    entry: dict,
    narration_pattern: str,
    account: str,
) -> tuple[datetime.date, str, dict] | None:
    if entry["entry_type"] != "transaction":
        return None
    narration = entry["narration"]
    if not re.match(narration_pattern, narration):
        return None

    entry_date = datetime.datetime.strptime(entry["date"], "%Y-%m-%d").date()
    for posting in entry["postings"]:
        if posting["account"] == account:
            return entry_date, entry["narration"], posting


@click.command()
@click.argument("invoice_number")
@click.argument("output_file", type=click.Path())
@click.option(
    "-s",
    "--start-week-date",
    type=click.DateTime(formats=["%Y-%m-%d"]),
    default=str(datetime.date.today() - datetime.timedelta(days=7)),
)
def main(
    invoice_number: str, output_file: pathlib.Path, start_week_date: datetime.datetime
):
    logger = logging.getLogger(__name__)
    start_week_date = start_week_date.date()
    if start_week_date.weekday():
        # find monday of this week
        start_week_date -= datetime.timedelta(days=start_week_date.weekday())
    # two weeks from now
    end_week_date = start_week_date + datetime.timedelta(days=14)
    logger.info("Generating invoice between %s to %s", start_week_date, end_week_date)
    items: list[ItemLine] = []
    for entry in fetch_entries():
        item = get_account_posting(
            entry,
            narration_pattern="Hours spent on the software development project for client XYZ",
            account="Assets:AccountsReceivable:Contracting:XYZ",
        )
        if item is None:
            continue
        date, narration, posting = item
        if date < start_week_date:
            continue
        if date >= end_week_date:
            continue
        items.append(
            ItemLine(
                date=date,
                desc=narration,
                quantity=posting["units"]["number"],
                price=posting["price"]["number"],
            )
        )
        logger.info("Found transaction %s %r with posting %s", date, narration, posting)
    make_invoice(invoice_number=invoice_number, output_file=output_file, items=items)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    main()

The future of BeanHub API

Suppose you are a nerd like me who enjoys automating tedious repeating works with software technology as much as possible. In that case, I bet you can already imagine how you can use BeanHub’s API to automate your accounting book routines and be more productive with your valuable time on important things instead.

And this is just the beginning. There will be filtering parameters available for entries reading API shortly. We will also provide API for writing Beancount entries, such as transactions and opening accounts. With that, you can write your code for inserting data into your accounting books from any source you like. And there are more APIs to come. Want to adopt an OCR library for scanning image-based PDF invoices and create an entry for you automatically? Sure! Go ahead. Want to be fancy and let ChatGPT help you create accounting book entries? Of course, why not? While I know this sounds awkward and nerdy, but hey!

Now, with BeanHub, the only limitation of your accounting book automation is your imagination!

I hope you enjoy the new BeanHub API feature and find it useful as we did. As usual, please feel free to contact us at support@beanhub.io and let us know what you think about the new API features or if you have any feature ideas we should consider 😄