# elnora-merit-aktiva

[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![npm version](https://img.shields.io/npm/v/@elnora-ai/merit-aktiva.svg)](https://www.npmjs.com/package/@elnora-ai/merit-aktiva)
[![CI](https://github.com/Elnora-AI/elnora-merit-aktiva/actions/workflows/ci.yml/badge.svg)](https://github.com/Elnora-AI/elnora-merit-aktiva/actions/workflows/ci.yml)

Do your [Merit Aktiva](https://www.merit.ee/) accounting from the command line — or let Claude Code do it for you.

Merit Aktiva is Estonia's cloud accounting platform. This repo gives you two ways to drive its API:

- **`elnora-merit` — a CLI** that covers the entire Merit Aktiva API (invoices, payments, VAT/KMD, ledger, reports, and more) plus Merit Palk payroll. JSON in, JSON out.
- **A Claude Code plugin** that wraps the CLI in skills, agents, and slash commands, so you can ask in plain English ("book this purchase invoice", "file my KMD") and Claude runs the right commands the right way.

Everything is universal — nothing about any one company is hardcoded. Point it at your own Merit credentials and it works.

> Requires a Merit Aktiva **Pro** or **Premium** license — the API is not available on lower tiers.

---

## Install

### As a CLI

```bash
npm install -g @elnora-ai/merit-aktiva
```

This puts the `elnora-merit` command on your PATH. Node.js ≥ 20 required.

### As a Claude Code plugin

Paste these two slash commands into Claude Code one at a time — wait for the first to finish before the second:

```
/plugin marketplace add Elnora-AI/elnora-merit-aktiva
```

```
/plugin install merit-aktiva-workspace@elnora-merit-aktiva
```

The plugin uses the `elnora-merit` CLI under the hood, so install that first.

### Make the write agents actually fire

The plugin ships three agents that wrap the CLI with guardrails — resolve the customer, build per-rate VAT totals, preview the payload, gate on your approval:

| When you ask to… | Agent |
|---|---|
| create / bill a **sales invoice** | `merit-aktiva-workspace:merit-invoice-creator` |
| **record a payment**, enter a **purchase invoice**, **reconcile** open items | `merit-aktiva-workspace:merit-bookkeeper` |
| **look up a company** (legal name / VAT / address / e-invoice capability) | `merit-aktiva-workspace:merit-company-lookup` |

A host that only loads the `merit-*` skills gets a *recipe* and then hand-runs the CLI itself — skipping those guardrails. To make the agents fire at the right time, add one routing rule to **your host's own instructions** (Claude Code `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or persistent agent memory), e.g.:

> For Merit **write/lookup** work (create invoice, record payment, enter purchase, reconcile, company lookup) dispatch the `merit-aktiva-workspace` agent above — do not hand-run the `elnora-merit` CLI. **Read/report/VAT** work (KMD, reports, Stripe/LHV import, reverse charge, payroll) has no agent — use the `merit-*` skills directly.

That write-vs-read split is the whole rule: agent-first only where an agent exists.

---

## Authenticate

Generate an **API ID** and **API Key** in Merit Aktiva:
**Settings → Company data → API settings → "Koosta võti" (Generate key)**.

The CLI reads them from environment variables. On first run it also prompts and saves them to `~/.config/elnora-merit/.env` (mode `0600`):

```bash
export MERIT_API_ID=your-api-id
export MERIT_API_KEY=your-api-key
```

Or copy [`.env.template`](.env.template) to `.env` and fill it in (`.env` is gitignored — never commit it).

| Variable | Required | Notes |
|---|---|---|
| `MERIT_API_ID` | yes | GUID from API settings |
| `MERIT_API_KEY` | yes | base64 secret; the HMAC signing key |
| `MERIT_API_VERSION` | no | `v1` (default) or `v2` for dual-version endpoints |
| `MERIT_PALK_API_ID` | for `palk` | Merit Palk API ID (separate payroll product) |
| `MERIT_PALK_API_KEY` | for `palk` | Merit Palk API key (base64 secret) |
| `MERIT_REFERENCES_DIR` | no | Base dir for config + reference files (stripe map, ledger, overrides, `company-profile.json`). Default `~/.config/elnora-merit`. |

### Company profile (optional)

Snapshot your account's own chart of accounts, banks, VAT codes, and financial years so
an agent (or you) can look up the real codes without transcribing them by hand:

```bash
elnora-merit profile sync                  # → company-profile.json (in MERIT_REFERENCES_DIR)
elnora-merit profile show --section taxes  # the VAT TaxId guids, etc.
```

`company-profile.json` holds no secrets but is company-specific, so it is gitignored.
Re-run `profile sync` whenever the chart of accounts changes.

### Estonian Business Register lookups (optional)

**This step is entirely optional.** Everything else in the CLI and the Merit agent works
without it. Setting it up just adds one extra capability: the `elnora-merit ariregister`
commands, which pull company data straight from the Estonian Business Register (äriregister)
using RIK's **free** ("tasuta") API services — so an agent can fill a new customer's legal
name, VAT number, and address, and check e-invoice capability, from the authoritative source
instead of asking you.

One of the two lookups needs no setup at all:

```bash
# e-invoice capability — works with NO credentials:
elnora-merit ariregister e-invoice-check 16818352
```

The requisite lookup needs a free äriregister contract + login. To enable it:

1. **Register for the free API tier.** Go to [rik.ee → e-äriregister → XML teenus](https://www.rik.ee/et/e-ariregister/xml-teenus)
   and conclude a contract for the **"Ainult e-äriregistri tasuta API-teenused"** (free API
   services only) tier at the [e-äriregister portal](https://ariregister.rik.ee/). You log in
   with an Estonian ID-card / Mobiil-ID / Smart-ID. The free tier has no per-query cost. See
   RIK's [contractual-client admin guide](https://abiinfo.rik.ee/e-ariregistri-paringud/juhised-lepingulise-kliendi-administraatorkasutajale).
2. **Create an XML-authorised user.** In the portal: **Haldus → Kasutajate haldamine → Lisa
   kasutaja**, and enable XML/API access for it. (Optionally set the credit limit to `0` so
   only free services can ever run.)
3. **Get the username + password.** The username is shown in the users table; generate the
   password with the **"Uus parool"** button (note: this XML-service password is separate from
   your portal login, and a freshly generated one can take ~10 minutes to activate).
4. **Save them to `~/.config/elnora-merit/.env`** (mode `0600`, gitignored — never commit):

   ```bash
   ARIREG_XML_USER=YOURUSERNAME
   ARIREG_XML_PASSWORD=your-xml-service-password
   ```

Then both lookups work:

```bash
elnora-merit ariregister requisites 16818352      # → name, VAT, status, address
elnora-merit ariregister e-invoice-check 16818352 # → OK/MR + e-invoice operator
```

Only the free services are exposed; billable queries (detailed data, beneficial owners,
representation rights) are intentionally excluded. See the `merit-company-lookup` skill/agent.

### LHV bank connection (optional — recommended if you bank with LHV)

**Optional. Everything else works without it.** Connecting it automates the other half of the
books: instead of exporting a statement file from your internet bank and uploading it, your
agent pulls the statement from LHV directly and imports it into Merit.

[LHV](https://lhv.ai/) (AS LHV Pank) publishes its own **read-only** MCP server, and this
plugin already bundles it — there is nothing to install and **no key to add**. Just connect it:

1. Run **`/mcp`**, pick **lhv**, choose **Authenticate**.
2. Sign in exactly as you would in your internet bank — **Smart-ID, Mobiil-ID, ID-card, or
   biometrics**.
3. On LHV's consent screen pick the scopes. Statement import needs **both** `accounts:read`
   and `transactions:read`.

The token lives in your own MCP client for 30 days. **This plugin never sees or stores it**,
and LHV exposes **no write scope at all** — an assistant can read your accounts and can never
move money. Revoke any time: internet bank → **Settings → Active sessions**.

**What the connection gives you:**

| Ask your agent | What it uses |
|---|---|
| "What's our balance?" | every account — IBAN, currency, available balance |
| "How much is actually available vs reserved?" | available, **settled**, and **reserved** balance per IBAN |
| "What did we spend on X last month?" | totals, top counterparties, income vs spend (max 31 days) |
| "Show me last week's transactions" | the full statement — what came in, what went out |
| **"Import July's statement into Merit"** | the statement as **camt.053**, straight into `payments import-statement` — no file export |

The last one is the point: LHV returns a real ISO 20022 **camt.053** statement, which is
exactly what Merit's bank import accepts, so the bank and the books talk to each other with no
file in between. The **`merit-lhv`** skill drives it — and its first step is checking whether
the period is *already booked*, because importing an already-booked month double-books it.

Estonia-only, and only for LHV customers. Other banks: export camt.053 from your internet bank
and use `payments import-statement --file` as usual.

---

## Quickstart

```bash
elnora-merit accounts list                              # chart of accounts
elnora-merit banks list                                 # bank accounts
elnora-merit taxes list                                 # VAT rates
elnora-merit customers list --name "Acme"               # find a customer
elnora-merit sales-invoices list --period-start 2026-01-01 --period-end 2026-03-31
elnora-merit reports income-statement --end-date 20260331 --per-count 3

# Output controls (work on any command)
elnora-merit accounts list --output table --fields Code,Name
elnora-merit accounts list --pretty                     # pretty JSON
```

**Creating documents.** Create/send commands take the Merit JSON body via `--data` (inline) or `--file` (path). Each command's `--help` lists the required fields:

```bash
elnora-merit sales-invoices create --file invoice.json
```

See the [official Merit reference manual](https://api.merit.ee/connecting-robots/reference-manual/) for field details.

---

## What you can do

Full coverage of the Merit Aktiva REST API — **22 resource groups** — plus four local helpers: `profile` (snapshot your account's codes), `reconcile` (book Stripe payouts), `documents` (audit + attach missing receipts), and `ariregister` (free Estonian Business Register lookups). Run `elnora-merit <group> --help` for per-command options and payload schemas.

| Group | Commands |
|---|---|
| `sales-invoices` | list, find, get, create, create-v2, create-credit, create-multi-payment, create-from-xml, get-pdf, send-email, send-einvoice, delete |
| `sales-offers` | list, get, create, create-v1, update, set-status, create-invoice |
| `recurring-invoices` | create, list, get, list-client-addresses, send-indication-values |
| `purchase-invoices` | create, create-pending, create-pending-xml, list, find, list-pending, get, delete, pay, report |
| `inventory` | list, list-locations, send, send-v1 |
| `payments` | list, find, list-types, create, create-purchase, create-offer, delete, list-income, list-expense, send-income, send-expense, send-prepayment, send-prepayment-vendor, send-settlement, import-statement, list-imports |
| `gl` | create, list, get, list-full |
| `fixed-assets` | list, list-locations, list-responsible-persons, send |
| `taxes` | list, create |
| `customers` | list, create, update, create-group, list-groups |
| `vendors` | list, create, update, update-v1, create-group, list-groups |
| `accounts` | list |
| `projects` | list |
| `cost-centers` | list |
| `dimensions` | list, create, create-values |
| `departments` | list |
| `prices` | list, get, send, list-discounts, send-discounts |
| `units` | list, create |
| `banks` | list |
| `financial-years` | list |
| `items` | list, list-groups, create, create-group, update |
| `reports` | income-statement, balance-sheet, inventory, sales, purchase, customer-debts, customer-payments, more-data |
| `profile` | sync, show — snapshot the account's chart / banks / VAT codes / financial years for lookups |
| `reconcile` | init, preview, run, status — book Stripe payouts into Merit (see below) |
| `documents` | list-missing, run, install-schedule — find transactions missing their receipt/invoice, locate the file, stage/attach it, digest to Slack (see below) |
| `ariregister` | requisites, e-invoice-check — free live Business Register lookups (company name/VAT/address; e-invoice capability) |

### Payroll (Merit Palk)

`palk` is Merit's separate payroll product. It uses its own credentials (`MERIT_PALK_API_ID` / `MERIT_PALK_API_KEY`) and needs a Merit Palk **PRO** license.

```bash
elnora-merit palk employees list
elnora-merit palk base-salary list --start-month 202601 --end-month 202612
elnora-merit palk gl get --month 202606                   # GL batch for a month
elnora-merit palk salary-report --start-date 2026-06-01 --end-date 2026-06-30
```

| Group | Commands |
|---|---|
| `palk employees` | list, create |
| `palk contacts` | add |
| `palk base-salary` | list, create |
| `palk salary` | create |
| `palk absences` | create |
| `palk gl` | get |
| `palk vacation` | balance, set-liability |
| `palk salary-report` | _(leaf command)_ |
| `palk dimensions` | set |
| `palk tax-free` | set |
| `palk reduced-capacity` | set |

> Palk dates are `YYYY-MM-DD` and months are `YYYYMM`. See the [Palk reference manual](https://api.merit.ee/merit-palk-api/palk-reference-manual/).

### Reconcile Stripe payouts

Book Stripe card sales, fees, and refunds into Merit, one **payout** at a time (a payout = one bank deposit). Works for any Stripe account → any Merit company.

```bash
elnora-merit reconcile init                 # write a config template to ~/.config/elnora-merit/stripe-map.json
# ...edit that file: account codes + VAT (see stripe-map.example.json)

export STRIPE_API_KEY=sk_live_...           # a read-only restricted key is enough

elnora-merit reconcile preview --output table   # read-only: shows exactly what would be booked
elnora-merit reconcile run --yes                # book it (idempotent — never books a payout twice)
elnora-merit reconcile status                   # booked vs outstanding
```

Each payout becomes one balanced summary GL batch: card sales debit a clearing account, revenue is credited net of VAT (the VAT posts implicitly from the revenue line's tax tag, which auto-populates the KMD), and fees are booked as a separate expense. Your real bank-import row then clears the payout net in Merit — no double posting. The connector refuses to book a payout whose figures don't balance. See [docs/stripe-reconciliation-spec.md](docs/stripe-reconciliation-spec.md) for the full design.

### Import an LHV bank statement — no file export

If you bank with LHV and connected it above (`/mcp` → lhv), the statement comes straight from
the bank as **camt.053** — the format Merit's import already accepts.

```bash
elnora-merit banks list                                  # bankId + IBAN + currency (live, no config file)
elnora-merit reports balance-sheet --end-date 20260731   # STEP 0: already booked? then stop
elnora-merit payments import-statement --file statement.xml
elnora-merit payments list-imports <bankId> --booking-date-from 2026-07-01
```

**Check the period isn't already booked before importing.** If the bank account's GL balance
already equals the bank's real closing balance, the month is booked and there is nothing to
import — the usual case. Merit's own idempotency will not catch this for you: it cannot see
payments posted through the API. Then match rows in the Merit UI, where **Võlgnevused** clears
an invoice that already exists and **Muud** creates a *new* expense — picking `Muud` for an
already-invoiced payment books it twice. The **`merit-lhv`** and **`merit-payments-bank`**
skills carry the full procedure and the traps.

### Document sync — never lose a receipt

Every transaction needs its source document. `documents` audits Merit for invoices with no attachment, searches the places your receipts live, stages or attaches the file, and reports the rest.

```bash
elnora-merit documents list-missing --from 2026-01-01 --to 2026-07-31   # read-only audit
elnora-merit documents run                       # audit → search sources → match → digest (read-only)
elnora-merit documents run --apply               # stage matched PDFs for a one-click UI upload
elnora-merit documents install-schedule          # unattended run on an interval (macOS launchd)
```

Sources are generic: local folders, plus a `command` adapter that lets you plug in Gmail, Google Drive, or a scanner **without this package holding those credentials** (your command fetches and prints candidates as JSON). A ready-made Gmail+Drive adapter ships at [`adapters/gmail-drive-gw.mjs`](adapters/gmail-drive-gw.mjs) — we recommend installing the companion [**elnora-google-workspace**](https://github.com/Elnora-AI/elnora-google-workspace) plugin so the two connect and your receipts flow from email/Drive into Merit automatically. Merit's API can only attach a file when an invoice is *created*, so the default resolves a backlog by **staging** the matched PDF for a two-second UI upload (`--rebook` delete+recreates in place). The digest posts to a Slack-compatible webhook (`MERIT_DOCSYNC_WEBHOOK`). See [docs/document-sync.md](docs/document-sync.md).

---

## What's in this repo

```
elnora-merit-aktiva/
├── src/                    # the elnora-merit CLI (TypeScript)
├── skills/                 # Claude Code how-to skills (the right Merit procedure for each task)
├── agents/                 # plugin agents
├── commands/               # plugin slash commands
├── docs/                   # Stripe reconciliation spec and design notes
├── .claude-plugin/         # plugin + marketplace manifest
├── AGENTS.md               # agent usage conventions
└── INSTALL_FOR_AGENTS.md   # step-by-step agent setup walkthrough
```

The plugin ships how-to skills for both products so Claude follows the correct Merit procedure, not just the raw API:

- **Accounting** — `merit-aktiva-workspace` (router), `merit-sales-invoices`, `merit-purchase-invoices`, `merit-payments-bank`, `merit-vat-kmd`, `merit-reports`, `merit-reverse-charge`, `merit-stripe`, `merit-lhv` (LHV bank statements via LHV's official read-only MCP server).
- **Payroll** — `merit-palk-workspace` (router), `merit-palk-employees`, `merit-palk-payroll`, `merit-palk-reports`, `merit-palk-settings`.

---

## Design

- **Universal & open** — no account-specific values baked in. Configure with env vars; works for any Merit company.
- **Correct by construction** — HMAC-SHA256 request signing, verified against Merit's published test vector.
- **Agent-friendly** — JSON by default, machine-readable error envelopes with dedicated exit codes, `--data`/`--file` for complex payloads.
- **Safe** — destructive operations require an explicit `--yes`; credentials and signatures are redacted from all output.

### Output & errors

- **Formats:** `--output json` (default, compact), `table`, or `csv`. `--pretty` for indented JSON. `--fields a,b` to pick columns.
- **Errors** are JSON on stderr with a message, a suggestion, and structured data.
- **Exit codes:** `0` success · `1` general · `2` validation · `3` auth · `5` rate limited · `6` API error.

### Notes & gotchas

- Merit endpoints are **POST with a JSON body**, even read/query operations.
- **Dates:** query fields use `YYYYMMDD` (the CLI also accepts `YYYY-MM-DD` and normalizes). Some payload fields use `YYYYMMDDHHMMSS` — see each command's help.
- **Period limits:** invoice list queries span at most 3 months.
- **Batch limit:** at most 500 rows per document.
- **Sales invoices cannot be updated via the API** — there is no update endpoint, so the API route is delete and re-create. The Merit UI edits an invoice in place, which is usually the better fix for a single wrong field. A **paid** invoice is locked in both: remove the payment first. Merit does not issue invoice numbers; manage your own.
- **Rate limit:** 100 requests/minute. The CLI auto-retries HTTP 429 honouring `Retry-After`.

---

## Part of the Elnora family

Open-source agent tooling from [Elnora AI](https://github.com/Elnora-AI) — free, universal, config-driven tools that wire Claude Code (or any AI coding agent) into the systems you run your company on. Each works 100% standalone; install several and they chain into end-to-end workflows.

<!-- ELNORA-FAMILY:START -->
- [elnora-linear](https://github.com/Elnora-AI/elnora-linear) — Linear issue management — search, bulk edit, agents, and a config-driven curator
- [elnora-slack](https://github.com/Elnora-AI/elnora-slack) — the entire Slack Web API as a CLI plus agent skills with a draft-and-approve send gate
- [elnora-whatsapp](https://github.com/Elnora-AI/elnora-whatsapp) — read, search, and send WhatsApp from your own paired account, 100% local
- [elnora-google-workspace](https://github.com/Elnora-AI/elnora-google-workspace) — Gmail, Calendar, Drive, Docs, Sheets, Forms, Tasks, plus any Google API via Discovery
- [elnora-vanta](https://github.com/Elnora-AI/elnora-vanta) — read-only Vanta compliance — frameworks, tests, controls, and vulnerabilities as agent-friendly JSON
- [elnora-luma](https://github.com/Elnora-AI/elnora-luma) — Luma (lu.ma) events — all 61 public API endpoints as a spec-driven CLI with safety guardrails
- [elnora-travel](https://github.com/Elnora-AI/elnora-travel) — a real travel agent — live flights, hotels, Airbnb, Booking.com, and routes in one itinerary
- [elnora-websearch-tools](https://github.com/Elnora-AI/elnora-websearch-tools) — web search — Exa, Tavily, Perplexity, Firecrawl, and Valyu CLIs and skills in one plugin
- [knowledge-vault](https://github.com/Elnora-AI/knowledge-vault) — an Obsidian-compatible knowledge base for agent teams — search and save your work to any vault
<!-- ELNORA-FAMILY:END -->

## Development

```bash
pnpm install
pnpm dev -- accounts list      # run from source
pnpm typecheck && pnpm lint && pnpm test && pnpm build
```

See [CONTRIBUTING](.github/CONTRIBUTING.md) and [SAFETY](SAFETY.md).

## License

[Apache-2.0](LICENSE) © Elnora AI. Not affiliated with or endorsed by Merit Tarkvara AS.
</content>
</invoke>
