akt — Akaunting CLI toolbox
Drive your Akaunting accounting instance entirely from the command line
aktgives you full create / read / update / delete for customers, vendors, items, invoices, bills, payments, banks, categories, taxes, currencies and transfers — plus double-entry journal entries and the chart of accounts, and arawescape hatch for any other endpoint. Built and tested against Akaunting 3.1.x; works with any 3.x deployment that exposes the REST API.
Install
From PyPI (the distribution is akt-cli; the command is akt):
uv tool install akt-cli # installs the `akt` command globally
# or
pip install akt-cli
# or run without installing
uvx --from akt-cli akt --help
From a checkout (the project is managed with uv):
uv sync # create .venv and install
uv run akt --help # run without activating
uv tool install . # install the `akt` command from source
Configuration
akt needs a base URL, an admin email + password, and a company id. They are
resolved in this order (first wins):
- CLI flags:
--base-url,--email,--password,--company(given before the subcommand, e.g.akt --company 2 customer list). - Environment:
AKT_BASE_URL,AKT_EMAIL,AKT_PASSWORD,AKT_COMPANY,AKT_THROTTLE. - A dotenv file —
$AKT_ENV_FILE, then./.env, then~/.config/akt/akt.env. Akaunting's own install keys are recognised too:APP_URL,AKAUNTING_ADMIN_EMAIL,AKAUNTING_ADMIN_PASSWORD.
Minimal ~/.config/akt/akt.env:
AKT_BASE_URL=https://accounting.example.com
AKT_EMAIL=admin@example.com
AKT_PASSWORD=your-password
AKT_COMPANY=1
Then:
uv run akt ping
uv run akt company
Authentication is HTTP Basic against your Akaunting admin user.
Concepts mapped to Akaunting
Akaunting folds several nouns onto shared endpoints; akt hides that:
| akt noun | API endpoint | notes |
|---|---|---|
customer |
contacts |
contact of type customer |
vendor |
contacts |
contact of type vendor (supplier) |
invoice |
documents |
document of type invoice |
bill |
documents |
document of type bill |
payment |
transactions |
income (invoice) or expense (bill) transaction |
journal-entry |
journal-entry |
double-entry general-ledger entry (module) |
account |
chart-of-accounts |
GL accounts (general ledger) — read via API, CRUD via web |
bank |
accounts |
bank / cash accounts (the money, not the GL) |
item, category, tax, currency, transfer |
as named |
journal-entryandaccountrequire the Double-Entry module installed on the instance. The module publishes chart-of-accounts read-only on the/apisurface (index/show); its create/update/delete live only on the session/CSRF web route.akt accountgives you the full verb set anyway —list/gethit/api, whilecreate/update/deletetransparently drive the web CRUD with your admin session (the same mechanismdownload-attachmentalready uses).
The
contactsanddocumentsendpoints derive their permission from asearch=type:<x>query param.aktinjects this automatically — calling them raw without it returns403 necessary access rights.
Verbs
Every resource supports:
akt <noun> list [--search 'field:value'] [--all] [--limit N] [--json]
akt <noun> get <id>
akt <noun> create --field value ...
akt <noun> update <id> --field value ...
akt <noun> delete <id>
akt <noun> enable <id> # where applicable
akt <noun> disable <id>
Bills, invoices and payments additionally support attachments (scanned bills, receipts, PDFs):
akt <noun> create ... --attachment ./file.pdf # repeatable; upload on create
akt <noun> update <id> --attachment ./file.pdf # attach to an existing record
akt <noun> update <id> --remove-attachment # clear existing attachment(s)
akt <noun> attachments <id> # list attached files (id, name, size)
akt <noun> download-attachment <id> [--out DIR] [--media-id ID]
Output is a table by default; add --json (works before or after the verb) for
raw JSON suitable for piping into jq.
Three ways to set body fields on create/update:
- typed flags shown by
akt <noun> create --help --set key=value(repeatable; values are JSON-coerced, so--set enabled=0)--data '<json>'or--data @file.json(merged last, wins over everything)
Examples
# Contacts
akt customer create --name "Northwind Traders" --email ar@northwind.com --currency-code USD
akt vendor create --name "Office Supply Co" --email billing@osc.com
akt customer list --search 'name:Northwind'
akt customer update 12 --phone "555-2000"
akt customer disable 12
# Items, categories, taxes
akt item create --name "Consulting Hour" --sale-price 150 --purchase-price 0
akt category create --name "Services" --type income
akt tax create --name "Sales Tax" --rate 8.25
# Bank / cash accounts (the money side — see the COA section below for GL accounts)
akt bank create --name "Business Checking" --number 1001 --currency-code USD
akt bank list
# Open a book with a prior-period opening balance. The Double-Entry module
# auto-posts an opening-balance journal entry dated to the account's creation
# date; --opening-balance-date re-dates it to the period boundary so it lands in
# the right financial year. Needs a positive --opening-balance.
akt bank create --name "Business Checking" --number 1001 --currency-code USD \
--opening-balance 5000 --opening-balance-date 2024-12-31
# Re-date the opening entry of an existing account:
akt bank update 3 --opening-balance-date 2024-12-31
# Invoice with line items (totals computed server-side; number auto-generated)
akt invoice create --contact 12 \
--item 'name=Consulting,price=150,quantity=10,item_id=2' \
--item 'name=Setup fee,price=500,quantity=1' \
--status sent
# Record a customer payment against that invoice (amount defaults to amount due)
akt payment create --invoice 34
# Partial payment of a specific amount via bank transfer
akt payment create --invoice 34 --amount 750 \
--payment-method offline-payments.bank_transfer.2
# Bills and vendor payments work the same way
akt bill create --contact 13 --item 'name=Paper,price=40,quantity=5'
akt payment create --bill 41
# Attachments: upload the source PDF/scan and fetch it back later
akt bill create --contact 13 --item 'name=Paper,price=40,quantity=5' \
--attachment ./supplier-bill.pdf
akt payment update 57 --attachment ./receipt.pdf # attach to an existing payment
akt bill attachments 41 # list attached files
akt bill download-attachment 41 --out ./downloads # save to disk
akt payment update 57 --remove-attachment # clear attachments
# Double-entry general ledger (requires the Double-Entry module)
akt account list # read the chart of accounts
akt account get 12
# Build the chart of accounts as code (create/update/delete run via the web
# session; type-id is the double-entry account-type id — copy it from an
# existing account's `type_id`)
akt account create --name "Cash on Hand" --code 1010 --type-id 6
akt account create --name "Petty Cash" --code 1011 --type-id 6 --parent-id 12
akt account update 12 --code 1000 --description "Operating cash"
akt account delete 12
# Post a balanced journal entry (>= 2 lines; debits must equal credits;
# journal number auto-generated, basis defaults to accrual)
akt journal-entry create --description "Owner capital contribution" \
--item 'account_id=10,debit=5000' \
--item 'account_id=30,credit=5000'
akt journal-entry list
akt journal-entry update 4 --description "Corrected memo"
akt journal-entry create --description "Vendor bill accrual" --basis accrual \
--item 'account_id=60,debit=250' --item 'account_id=21,credit=250' \
--attachment ./invoice.pdf
## COA config: link categories and accounts (Xero-style)
Akaunting keeps *categories* (required on every transaction) separate from the
double-entry *chart of accounts*. Point akt at a COA config and it keeps them in
lockstep: one list to maintain (accounts), a 1:1 mirror of categories generated
from it, and `payment create` coded by `--account` — with the mirror category
filled in automatically.
akt finds the config at `--coa FILE` → `AKT_COA_FILE` → `./coa.toml` →
`~/.config/akt/coa.toml`. Minimal schema (extra keys are ignored):
```toml
[[account]]
code = 400
name = "API Subscription Revenue"
type_id = 13 # DoubleEntry account type; income/expense class -> category type
# optional: category = "Revenue" (override the mirror name)
# optional: mirror = false (skip mirroring, e.g. bank/AR/AP accounts)
akt coa diff # preview: accounts/categories to create or rename
akt coa sync # apply (create + rename; idempotent)
akt coa sync --prune # also DISABLE accounts/categories absent from the config
# code a transaction by GL account — akt auto-fills the mirror category:
akt payment create --type expense --bank 1 --amount 120 --account 628
# ...or by category name — akt reverse-fills the matching GL account:
akt payment create --type expense --bank 1 --amount 120 --category "Cloud Hosting"
--account/--category are two ends of the same coin: give either and akt fills
the other from the COA config, so the category report and the COA report always
agree. --account takes a GL code or name; --category a mirror-category name
(see --bank for the bank/cash account). An explicit --set de_account_id= still
wins.
Split one transaction across several GL accounts
--split posts one bank transaction to multiple GL accounts at once — the way
an invoice's line items each hit their own account. Repeat it per leg as
account=<code|name>,debit=<x> or account=<code|name>,credit=<x>; the legs must
net to --amount (income legs credit-heavy, expense legs debit-heavy):
# one $13,400 PayPro deposit that books gross revenue, a refund and a fee together
akt payment create --type income --bank 20 --amount 13400 \
--split 'account=400,credit=13686' \
--split 'account=545,debit=280' \
--split 'account=605,debit=6'
Needs the akt-api companion module — it writes the extra DoubleEntry
item legs (a standalone transaction can't natively carry more than one). The
transaction keeps a single category_id, a label just like a multi-item
invoice's, so Akaunting's native category report shows it under one bucket; the
DoubleEntry GL (the item legs) is the source of truth.
Enforcement (when a --coa config is loaded): akt refuses to create a
standalone income/expense transaction that has no GL account — you must pass
--account, --category, or an explicit --set de_account_id=. This makes the
"category set but GL account left to the module's 628/400 default" mistake
impossible. Bill/invoice payments (which post to A/P–A/R) are exempt, and with no
--coa config the old behaviour is unchanged. Use akt verify to catch any
transactions (e.g. web-UI entries) that slipped past.
Refunds — mind the report's type-guard
Akaunting's DoubleEntry chart-of-accounts P&L report only counts a posting when
the transaction type matches the account class — an income transaction on an
expense account (or an expense on an income account) is silently dropped
from the report even though it balances in the ledger. This bites refunds: a
vendor refund is money in, so Akaunting makes it an income transaction — but if
you code it onto the expense account it was reversing, the report ignores it and
overstates that expense. (Swapping the debit/credit to keep it "on the expense
account" doesn't help: Akaunting picks Dr/Cr from the type, rejects negative
amounts, and re-posts on edit.)
Book a refund so its type matches its account's class:
- To a matching-class account — a money-in refund → an income-class account (e.g. a dedicated "Vendor Refunds" income account). Net profit is unchanged; the original expense stays gross.
- As a journal entry (
Dr bank / Cr expense) — the report's separate journal path nets it by account class regardless of type. Caveat: a JE that touches a bank account also spawns a bank-register transaction.
akt verify flags any standalone income/expense transaction posted to an
opposite-class account (and exits non-zero), so you can gate CI on it and never
ship a refund the report will quietly ignore.
Anything else: raw API access
akt raw GET reports akt raw POST items --data '{"name":"Ad-hoc","type":"service","sale_price":99}' akt company akt settings --search 'key:default.account'
## Foreign currency
Akaunting stores every transaction in its own currency plus a `currency_rate`
snapshot (foreign units per 1 unit of the company default currency); the base
value is `amount ÷ currency_rate`. In the web UI that rate is auto-filled from
the currency's live rate — but over the API you must supply it, and akt used to
default it to `1`, silently mis-stating any foreign transaction. akt now fetches
the right rate for you, **for the transaction's own date**, from keyless public
feeds:
* **majors** (~30 ECB currencies) — [Frankfurter](https://frankfurter.dev)
* **ARS** (Argentine peso, which ECB doesn't publish) — historical by date from
[ArgentinaDatos](https://argentinadatos.com), latest from
[dolarapi.com](https://dolarapi.com), with a selectable *casa*
(oficial/blue/bolsa[MEP]/contadoconliqui[CCL]/…) and price side.
```bash
# Auto-fills currency_rate for the payment's date (paid_at). No extra flags.
akt payment create --type expense --bank 3 --amount 100 --currency-code EUR
# Back-dated entry books at that day's historical rate:
akt bill create --contact 13 --currency-code EUR \
--item 'name=Hosting,price=100,quantity=1' --issued-at 2025-06-02
# Argentine peso, defaulting to the MEP (bolsa) rate at the mid price:
akt payment create --type expense --bank 5 --amount 500000 --currency-code ARS
# ...or pick the rate type / side per transaction:
akt payment create --type expense --bank 5 --amount 500000 --currency-code ARS \
--ars-casa blue --ars-side venta
# Override or opt out:
akt payment create ... --currency-code EUR --currency-rate 0.905 # pin it yourself
akt payment create ... --currency-code EUR --no-auto-rate # store 1 (legacy)
akt payment create ... --currency-code EUR --rate-date 2025-06-30 # a different rate date
The currency must first exist in Akaunting (it rejects a transaction in an
unconfigured currency): add it once with akt currency create --name Euro --code EUR --rate 0.9 (any rate — each transaction stores its own).
If a rate can't be resolved (offline, feed down, an unsupported currency, or a
future date) akt fails loudly rather than booking at 1 — pass
--currency-rate to set it manually. Historical rates are cached under
~/.cache/akt/fx/; set AKT_FX_DISABLE=1 to force fully offline.
Preview or convert a rate without touching a transaction:
akt fx EUR # latest EUR per 1 default-currency, and the inverse
akt fx ARS --on 2025-06-02 # historical, MEP/mid by default
akt fx ARS --ars-casa blue --amount 500000 # convert 500,000 ARS to the base currency
Reporting. With the companion module updated (see below), akt's reports
mirror Akaunting exactly: trial-balance, report profit-loss, report balance-sheet and balance are computed in the company default currency,
converting each ledger leg at its historical rate the way Akaunting's own
statements do — so a EUR or ARS posting is no longer summed at face value.
akt ledger shows each leg as posted (its own currency) alongside a
converted column, so nothing is silently mislabelled. Defaults: ARS casa bolsa
(MEP), price mid; override globally with AKT_FX_ARS_CASA / AKT_FX_ARS_SIDE.
The reporting fix lives in the akt-api companion module — update it in your Akaunting
modules/directory (seeakt-api/README.md) for base-currency reports. The rate-input feature above needs only the CLI.
Financial reports (needs the akt-api module)
Akaunting's trial balance, P&L, and balance sheet aren't in its JSON API. With the companion module installed, akt computes them from the ledger:
akt trial-balance --to 2025-12-31 # every account, debits = credits
akt report profit-loss --from 2025-01-01 --to 2025-12-31
akt report balance-sheet --to 2025-12-31
akt balance --account 105 --to 2025-12-31 # one account's balance
akt balance --account 200 --to 2025-12-31 --expected 0 # reconcile: exit 1 on mismatch
--expected turns akt balance into a reconciliation primitive (compare an
account to an external figure, non-zero exit on a mismatch). Balances aggregate
through Akaunting's ledger relation, so soft-deleted transactions are excluded
just as its own reports do.
Reading the ledger: the akt-api companion module
Akaunting's Double-Entry app exposes only journal-entry and chart-of-accounts
over the API — the ledger itself (where every transaction's postings land) has no
endpoint. The optional akt-api companion module (in akt-api/, install it
into your Akaunting modules/ directory — see akt-api/README.md) adds a
read-only GET /api/akt/ledgers. When it's present, extra commands light up; when
it's not, they print an install hint and everything else works unchanged.
# Show what actually posted to a GL account (by code or name):
akt ledger --account 628 --from 2025-01-01 --to 2025-12-31
# Audit: every standalone income/expense transaction whose ACTUAL posted GL
# account differs from the account that mirrors its category. Needs a --coa
# config (to know the mapping) and the companion module (to read the ledger).
# Exits non-zero when it finds mis-postings — handy in CI / a pre-close gate.
akt --coa coa.toml verify --from 2025-01-01 --to 2025-12-31
Because the Double-Entry module posts to the ledger by de_account_id, not by
category, a transaction can carry a tidy category yet post to the wrong GL
account (e.g. the module's default "Other / Uncategorized"). akt verify is how
you catch that — the category report and the COA report only agree when it comes
back clean.
Akaunting gotchas akt handles for you
Driving Akaunting's API directly has sharp edges; akt papers over these:
- Type-scoped ACL —
contactsanddocumentsneedsearch=type:<x>on every verb or the API returns403 necessary access rights. - Doubled totals — Akaunting recomputes a document's total from its line
items and adds it to the
amountyou send.aktalways sendsamount: 0so the server-computed total is authoritative. - Item
description— line items need adescriptionkey even when empty, or creation 500s withUndefined array key "description". - Updates wipe items — a document update deletes and recreates all line
items from the request.
aktresends the existing items on a partial update so they aren't lost. - Nested payment route — paying a document must POST to
documents/{id}/transactions; the flattransactionsendpoint rejects it. The same applies to updating a document-linked payment (e.g. attaching a file to it) —aktpicks the nested route automatically. - Multipart uploads — attachments switch the request from JSON to
multipart/form-datawith theattachment[]field; updates are sent asPOST+_method=PATCHbecause PHP won't populate$_FILESon a realPUT. - Attachment download isn't on
/api— Akaunting only serves attachment bytes from the session-authenticated web route/{company}/uploads/{id}/download.akt download-attachmenttransparently logs in a web session with your admin credentials (reused for the process) to fetch the file; metadata (id, name, size) comes from the/apirecord itself. - Full-replace updates — Akaunting PUT re-validates required fields, so
aktmerges your changes onto the current record. - Journal entries must balance — a
journal-entryneeds >= 2 lines whose debits equal its credits;aktvalidates this client-side (clear error) before hitting the API. Each line carries both adebitand acreditkey (the unused side sent as0) because Akaunting validates both as required. - Journal updates re-derive ledgers — like documents, a journal update
deletes any ledger line absent from the request.
aktresends the existing lines (with their ledger ids) so an update that only changes a field doesn't wipe the entry, and auto-generates thejournal_numberfrom the module'sdouble-entry.journal.number_*settings when you don't pass one. - Chart-of-accounts CRUD is web-only — the Double-Entry module exposes
accounts read-only on
/api; create/update/delete exist solely on the session/CSRF web route.akt account create|update|deletelogs in a web session (reusing your admin credentials, cached for the process), attaches the CSRF token, and unwraps Akaunting's{success, error, data, message}AJAX envelope — so a server-side block (e.g. deleting an account that has ledgers) surfaces as a normal error. Updates resendname(required by Akaunting on update) from the current record when you don't pass one.
Invoice creation may be gated by a plan check
In Akaunting 3.x, CreateDocument::authorize() gates invoice creation (only
type == invoice) behind a call to api.akaunting.com/plans/limits using the
apps.api_key setting. If that key is unset or the host can't reach
api.akaunting.com, invoice creation fails closed with
500 Not able to create a new user — in the web UI too, not just akt.
Bills, payments, contacts, items and transfers are unaffected. Fix it by setting
a valid apps.api_key (and allowing outbound HTTPS to api.akaunting.com).
Host bot-protection / throttling
Some hosts (e.g. cPanel with Imunify360) greylist an IP that issues a burst of
automated requests, returning an Access denied by … bot-protection page or
timing out. akt retries throttle/WAF responses with backoff, and
--throttle SECONDS (or AKT_THROTTLE) enforces a minimum gap between calls —
use --throttle 1 for bulk work. A durable fix is to whitelist your IP in the
host firewall.
Contributing
Developing, testing, or releasing akt? See CONTRIBUTING.md
for the test suite, CI/CD, release process, and a map of the source files.
License
MIT © AsyncAlchemist
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file akt_cli-0.12.0.tar.gz.
File metadata
- Download URL: akt_cli-0.12.0.tar.gz
- Upload date:
- Size: 56.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3748a71aab644c99627eb6b74f6db5bd6176d2686fbcd009734751197260633b
|
|
| MD5 |
6b9a8c2621005ef8fc03772635e1b013
|
|
| BLAKE2b-256 |
ff333dbdcd0d79cb6c1a6c93e9713c0687c3e4c5d9f7cc8b6de9efb94ee95949
|
Provenance
The following attestation bundles were made for akt_cli-0.12.0.tar.gz:
Publisher:
publish.yml on AsyncAlchemist/akt-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
akt_cli-0.12.0.tar.gz -
Subject digest:
3748a71aab644c99627eb6b74f6db5bd6176d2686fbcd009734751197260633b - Sigstore transparency entry: 2273748475
- Sigstore integration time:
-
Permalink:
AsyncAlchemist/akt-cli@0caba62dc105661e8ca52f5a4b4b46d8b2f3a508 -
Branch / Tag:
refs/tags/v0.12.0 - Owner: https://github.com/AsyncAlchemist
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0caba62dc105661e8ca52f5a4b4b46d8b2f3a508 -
Trigger Event:
release
-
Statement type:
File details
Details for the file akt_cli-0.12.0-py3-none-any.whl.
File metadata
- Download URL: akt_cli-0.12.0-py3-none-any.whl
- Upload date:
- Size: 61.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25700ef9dd2efcad5aa3c08fa3b11e7cb0e06ba743ebf4d05a140528427b8ccd
|
|
| MD5 |
e1e9cf3e7964be70a6c69540b527d426
|
|
| BLAKE2b-256 |
d589c523c6fb3cc564337ccddd5c2195492ee6494e35759061655d8354cbd410
|
Provenance
The following attestation bundles were made for akt_cli-0.12.0-py3-none-any.whl:
Publisher:
publish.yml on AsyncAlchemist/akt-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
akt_cli-0.12.0-py3-none-any.whl -
Subject digest:
25700ef9dd2efcad5aa3c08fa3b11e7cb0e06ba743ebf4d05a140528427b8ccd - Sigstore transparency entry: 2273748559
- Sigstore integration time:
-
Permalink:
AsyncAlchemist/akt-cli@0caba62dc105661e8ca52f5a4b4b46d8b2f3a508 -
Branch / Tag:
refs/tags/v0.12.0 - Owner: https://github.com/AsyncAlchemist
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0caba62dc105661e8ca52f5a4b4b46d8b2f3a508 -
Trigger Event:
release
-
Statement type: