Accounts payable (AP) is the money your business owes to its suppliers for goods or services you’ve already received but haven't paid for yet. In the world of accounting, AP is classified as a current liability on your balance sheet—an amount typically due within the next year, and often within 30 to 60 days.
This concept is central to accrual accounting, where you record the expense and the corresponding liability the moment a bill arrives, not when you actually send the cash. This guide will show you how to manage the entire AP workflow cleanly and efficiently using the plain-text accounting tool, Beancount.
Quick Summary
Before we dive into the details, let's cover the essentials:
- Accounts Payable (AP) represents your short-term debts to vendors. You'll find it under the
Liabilitiessection of your balance sheet. - Accrual vs. Cash: AP is a concept that exists only if you keep your books on an accrual basis. Beancount fully supports accrual workflows, and its web interface, Fava, will display your liabilities correctly.
- AP vs. AR: It's simple: Payables are what you owe, while Receivables (AR) are what others owe you.
Where AP Lives in Beancount (and Fava)
To start tracking AP, you first need to declare an account for it in your ledger. A standard convention is:
Liabilities:AccountsPayable
You can optionally create subaccounts for major vendors (e.g., Liabilities:AccountsPayable:ForestPaintSupply).
In Fava, this account will appear on your Balance Sheet under Liabilities. You can click on it to drill down and see the postings that make up the balance. You can even see this in action in Fava's public example ledger, which includes a Liabilities:AccountsPayable account.
Sign convention: Beancount stores liability balances as credits (negative numbers). A balance of -500.00 USD on Liabilities:AccountsPayable means you owe $500. The open-item report below flips the sign with -SUM(number) so each unpaid invoice shows a positive outstanding amount. A fully settled invoice sums to zero and is excluded by HAVING.
Beancount Building Blocks You’ll Use
A robust AP workflow in Beancount relies on a few core features:
- Accounts: You'll primarily use your
Liabilities:AccountsPayableaccount, a cash account likeAssets:Bank:Checking, and your various expense accounts (e.g.,Expenses:Supplies). - Metadata: You can attach key-value data to any transaction. For AP, you'll use metadata like
invoice:,due:,terms:, anddocument:. Setoption "documents"to a folder so Beancount can discover files there; in Fava, dropping a file onto a journal row inserts adocument:metadata entry, and enablingplugin "fava.plugins.link_documents"links those entries to discovered Document directives. - Tags & Links: Use
#tags(like#ap) for easy filtering and^links(like^INV-10455) to programmatically tie a bill and its subsequent payment together. This creates a clear, auditable trail in the journal — but links alone are not enough for the open-invoice query below. - Queries (BQL): Beancount's SQL-like query language (BQL) allows you to run powerful reports, like listing all open payables sorted by due date, directly from the command line with
bean-queryor on Fava's "Query" page.
Invoice identity (required for open-invoice queries): Every bill and every settling payment — full, partial, or early-discount — must carry the same invoice: and due: transaction metadata, plus the same ^link. The query groups by payee, any_meta('invoice'), and any_meta('due'). If a payment omits those keys, its AP posting lands in a separate (payee, null, null) group and a settled bill still looks open. Keep (payee, invoice) unique across vendors. Optional bill-only fields such as terms: and document: need not repeat on payments.
Core AP Workflow in Beancount
Managing AP in your ledger involves two or three key steps: recording the bill, paying it, and sometimes handling partial payments or discounts.
1) Record the Vendor Bill (This Creates the Liability)
First, you book the expense and create the payable when the invoice arrives.
; @m49-fragment env-documents
; Optionally set your documents folder in your main Beancount file:
option "documents" "documents"
2025-08-05 * "Forest Paint Supply" "Paint order INV-10455" ^INV-10455 #ap
invoice: "INV-10455"
due: "2025-09-04"
terms: "2/10, n/30"
document: "invoices/2025-08-05-forest-paint-INV-10455.pdf"
Expenses:Supplies:Paint 500.00 USD
Liabilities:AccountsPayable -500.00 USDThis single entry accomplishes two critical things:
- It immediately recognizes the $500 expense in the correct period (August) — once. Do not re-post the expense on the payment.
- It credits AP by $500 (
-500.00 USD), recording that you owe Forest Paint Supply that amount.
The ^INV-10455 link plus the invoice: / due: metadata form the invoice's identity. Every later payment of this bill must reuse all three.
2) Pay the Bill (This Clears the Liability)
When you pay the invoice, you create a transaction that moves money from your bank account to clear the liability. Copy invoice: and due: from the bill onto the payment so both postings share one query group.
a) Standard Payment (No Discount):
2025-09-01 * "Forest Paint Supply" "Payment INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -500.00 USDThis entry reduces your AP balance by $500 and your checking account balance by the same amount. The liability for this invoice is now cleared (the group's AP sum is zero). The expense stays on the August bill — the payment does not touch Expenses:.
b) Early-Payment Discount (e.g., "2/10, n/30") — pick exactly one method:
If the terms are "2/10, n/30", you can take a 2% discount if you pay within 10 days. For our $500 invoice, that's a $10 discount. The fence below shows two mutually exclusive alternatives: Option 1 is live; Option 2 is fully commented out so a paste cannot book both. To use Option 2 instead, comment out Option 1 and uncomment Option 2. Never leave both active — that clears AP twice and invents a second $10 benefit. Which income vs. expense account you use is an accounting-policy choice for your books; Beancount does not require one over the other.
; Pick EXACTLY ONE of the two transactions below for a given invoice.
; Do not paste both into the same ledger — that clears AP twice.
; Option 1: Record the discount as other income
2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -490.00 USD
Income:Discounts:Payables -10.00 USD
; Option 2: Reduce the original expense directly
; 2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
; invoice: "INV-10455"
; due: "2025-09-04"
; Liabilities:AccountsPayable 500.00 USD
; Assets:Bank:Checking -490.00 USD
; Expenses:Supplies:Paint -10.00 USDIn either alternative alone, you clear the full $500 liability, reduce your bank balance by the $490 you actually paid, and account for the $10 benefit. Replace the standard full-payment transaction with the chosen alternative — do not stack a discount payment on top of the $500 cash payment, and do not combine this fence with section 2a for the same invoice.
3) Handling Partial Payments
Reuse the same invoice: / due: / ^link identity on every installment so each payment reduces the same open-invoice group.
; Invoice for $1,200
2025-08-10 * "Acme Parts" "INV-9001" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Expenses:Parts 1200.00 USD
Liabilities:AccountsPayable -1200.00 USD
; First payment of $400
2025-08-20 * "Acme Parts" "Payment INV-9001 (1/3)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 400.00 USD
Assets:Bank:Checking -400.00 USD
; Final payment of $800
2025-09-05 * "Acme Parts" "Payment INV-9001 (final)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 800.00 USD
Assets:Bank:Checking -800.00 USDAfter the first payment, the invoice group's AP sum is -800.00 USD (you still owe $800). After the final payment it is zero. Filtering the journal by ^INV-9001 still shows the complete history.
Helpful Queries (BQL)
You can run these queries in Fava’s “Query” tab or from the command line with bean-query (BQL reference). Verified below with Beancount 3.2.3 and beanquery 0.2.0.
Tip: The
any_meta()function looks up a metadata key on the posting first, then falls back to the transaction — so transaction-levelinvoice:/due:values appear in the result set.
Open AP by Vendor (Balance View):
This query sums the liability inventory per supplier. A credit (negative) total is what you still owe that vendor.
SELECT payee, COST(SUM(position)) AS amount
WHERE account ~ "^Liabilities:AccountsPayable"
GROUP BY payee
ORDER BY payee;Open invoices as of a fixed date (open-item report):
An open-item report answers “what remains unpaid?” It must (1) cut off at an explicit as-of date so later payments cannot silently clear an earlier snapshot, (2) group by the shared invoice identity (payee + invoice: + due:), (3) exclude settled (zero) groups, and (4) show a positive outstanding amount — not a raw signed AP credit.
Reporting date used everywhere below: 2025-09-04.
Downloadable AP starter ledger
Download accounts-payable-starter.bean — one self-contained ledger generated from the complete fence below (account opens, fictional opening cash, settled / partial / unpaid invoices, as-of 2025-09-04). Every name and amount is fictional sample data. No invoice PDF, documents folder, or other file is required to validate or query it.
First-run from a clean temporary directory (pinned Beancount 3.2.3 / beanquery 0.2.0):
# Save the download as accounts-payable-starter.bean in this directory, then:
uvx --from beancount==3.2.3 bean-check accounts-payable-starter.bean
uvx --from beanquery==0.2.0 --with beancount==3.2.3 bean-query accounts-payable-starter.bean \
"SELECT payee, any_meta('invoice') AS invoice, any_meta('due') AS due, -SUM(number) AS outstanding WHERE account ~ '^Liabilities:AccountsPayable' AND date <= 2025-09-04 GROUP BY payee, invoice, due HAVING SUM(number) < 0 ORDER BY due, payee"bean-check exits 0 with no output. The query returns the five open rows in the expected table below (Forest absent; Acme outstanding 800.00; sum of positive outstanding = 1500.00). Regenerate the committed download with yarn generate:ap-starter after editing the fence; yarn generate:ap-starter --check fails if the artifact is stale.
Paste this complete fixture (opens + opening cash + the three invoice shapes, plus due-today / overdue / edge-case rows for aging). Early-discount alternatives for INV-10455 stay fully commented — activate exactly one only if you replace the standard Forest payment; never leave both discount options live alongside that payment.
; accounts-payable-starter — complete runnable AP workflow
option "operating_currency" "USD"
2025-01-01 open Assets:Bank:Checking USD
2025-01-01 open Equity:Opening-Balances USD
2025-01-01 open Liabilities:AccountsPayable USD
2025-01-01 open Expenses:Supplies:Paint USD
2025-01-01 open Expenses:Parts USD
2025-01-01 open Expenses:Office USD
2025-01-01 open Income:Discounts:Payables USD
; Fictional opening cash — enough to cover every payment in this ledger
2025-01-01 * "Opening balance"
Assets:Bank:Checking 5000.00 USD
Equity:Opening-Balances -5000.00 USD
; Settled before as-of — must disappear from the open report
2025-08-05 * "Forest Paint Supply" "Paint order INV-10455" ^INV-10455 #ap
invoice: "INV-10455"
due: "2025-09-04"
Expenses:Supplies:Paint 500.00 USD
Liabilities:AccountsPayable -500.00 USD
2025-09-01 * "Forest Paint Supply" "Payment INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -500.00 USD
; Early-payment discount alternatives for INV-10455 — mutually exclusive with
; the standard payment above AND with each other. To take 2/10 instead: delete
; (or comment out) the 2025-09-01 payment, then uncomment EXACTLY ONE option.
; Never leave both options active — that clears AP twice.
;
; Option 1: Record the discount as other income
; 2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
; invoice: "INV-10455"
; due: "2025-09-04"
; Liabilities:AccountsPayable 500.00 USD
; Assets:Bank:Checking -490.00 USD
; Income:Discounts:Payables -10.00 USD
;
; Option 2: Reduce the original expense directly
; 2025-08-12 * "Forest Paint Supply" "Early payment discount INV-10455" ^INV-10455
; invoice: "INV-10455"
; due: "2025-09-04"
; Liabilities:AccountsPayable 500.00 USD
; Assets:Bank:Checking -490.00 USD
; Expenses:Supplies:Paint -10.00 USD
; Partial before as-of; final $800 payment is AFTER 2025-09-04
2025-08-10 * "Acme Parts" "INV-9001" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Expenses:Parts 1200.00 USD
Liabilities:AccountsPayable -1200.00 USD
2025-08-20 * "Acme Parts" "Payment INV-9001 (1/3)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 400.00 USD
Assets:Bank:Checking -400.00 USD
2025-09-05 * "Acme Parts" "Payment INV-9001 (final)" ^INV-9001
invoice: "INV-9001"
due: "2025-09-09"
Liabilities:AccountsPayable 800.00 USD
Assets:Bank:Checking -800.00 USD
; Unpaid — not yet due as of 2025-09-04
2025-08-15 * "Bright Office Co" "INV-220" ^INV-220
invoice: "INV-220"
due: "2025-09-14"
Expenses:Office 300.00 USD
Liabilities:AccountsPayable -300.00 USD
; Unpaid — due today (as-of == due)
2025-08-20 * "Helix Industries" "INV-440" ^INV-440
invoice: "INV-440"
due: "2025-09-04"
Expenses:Parts 200.00 USD
Liabilities:AccountsPayable -200.00 USD
; Unpaid — 30 days overdue as of 2025-09-04
2025-07-06 * "Metro Hardware" "INV-77" ^INV-77
invoice: "INV-77"
due: "2025-08-05"
Expenses:Parts 150.00 USD
Liabilities:AccountsPayable -150.00 USD
; Overpayment — credit balance (not an amount due; see edge cases)
2025-08-01 * "Credit Vendor" "INV-C1" ^INV-C1
invoice: "INV-C1"
due: "2025-08-20"
Expenses:Parts 100.00 USD
Liabilities:AccountsPayable -100.00 USD
2025-08-18 * "Credit Vendor" "Overpay INV-C1" ^INV-C1
invoice: "INV-C1"
due: "2025-08-20"
Liabilities:AccountsPayable 150.00 USD
Assets:Bank:Checking -150.00 USD
; Missing due metadata — still open; age into a separate bucket, do not drop
2025-08-12 * "No Due Co" "INV-ND" ^INV-ND
invoice: "INV-ND"
Expenses:Office 50.00 USD
Liabilities:AccountsPayable -50.00 USDOpen-item query (beanquery 0.2.0 supports HAVING SUM(number) < 0; it does not support comparing an inventory to 0 with COST(SUM(position)) != 0):
SELECT payee,
any_meta('invoice') AS invoice,
any_meta('due') AS due,
-SUM(number) AS outstanding
WHERE account ~ "^Liabilities:AccountsPayable"
AND date <= 2025-09-04
GROUP BY payee, invoice, due
HAVING SUM(number) < 0
ORDER BY due, payee;Expected rows as of 2025-09-04 (derived independently from the transactions: bill − payments dated ≤ as-of; Forest nets to 0 and is absent):
| payee | invoice | due | outstanding |
|---|---|---|---|
| No Due Co | INV-ND | (empty) | 50.00 |
| Metro Hardware | INV-77 | 2025-08-05 | 150.00 |
| Helix Industries | INV-440 | 2025-09-04 | 200.00 |
| Acme Parts | INV-9001 | 2025-09-09 | 800.00 |
| Bright Office Co | INV-220 | 2025-09-14 | 300.00 |
Sum of positive outstanding = 1,500.00. Forest’s $500 bill is gone. Acme still shows $800 ($1,200 − $400); the $800 payment on 2025-09-05 is after the as-of date, so it cannot close this snapshot.
Reconcile to the AP liability at the same cut-off:
SELECT COST(SUM(position)) AS ap_balance
WHERE account ~ "^Liabilities:AccountsPayable"
AND date <= 2025-09-04;Expected: ap_balance = (-1450.00 USD). Independently: open outstanding $1,500 minus the $50 vendor credit (below) = $1,450 owed → liability inventory -1450.00 USD.
Move the as-of one day later (date <= 2025-09-05) and re-run the open-item query: Acme’s row disappears (final payment included), and ap_balance becomes (-650.00 USD). A payment after the reporting date never rewrites the earlier report.
Vendor credits / overpayments: HAVING SUM(number) < 0 keeps only amounts you still owe. An overpayment flips the group’s AP sum positive. List those separately — do not treat a positive AP balance as “amount due”:
SELECT payee,
any_meta('invoice') AS invoice,
any_meta('due') AS due,
SUM(number) AS credit
WHERE account ~ "^Liabilities:AccountsPayable"
AND date <= 2025-09-04
GROUP BY payee, invoice, due
HAVING SUM(number) > 0
ORDER BY payee;Expected: one row — Credit Vendor / INV-C1 / 2025-08-20 / credit = 50.00 (you overpaid by $50).
Missing due:: INV-ND still appears in the open report with an empty due. Age it into a dedicated missing-due bucket (below); never silently omit it.
List Bills with Attached PDFs:
SELECT date, payee, any_meta('invoice') AS invoice, any_meta('document') AS file
WHERE account ~ "^Liabilities:AccountsPayable"
ORDER BY date DESC;Where to See AP in Fava
- Balance Sheet: Navigate to
Balance Sheet→Liabilities→AccountsPayableto see the total balance and drill down into the transaction details. - Journal: Filter the journal by
account:Liabilities:AccountsPayableor a specific link like^INV-xxxxto see a bill's complete lifecycle. - Documents: With
option "documents"set, Beancount discovers files under that tree. Fava's Documents views and thelink_documentsplugin (see above) connect those files to transactions that carrydocument:metadata.
AP Aging, Turnover, and Cash-Flow Awareness
Aging schedule (reproducible export + buckets): beanquery 0.2.0 can emit the open-item rows above, but it does not provide a built-in aging-bucket expression over due metadata. Export the open report, then bucket deterministically against the same as-of date.
bean-query -f csv -o open-ap.csv accounts-payable-starter.bean \
"SELECT payee, any_meta('invoice') AS invoice, any_meta('due') AS due, -SUM(number) AS outstanding WHERE account ~ '^Liabilities:AccountsPayable' AND date <= 2025-09-04 GROUP BY payee, invoice, due HAVING SUM(number) < 0 ORDER BY due, payee"CSV columns: payee, invoice, due, outstanding.
Bucket rule for as-of date D = 2025-09-04 and each row’s due (ISO YYYY-MM-DD):
| Condition | Bucket |
|---|---|
due empty | missing-due |
due > D | not-yet-due |
due == D | due-today |
1 ≤ (D − due) ≤ 30 | 1-30-overdue |
31 ≤ (D − due) ≤ 60 | 31-60-overdue |
(D − due) ≥ 61 | 61+-overdue |
Compact Python (stdlib only) that implements that table:
from datetime import date
AS_OF = date(2025, 9, 4)
# Apply bucket() to each CSV row's due column (ISO YYYY-MM-DD or empty).
def bucket(due: str | None) -> str:
if not due:
return "missing-due"
days = (AS_OF - date.fromisoformat(due)).days
if days < 0:
return "not-yet-due"
if days == 0:
return "due-today"
if days <= 30:
return "1-30-overdue"
if days <= 60:
return "31-60-overdue"
return "61+-overdue"Expected aging for the open rows as of 2025-09-04:
| invoice | due | days past due | bucket | outstanding |
|---|---|---|---|---|
| INV-ND | (empty) | — | missing-due | 50.00 |
| INV-77 | 2025-08-05 | 30 | 1-30-overdue | 150.00 |
| INV-440 | 2025-09-04 | 0 | due-today | 200.00 |
| INV-9001 | 2025-09-09 | −5 | not-yet-due | 800.00 |
| INV-220 | 2025-09-14 | −10 | not-yet-due | 300.00 |
Due-today is not folded into “current”: due == as-of is its own bucket. Not-yet-due requires due > as-of.
- AP Turnover Ratio:
Total Supplier Purchases ÷ Average AP. A related metric, Days Payable Outstanding (DPO), is roughly365 ÷ Turnover Ratio. - If You Can’t Pay on Time: AP is meant for short-term debt. If a vendor agrees to formal, longer-term repayment, reclassify out of AP into a note payable — keep the same invoice identity on the conversion so the AP group clears.
2025-10-01 * "Helix Industries" "Convert overdue AP to 12-month note" ^INV-1110
invoice: "INV-1110"
due: "2025-09-15"
Liabilities:AccountsPayable 2000.00 USD
Liabilities:NotesPayable -2000.00 USDBest Practices for AP in a Plain-Text Ledger
- Go Paperless: Store invoice PDFs under your documents tree and attach them with the
document:metadata key (and Fava upload /link_documentswhen you use Fava). - Reuse Invoice Identity on Payments: Put the same
invoice:,due:, and^linkon the bill and on every payment that settles it. - Report Against an As-Of Date: Cut open-item and aging queries with
date <= YYYY-MM-DDso a later payment cannot rewrite an earlier snapshot; age with the sameD. - Keep Metadata Tidy: Consistently using
invoice:,due:, andterms:improves search, queries, and financial reviews. - Accrual All the Way: If you want useful AP reporting, commit to keeping your books on an accrual basis. Beancount and Fava handle accrual balances on the Balance Sheet without special plugins.
Copy-Paste Starter: Vendor Bill + Payment
For the full open-item workflow (settled + partial + unpaid, as-of date, aging), prefer the downloadable AP starter above. The minimal bill+payment pair below still shows identity on both legs — open the accounts first (or include these opens in your main ledger). Optional document: metadata is omitted here so nothing outside the ledger is required.
option "operating_currency" "USD"
2025-01-01 open Assets:Bank:Checking USD
2025-01-01 open Liabilities:AccountsPayable USD
2025-01-01 open Expenses:Supplies:Paint USD
; ---- Bill ----
2025-08-05 * "Forest Paint Supply" "Paint order INV-10455" ^INV-10455 #ap
invoice: "INV-10455"
due: "2025-09-04"
Expenses:Supplies:Paint 500.00 USD
Liabilities:AccountsPayable -500.00 USD
; ---- Payment (no discount) ----
2025-09-01 * "Forest Paint Supply" "Payment INV-10455" ^INV-10455
invoice: "INV-10455"
due: "2025-09-04"
Liabilities:AccountsPayable 500.00 USD
Assets:Bank:Checking -500.00 USDWith Beancount 3.2.3 and beanquery 0.2.0, loading this starter produces no errors, the AP account nets to zero, and the invoice+due query returns a single group (Forest Paint Supply, INV-10455, 2025-09-04) whose amount is empty (settled) — not a billed row plus an orphan (…, null, null) payment row.
This guide is for educational purposes and does not constitute tax, legal, or financial advice.
References & Further Reading:
- Beancount language syntax — accounts, metadata, tags, links, and documents (verified 2026-09-11)
- Beancount Query Language (BQL) reference (verified 2026-09-11)
- Fava features — documents upload and
link_documents(verified 2026-09-11) any_meta()for querying transaction/posting metadata (Martin Blais, Beancount mailing list; verified 2026-09-11)





