# Our plugins
Source: https://docs.lerian.studio/en/about-plugins
Explore the plugins available across Lerian products — what each plugin does, when to enable it, and how it fits with Midaz and other core services.
Instead of building custom features from scratch, you can plug in what you need and **keep everything else stable, scalable, and secure**.
Plugins run alongside the product platform, adding new logic, endpoints, or workflows to support specific business needs. Plugin availability is product-specific. The currently documented plugins below extend Midaz.
Plugins are premium features currently available only to **Enterprise customers** and are licensed separately.
**CRM** is no longer a plugin. It is now an embedded component of Midaz, available out of the box with no additional license required. See [CRM Overview](/en/midaz/crm/crm-overview) for details.
**Fees Engine** is part of Midaz rather than a standalone plugin — a licensed capability embedded in the unified Midaz Ledger process. See [Fees Engine overview](/en/midaz/fees/fees-engine-overview) for details.
To understand how plugins work under the hood, see [What are plugins?](/en/platform/plugins/what-are-plugins)
## Available plugins for Midaz
***
Integrate Pix with Midaz, including SPI and DICT services, to manage keys, transactions, claims, and regulatory workflows.
Process electronic transfers between banks through TED, managing transaction validation, settlement, and regulatory compliance.
Issue boletos and pay bills — DARF taxes and utility invoices — through the Payments module in Lerian Console, recorded in your Midaz ledger.
## Building your own plugin
***
We provide deployment guides and design standards to help partners and internal teams build plugins that are reliable, secure, and easy to maintain.
[Visit the Partners Hub →](/en/partners-hub/introduction) for technical documentation and contribution guidelines.
If you're interested in building or enabling a plugin, talk to a Lerian representative. We'll help you evaluate, license, and deploy the right setup for your use case.
# Our products
Source: https://docs.lerian.studio/en/about-products
Discover Lerian products, platform services, and Brazil Rails — and how modular building blocks compose into a complete core banking platform.
Lerian is a complete, modular core banking platform. Each product is a modular building block that delivers end-to-end capabilities for a specific financial domain.
Products can be adopted individually or combined into a composable ecosystem. [Midaz](https://github.com/LerianStudio/midaz) and [Fetcher](https://github.com/LerianStudio/fetcher) are source-available under the Elastic License 2.0 (ELv2). The other Lerian products are closed source and available to licensed customers, with their repositories maintained internally.
## Products, platform, and Brazil Rails
***
Create ledgers, manage transactions, and use core entities like accounts and assets — with CRM and the Fees Engine built in as part of Midaz. Connect rails like Pix and TED.
Generate and download reports from reusable templates, filter their data, and manage recurring reporting deadlines. Supports HTML, XML, CSV, PDF, and TXT output.
Validate external transactions against the ledger and surface mismatches. Built for accuracy, traceability, and automation.
Orchestrate transaction flows using validations, approvals, and external triggers with logic and plugin coordination.
Validate transactions in real time, apply rules and spending limits, and return ALLOW, DENY, or REVIEW decisions.
Connect to external databases and extract data on schedule, feeding Reporter, Matcher, and your own services. Source-available, like Midaz.
Run the credit journey end to end — loan products, origination, servicing, accounting, and Brazil-first regulatory support.
Consume platform events and fan them out to your systems through signed webhooks, queues, or a pull API — the platform's event delivery edge.
Use shared services for access, lifecycle management, observability, and deployment across the products you adopt.
Reach Brazil's payment and regulatory rails through partner integrations and Lerian native messaging.
# Building a complete core banking
Source: https://docs.lerian.studio/en/building-a-complete-core-banking
How the Lerian pieces come together into one working core banking — what connects to what, and how much you wire yourself versus what comes pre-configured.
In [Fundamentals](/en/fundamentals/core-banking/what-is-core-banking) you learned the concepts. This page is how the Lerian pieces come together into one working core banking.
A complete Lerian core banking is a **deliberate composition**: Midaz, plus the products and plugins you choose. Products can be adopted on their own — that's a feature, not a missing integration. CRM and Fees are embedded in Midaz; other products and plugins deploy separately. By design, separately deployed pieces aren't pre-wired to each other, so connecting them is a setup step.
How much of this you do depends on **who runs your infrastructure**:
* **Managed** — Lerian runs your infrastructure and wires the pieces at deploy time. You open the Console, enable the products, and use them — no environment variables, no integration calls. (You won't need the rest of this page.)
* **Self-hosted / BYOC** — you run the infrastructure, so you do the wiring: configure each product's URLs and environment, and deploy it. The rest of this page walks that. Once it's connected, you can operate from the Console or call the APIs directly — that's a UX choice, not extra wiring.
**Enterprise** is a commercial tier, not a deployment model — Enterprise clients can be **managed or self-hosted**. See [Products, plugins & plans](/en/products-and-plugins) for what's included.
## Start with the ledger
***
Midaz is the ledger foundation of a Lerian core banking. Stand it up first when the capabilities you adopt record to Midaz.
**CRM is already part of Midaz.** The holders and aliases you learned about are built in, so there's nothing separate to wire — you just enable it.
→ [Set up Midaz](/en/midaz/midaz-setup) · [CRM in Midaz](/en/midaz/crm/crm-overview)
## Connect the pieces to the ledger
***
Most other pieces are **standalone services** (Fees Engine is the exception; it runs inside the ledger process), and they don't all connect the same way. Here's who calls whom at runtime — that difference is the practical part:
* **Payment plugins write to the ledger for you.** Your application calls a **payment plugin** (TED, Pix); the plugin moves the money and records the operations in Midaz — even from its own background workers — so your app doesn't touch the ledger for those. Its connection to Midaz is set at deploy time, invisible to your app. → [TED setup](/en/rails/ted/jd/ted-configuration)
* **The Fees Engine runs inside the ledger.** The **Fees Engine** is a licensed capability that is part of the Midaz ledger process. When a configured fee package applies, Midaz incorporates the fee calculations into the transaction you submit; no separate fee service sits between your app and the ledger. Your application can still call the fees endpoints on the same process to estimate fees before it submits. (For any movement you record without a plugin, your application writes to Midaz's API directly.) → [Connect the Fees Engine to Midaz](/en/midaz/fees/connecting-fees-to-midaz)
* **Reporter reads configured data sources.** It reads those sources (read-only, one direction) to build statements and reports — it never writes back. In a managed setup, Lerian configures the agreed data sources during onboarding. In self-hosted or BYOC deployments, configure a data source before you build reports that use it. → [Reporter](/en/reporter/reporter-quick-start)
* **Tracer can connect to Midaz when configured.** When `TRACER_BASE_URL` is set, Midaz initializes a reservation client (gRPC by default; REST with `TRACER_TRANSPORT=rest`). It can call reserve, confirm, and release only when `tracer.mode` is configured and not `off`. A Tracer skip is honored only when the request asks for it and the ledger enables `allowTracerSkip`; otherwise the request is rejected. When the base URL is unset, the integration is disabled. Your application can still integrate with Tracer independently. → [Tracer integration guide](/en/tracer/integration-guide)
Tracer checks your configured **policies and limits** — not account balances. The ledger stays the source of truth for what an account actually holds.
The exact URLs, credentials, and environment settings live in each product's own setup docs (linked above).
## One login across the pieces
***
So the pieces work as one product (and not as separate logins), they share authentication through **Access Manager**: it issues the tokens each service trusts. Each piece authenticates with credentials managed there — keep the details in one place and point the pieces at it.
→ [Access Manager](/en/platform/access-manager/access-manager)
## Deploy the stack
***
There's no single "install everything" bundle — deploy each selected component with its supported distribution artifact and bring them up **in order**:
**Midaz → Access Manager → products and plugins → Console**
The Console comes last because it needs the others already running.
→ [Helm charts](/en/platform/helm/helm-overview)
## Operate it from the Console
***
Once everything is running, the **Console** is your single pane of glass: enable the pieces and manage them with one login. Enabling a module in the Console turns it on in the interface and shares the login — the piece-to-ledger wiring is the deployment step above, not something the Console does for you.
→ [Lerian Console](/en/platform/console/about-lerian-console)
## On a managed setup
***
If Lerian runs your infrastructure, all the wiring above is **already done for you**. From your side it's simple: **enable the modules in the Console and start using them** — no environment variables, no deployment. The integration journey on this page is what Lerian handles underneath.
## Going further (optional)
***
A complete Lerian core banking is assembled from modular building blocks. Add **[Matcher](/en/matcher/what-is-matcher)** (reconciliation) and **[Flowker](/en/flowker/what-is-flowker)** (workflow automation) when their capabilities fit your operation. Both deploy as pieces of the same platform and share the Access Manager sign-in; connecting them follows the same deployment step described above.
# Core Banking vision
Source: https://docs.lerian.studio/en/core-banking-vision
Understand Lerian's vision for modern core banking, structured around four pillars — Ledger, transactional services, governance, and connectivity.
Core banking should not be a rigid service. It should be a programmable platform.
Lerian’s vision is structured around four foundational pillars that work together to provide control, scalability, and long-term architectural flexibility.
## The four pillars of modern Core Banking
***
### 1. Ledger
The ledger is the foundation of any financial system. It manages accounts, balances, and transactions with precision and consistency.
Powered by Midaz, our ledger is designed for high throughput, reliability, and operational clarity.
**Key capabilities:**
* Real-time transaction processing
* Multi-currency support
* Immutable audit trails
* High-performance architecture that scales as transaction volume grows
### 2. Transactional services
Transactional services orchestrate how money moves.
From instant payments to interbank transfers and card settlement, this layer ensures consistent, secure debit and credit execution across channels and networks.
**Transaction types documented today:**
* Instant payments — [Pix](/en/rails/pix/pix-overview), including scheduled (Pix Agendado) and agreement-based automatic debits (Pix Automático)
* Interbank wire transfers — [TED](/en/rails/ted/ted-overview), settled through BACEN's STR
* Boletos and bill payments — [issuing and paying](/en/rails/boleto/boleto-and-bill-payment) boletos, DARF taxes, and utility invoices
* Card-settlement messaging — [Lerian SLC](/en/rails/native/slc/what-is-lerian-slc) operates Nuclea's ASLC messaging, while [Lerian SILOC](/en/rails/native/siloc/what-is-lerian-siloc) provides the participant-side gateway and card-domain relay
### 3. Governance
Governance ensures that growth does not compromise control.
This layer integrates compliance, risk management, accounting, and reporting into the operational flow, not as afterthoughts, but as structural components.
**Governance capabilities:**
* KYC/AML integrations through Flowker and third-party providers
* Fraud detection and prevention
* Regulatory reporting
* Accounting integration
* Audit and compliance tooling
### 4. Connectivity infrastructure
No core system operates in isolation.
Our connectivity infrastructure supports integration with external networks, partners, and internal systems.
**Integration capabilities:**
* Built for integration — every capability is accessible through standard interfaces
* Runs on modern cloud infrastructure with automated scaling and recovery
* Supports multiple communication protocols
* Real-time data synchronization
* Secure communication channels
## Why this matters
***
Traditional core banking platforms are monolithic and difficult to evolve.
Lerian’s modular architecture allows institutions to:
* Adopt components incrementally
* Integrate with existing systems
* Scale based on operational demand
* Avoid vendor lock-in
You stay in control of your architecture, and your roadmap.
# Lerian Cloud (SaaS) and BYOC
Source: https://docs.lerian.studio/en/deployment-models
Understand how Lerian can be deployed — in Lerian Cloud (SaaS) or through BYOC in your own infrastructure — and choose the model that fits your compliance and scale needs.
Lerian supports two deployment models for its complete, modular platform: **Lerian Cloud (SaaS)** and **BYOC** (Bring Your Own Cloud). The deployment model defines who operates the infrastructure; your license and commercial agreement define which modules are available.
Choose the model that fits your operational maturity, compliance requirements, and team structure. Availability depends on the permitted license and commercial arrangement.
## Lerian Cloud (SaaS) — Fully managed by Lerian
***
In Lerian Cloud, Lerian hosts and operates the platform for you. Your team focuses on building financial products. We handle everything underneath.
**What Lerian manages:**
* Cloud infrastructure (compute, storage, networking)
* Database provisioning, backups, and scaling
* Security patches and OS-level updates
* Application upgrades and version management
* Monitoring, alerting, and incident response
* High availability and disaster recovery
**What you manage:**
* Business configuration (organizations, ledgers, accounts, rules)
* API integrations with your systems
* User access and permissions within the platform
* Compliance obligations specific to your institution
Lerian Cloud is ideal if you want the fastest path to production without building or maintaining infrastructure. You get enterprise-grade operations from day one.
## BYOC — Bring Your Own Cloud
***
In the BYOC model, you deploy and operate Lerian in your own infrastructure — whether that's a public cloud (AWS, GCP, Azure), a private cloud, or on-premises.
You have full control over the environment. Lerian provides the software, Helm Charts, Terraform templates, and tooling to make deployment and lifecycle management straightforward.
**What Lerian provides:**
* Application software (container images, Helm Charts)
* [Terraform templates](/en/midaz/midaz-terraform-foundation) for infrastructure provisioning
* [Lifecycle Management](/en/platform/lifecycle-management) for deployment orchestration and version tracking
* Security patches and application updates (delivered by Lerian, applied by you)
* Technical support (Enterprise)
**What you manage:**
* Cloud infrastructure and Kubernetes clusters
* Database administration, backups, and scaling
* Network security (firewall rules, network isolation, and traffic routing)
* OS and container-level patching
* Monitoring and observability pipelines
* Encryption, secrets management, and access control
BYOC is ideal if your organization requires full data sovereignty, has strict regulatory constraints on where data lives, or has an established platform engineering team.
### Why Helm for BYOC deployments
Lerian publishes official Helm charts for every product. Helm is the recommended way to deploy, configure, and manage the platform in your infrastructure. Here's why it matters:
* **Reproducibility** — Every deployment from the same chart version produces the same result. No "it works on my cluster" surprises across staging, production, or disaster recovery environments.
* **Rollback** — Helm keeps a history of every release. Rolling back to a previous version is a single command — no need to reconstruct the previous state manually.
* **Reduced operational risk** — Charts encode configuration details (environment variables, ports, dependencies), so your team does not have to remember them. Review your values and rendered manifests; Helm does not eliminate operational risk.
* **Faster time to production** — Instead of writing Kubernetes manifests from scratch, start with Lerian's official charts and adjust a single values file. Days of YAML editing become an afternoon of parameter tuning.
* **GitOps-ready** — Charts integrate naturally with GitOps workflows (ArgoCD, Flux). Deployment configuration lives in version control, changes go through pull requests, and every deployment is traceable to a specific commit.
| Product | What it deploys | Helm documentation |
| :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- |
| **Midaz** | Core ledger engine (unified Ledger service — onboarding, transactions, and Fees) with an embedded CRM capability that you enable when needed | [Midaz Helm guide](/en/platform/helm/midaz/midaz-helm-overview) |
| **Plugins** | Pix, Pix Indirect, Bank Transfer | [Plugins Helm guide](/en/platform/helm/plugins/plugins-helm-overview) |
| **Reporter** | Report generation service | [Reporter Helm guide](/en/platform/helm/reporter/reporter-helm) |
| **Access Manager** | Authentication and identity | [Access Manager Helm guide](/en/platform/helm/access-manager/access-manager-helm) |
| **Console** | Web UI for managing the platform | [Console Helm guide](/en/platform/helm/console/console-helm) |
| **Infrastructure** | OTEL Collector, Fetcher, Flowker, Tracer | [Infrastructure Helm guide](/en/platform/helm/infrastructure/infrastructure-helm) |
For the full version compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility). For step-by-step installation instructions, start with the [Helm overview](/en/platform/helm/helm-overview) in the Platform section.
### BYOC variations
For products and Enterprise entitlements that support multi-tenancy, BYOC supports two configurations:
| Configuration | Description | Best for |
| :---------------- | :------------------------------------------------------- | :------------------------------------------------------------ |
| **Single-Tenant** | One dedicated environment per institution | Large institutions with strict isolation requirements |
| **Multi-Tenant** | One environment serving multiple subsidiaries or clients | Institutions operating as a platform or managing subsidiaries |
Lerian Cloud is operated as a multi-tenant environment managed by Lerian. If you need dedicated, isolated infrastructure, choose BYOC Single-Tenant.
## How your deployment model affects Midaz API integration
***
The Midaz API surface is identical across deployment models — same endpoints, same payloads, same responses. The difference is in how authentication and tenant scoping work.
**Lerian Cloud and BYOC Multi-Tenant (Midaz)**
Your tenant context is established automatically through your authentication token. There is no tenant header to pass and no tenant ID to manage manually.
* You authenticate via [Access Manager](/en/platform/access-manager/access-manager) and receive a JWT.
* That token includes your tenant context.
* Every API call is automatically scoped to your tenant — organizations, ledgers, accounts, and transactions are all isolated.
* You never interact with data from other tenants, and they never interact with yours.
**BYOC Single-Tenant (Midaz)**
Configure authentication with `PLUGIN_AUTH_ENABLED`. It defaults to `false` for local and development onboarding, but production deployments must set it to `true`; BYOC Multi-Tenant deployments also require it. In a permitted non-production single-tenant environment with authentication disabled, API calls do not require a token.
For a deeper look at how tenant isolation works, see [Multi-tenancy](/en/multi-tenancy).
## At a glance
***
| | **Lerian Cloud (SaaS)** | **BYOC** |
| :------------------------------ | :------------------------------------------------------ | :------------------------------------------------------------------ |
| **Who manages infrastructure?** | Lerian | Your team |
| **Where does data live?** | Lerian-managed cloud | Your cloud or on-premises |
| **Time to production** | Weeks | Depends on your provisioning timeline |
| **Operational effort** | Low — Lerian handles operations | Higher — your team operates the platform |
| **Best for** | Teams that want to focus on product, not infrastructure | Organizations with strict data residency or compliance requirements |
Not sure which model fits? Jump to ["Which model is right for you?"](#which-model-is-right-for-you) below.
## Comparing the models
***
| | **Lerian Cloud (SaaS)** | **BYOC** |
| :-------------------------- | :------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- |
| **Infrastructure** | Managed by Lerian | Managed by you |
| **Data location** | Lerian-managed cloud | Your cloud or on-premises |
| **Data sovereignty** | Data hosted in Lerian-managed cloud infrastructure | Data is hosted in your environment; you control residency and egress according to your architecture and contracted services. |
| **Security responsibility** | Shared (Lerian manages infra + app) | Shared (you manage infra, Lerian manages app) |
| **Time to production** | Weeks — no infrastructure setup required | Depends on your team's provisioning timeline |
| **Operational overhead** | Low — Lerian handles operations | Higher — your team operates the platform |
| **Customization** | Standard configuration | Full control over infrastructure and networking |
| **Compliance** | Lerian's environment meets security standards | You ensure your environment meets your regulatory requirements |
| **Updates** | Applied by Lerian | Applied by you via Lifecycle Management or Helm |
| **Scalability** | Managed by Lerian | Managed by you |
## Which model is right for you?
***
**Choose Lerian Cloud if:**
* You want to go live quickly without provisioning infrastructure
* Your team prefers to focus on product and integration, not operations
* You don't have strict requirements for data to remain in your own environment
**Choose BYOC if:**
* Regulatory or compliance rules require data to stay in your infrastructure
* You need full control over networking, encryption, and access policies
* Your organization has a platform or DevOps team ready to operate Kubernetes workloads
* You want to integrate Lerian into an existing cloud environment with specific architectural constraints
## Deployment and licensing
***
Deployment and licensing are related dimensions, but not every pairing is available. Lerian Cloud is Enterprise-only; Community supports self-managed Midaz BYOC. See [Community and Enterprise](/en/midaz/midaz-models).
| | **Community** | **Enterprise** |
| :---------------------- | :-------------------------------------- | :------------------------------------------------ |
| **Lerian Cloud (SaaS)** | Not available | Available |
| **BYOC** | Available for Midaz only (self-managed) | Available (with Lifecycle Management and support) |
Lerian Cloud requires Lerian to manage infrastructure, operations, and lifecycle on your behalf — capabilities that are part of the Enterprise offering. That's why it is not available under the Community model.
* **Community + BYOC**: You deploy and manage the source-available, ELv2-licensed Midaz software using the published Midaz Helm charts and documentation. Community support via GitHub and Discord. Other Lerian products are available to licensed customers under the Enterprise model.
* **Enterprise + BYOC**: Full BYOC with Lifecycle Management, dedicated support, SLA, onboarding, and access to plugins.
* **Enterprise + Lerian Cloud**: Lerian manages everything. You get the licensed platform modules without infrastructure responsibilities.
## What's next
***
Understand the licensing differences and what's included in each model.
Follow the step-by-step guide to set up your first Ledger and run transactions.
Learn how the shared responsibility model works for each deployment option.
Explore Kubernetes, Helm, and Terraform options for BYOC deployments.
# The accounting cycle
Source: https://docs.lerian.studio/en/fundamentals/accounting/accounting-cycle
How transactions, journal entries, the ledger, trial balance, and financial statements fit together as one repeating cycle that keeps the books trustworthy.
You've met the pieces one at a time: [debits and credits](/en/fundamentals/accounting/debits-and-credits), [journal entries](/en/fundamentals/accounting/journal-entries), the [chart of accounts](/en/fundamentals/accounting/chart-of-accounts), the [financial statements](/en/fundamentals/accounting/financial-statements). This page is the one that puts them in a line. The **accounting cycle** is the repeating routine that takes a single thing that happened in the real world and turns it into a report you can trust — and then starts over.
## One event, all the way through
***
Every figure on a financial statement started life as something tiny and concrete: a sale, a payment, a bill. The cycle is just the path that event travels, step by step, until it shows up in a report.
* **Transaction** — something happens. A customer pays, you cover the rent, a bill arrives.
* **Journal entry** — you write it down as matching debits and credits, so both sides of the movement are recorded.
* **Ledger** — each entry is sorted into the right account, so every account gathers its own running history.
* **Trial balance** — you add up all the accounts and check that total debits still equal total credits.
* **Financial statements** — the balanced totals are arranged into the Balance Sheet, Income Statement, and Cash Flow Statement.
Then the cycle repeats — for the next transaction, the next day, the next period. It never really stops; it just keeps turning.
```mermaid theme={null}
flowchart LR
T["Transaction something happens"] --> J["Journal entry debits = credits"]
J --> L["Ledger sorted by account"]
L --> TB["Trial balance check it still balances"]
TB --> FS["Financial statements the reports"]
FS -.->|"next transaction · next period"| T
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class TB,FS bal;
```
## Walking one transaction around the loop
***
Say a customer pays you \$2,000 for a sale you make right then — so the revenue is earned at that moment. Follow it through:
1. **Transaction.** The sale happens and the customer pays — money arrives.
2. **Journal entry.** You record it: **Cash** debited \$2,000, **Revenue** credited \$2,000. Debits equal credits, so the entry balances.
3. **Ledger.** That \$2,000 debit joins the running history of the Cash account; the \$2,000 credit joins the Revenue account. Do this for every entry and each account always knows its own total.
4. **Trial balance.** Add every account's debits and credits across the whole book. If the two grand totals match, the records are internally consistent — a quick health check before you report anything.
5. **Financial statements.** The Cash balance feeds the Balance Sheet; the Revenue feeds the Income Statement. The same \$2,000, now part of the big picture.
Nothing new is invented along the way. Each step just **reorganizes** the same balanced movement into a more useful shape — from a single event, to a record, to a sorted history, to a verified total, to a readable report.
## Why the order matters
***
Each step depends on the one before it, which is exactly why the sequence is fixed:
* You can't sort an entry into the **ledger** until you've written the **journal entry**.
* The **trial balance** is only meaningful once every entry has landed in the ledger.
* The **financial statements** are only trustworthy once the trial balance confirms debits equal credits.
That dependency is the cycle's quiet safeguard. The balancing rule from [double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) rides along at every stage, so a mistake shows up early — at the trial balance — instead of hiding inside a finished report.
**See also in Core Banking**
See how this whole flow lands in a real ledger system in [Designing your ledger plan](/en/fundamentals/core-banking/designing-your-ledger-plan) and [Where to go next](/en/fundamentals/core-banking/where-to-go-next).
## In short
***
* The **accounting cycle** is the repeating path from a real event to a finished report: **transaction → journal entry → ledger → trial balance → financial statements**.
* Each step just **reorganizes** the same balanced movement into a more useful shape — nothing is invented or lost along the way.
* The steps run in a **fixed order** because each depends on the last, and the **debits-equal-credits** rule is checked at the trial balance before anything is reported.
* Then it **repeats**, transaction after transaction, period after period.
**Next up**
You've seen how these ideas connect in the abstract. Now see how they map onto a real ledger system in [Accounting in Lerian](/en/fundamentals/accounting/accounting-in-lerian).
# Accounting in Lerian
Source: https://docs.lerian.studio/en/fundamentals/accounting/accounting-in-lerian
How core accounting ideas map onto real Lerian and Midaz entities — accounts, transactions, operations, and routes — so the concepts and the product line up.
Everything in this guide has been about accounting in general. This page connects it to the system you're actually here for. The good news: **Midaz doesn't reinvent accounting — it implements it.** The terms you've already learned have direct counterparts in the product, and once you see the mapping, the documentation reads like a language you already speak.
## The same ideas, renamed for a ledger
***
From here on, the words get more product-specific — but don't let that throw you. The accounting ideas stay exactly the same; only the names change. Here's the whole bridge in one table: each accounting idea on the left is something concrete in Midaz on the right.
| Accounting idea | In Lerian / Midaz |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| [Chart of accounts](/en/fundamentals/accounting/chart-of-accounts) | The **account hierarchy** inside a Ledger |
| The five account families (assets, liabilities, equity, revenue, expense) | How you **structure and group** accounts in that hierarchy |
| [Journal entry](/en/fundamentals/accounting/journal-entries) | A **transaction** made of **operations** |
| A single debit or credit line | One **operation** |
| [Double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) — debits must equal credits | A transaction's operations **always balance** |
| Which accounts a kind of entry may touch | An **accounting route** |
```mermaid theme={null}
flowchart LR
subgraph ACC["Accounting"]
direction TB
CA["Chart of Accounts"]:::a
JE["Journal Entry"]:::a
DC["Debit / Credit rule"]:::a
end
subgraph MID["Lerian / Midaz"]
direction TB
AH["Account Hierarchy"]:::m
TO["Transaction + Operations"]:::m
AB["Always Balanced"]:::m
end
CA --> AH
JE --> TO
DC --> AB
classDef a fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef m fill:#dcfce7,stroke:#16a34a,color:#14532d
```
The rest of this page walks through the rows that matter most.
## Chart of accounts → the account hierarchy
***
A [chart of accounts](/en/fundamentals/accounting/chart-of-accounts) is the organized list of buckets a business sorts its money into. In Midaz, an [account](/en/fundamentals/core-banking/building-blocks) is tied to one asset code and can have one or more keyed [balance](/en/fundamentals/core-banking/building-blocks) records. Accounts aren't a flat list: they're arranged in a **hierarchy**, so you can nest and group them to mirror how a real business is structured. The five accounting families — assets, liabilities, equity, revenue, expenses — are exactly the kind of grouping that hierarchy is built to express.
A [Ledger](/en/fundamentals/core-banking/building-blocks) is one self-contained book of records, and an Organization can hold several. So your chart of accounts lives as the structured set of accounts inside a Ledger — the same map of "where value lives," given room to scale.
## Journal entry → transaction + operations
***
A [journal entry](/en/fundamentals/accounting/journal-entries) records one event as matching debits and credits. Midaz splits that single idea into two precise words you'll see everywhere in [how money moves](/en/fundamentals/core-banking/how-money-moves):
* A **transaction** is one complete movement of value — the whole event, start to finish. That's the journal entry.
* An **operation** is one part of that movement — the smallest Midaz record of a balance effect. Non-pending transfers can produce debit and credit operations; pending or canceled flows can also include `ON_HOLD` or `RELEASE`.
So when you recorded "Cash debited \$2,000, Revenue credited \$2,000" as a journal entry, the Midaz version is **one transaction made of two operations**. Same event, same two sides, just the product's vocabulary for them.
```mermaid theme={null}
flowchart TD
A["Journal entry accounting"]:::acc --> B["Transaction the whole movement"]:::mid
B --> C["Operation debit"]:::mid
B --> D["Operation credit"]:::mid
C --> E["Debits total"]:::tot
D --> F["Credits total"]:::tot
E --> G["Transaction balances"]:::bal
F --> G
classDef acc fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mid fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef tot fill:#f3f4f6,stroke:#6b7280,color:#111827
classDef bal fill:#fde68a,stroke:#d97706,color:#78350f
```
## Double-entry → transactions always balance
***
The iron rule of accounting — [debits must equal credits](/en/fundamentals/accounting/debits-and-credits) — is built into Midaz processing. Midaz validates resolved source, destination, and transaction totals before processing; if they differ, the movement will not go through. Its source separates atomic hot-balance mutation from append-only transaction and operation persistence, so this page does not make an all-or-nothing persistence promise.
This is the same trustworthiness promise the whole guide keeps coming back to — every accepted movement is validated with a clear source and a clear destination — now enforced by the system instead of by hand. Because balance mutation and history persistence are separate, reconciliation and recovery controls are what surface any persistence gap between them. The [core-banking explanation of how money is recorded](/en/fundamentals/core-banking/double-entry-explained) covers this from the ledger's side.
## Accounting routes → which accounts may take part
***
In plain accounting you decide by judgment which accounts a given kind of entry should touch; in Midaz that judgment is captured once as an [accounting route](/en/fundamentals/core-banking/routes-and-fees) — a reusable rule that fixes who can send, who can receive, and which debits and credits get recorded — and a **fee** is just more balanced operations on the same transaction.
For example, a wallet transfer's route fixes which customer account may be **debited**, which customer account may be **credited**, and which **fee accounts** join in when a fee applies. The [core-banking guide to routes and fees](/en/fundamentals/core-banking/routes-and-fees) walks through it in full.
## A note on holders and accounts
***
In Midaz the [account](/en/fundamentals/core-banking/holders-accounts-aliases) is just the balance container, while the real-world owner is a separate **holder** record and friendly nicknames are **aliases** — kept deliberately separate so the Ledger's record of value stays clean. If that distinction matters for your work, [who owns what](/en/fundamentals/core-banking/holders-accounts-aliases) untangles it.
## In short
***
* Midaz **implements** the accounting you've learned — it doesn't replace it.
* A **chart of accounts** becomes the **account hierarchy** inside a Ledger; the five account families are how you group it.
* A **journal entry** becomes a **transaction** (the whole movement) made of **operations** (its individual debits and credits), and those operations **always balance**.
* An **accounting route** captures, once, which accounts may take part in a kind of movement — and a **fee** is just more balanced operations on the same transaction.
**Go deeper**
To see these entities in full, continue into the [Core banking fundamentals](/en/fundamentals/core-banking/what-is-core-banking), or revisit any idea from the [Accounting overview](/en/fundamentals/accounting/what-is-accounting).
# Accrual vs. cash accounting
Source: https://docs.lerian.studio/en/fundamentals/accounting/accrual-vs-cash
Why revenue isn't always money in the bank and profit isn't the same as cash — the difference between accrual and cash accounting, explained in plain language.
Here's a question that quietly confuses almost every beginner: **when does a sale count?** The moment you agree to it and send the bill — or the moment the money actually lands in your account? Your answer decides whether you're doing **accrual** or **cash** accounting, and it changes how every report reads.
## Two moments, not one
***
Every sale really has *two* moments:
```mermaid theme={null}
flowchart LR
M1["📋 Moment 1 Work done · Invoice sent March"]:::moment
M2["💵 Moment 2 Cash received May"]:::moment
M1 -->|"time passes"| M2
ACC(["Accrual records the sale here"]):::accrual
CASH(["Cash records the sale here"]):::cash
ACC -.-> M1
CASH -.-> M2
classDef moment fill:#eef2ff,stroke:#4f46e5,color:#312e81
classDef accrual fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef cash fill:#fef3c7,stroke:#d97706,color:#78350f
```
* The moment you **earn** it — you deliver the goods, finish the work, send the invoice.
* The moment you **get paid** — the cash actually arrives.
Sometimes these happen together (you buy coffee, you pay at the counter). Often they don't — you finish a job in March but the customer pays in May. The two methods simply disagree about which moment to record.
## Cash accounting — record when money moves
***
**Cash accounting** is the simple one: you record a transaction **only when the cash actually changes hands**. Money in the account? Write it down. Money out? Write it down. No money moved? Nothing to record yet.
It works like your personal bank statement. You don't note that a friend *promised* to pay you back — you note it when they actually do. Small businesses like it because it's easy and it mirrors the bank balance you can see.
The catch: it can mislead you about timing. Do \$50,000 of work in December but get paid in January, and cash accounting says December earned **nothing**. That's tidy for taxes but a poor picture of what the business actually did.
## Accrual accounting — record when it's earned
***
**Accrual accounting** records a transaction **when it's earned or incurred — not when the cash moves.** Finish the work and send the invoice? That's revenue *now*, even if payment is weeks away. Receive a bill for electricity you've already used? That's an expense *now*, even if you pay it later.
It captures *promises*, not just cash:
* Money customers owe you (but haven't paid) is recorded as revenue and as an asset — **accounts receivable**.
* Bills you owe (but haven't paid) are recorded as expenses and as a liability — **accounts payable**.
This gives a truer picture of how the business is *really* doing in a period, because it matches the income to the work that earned it. That's why larger businesses — and the rules they answer to — use it.
| | **Cash accounting** | **Accrual accounting** |
| ------------------------- | ---------------------------- | --------------------------------- |
| **Records revenue when** | Cash arrives | Work is done / invoice sent |
| **Records expenses when** | Cash is paid | Bill is incurred |
| **Shows you** | What's in the bank | What you've really earned and owe |
| **Best for** | Simplicity, small operations | An accurate picture over time |
Here's that same job made concrete — **\$50,000 of work done in December, paid in January** — booked under each method:
| The \$50,000 job | December *(work done)* | January *(customer pays)* |
| -------------------------------------- | ---------------------- | ------------------------- |
| **Cash accounting** records revenue | \$0 | \$50,000 |
| **Accrual accounting** records revenue | \$50,000 | — *(cash just arrives)* |
Same sale, same \$50,000 — but the methods book it in different months. Accrual ties the revenue to the **work** in December; cash waits for the **money** in January.
## Why revenue isn't "money in the bank"
***
This is the part that catches people out. Under accrual accounting, **revenue means money earned — not money received.** You can book \$10,000 of revenue this month and have **none of it** in your account yet, because the customers haven't paid.
So when you read an [Income Statement](/en/fundamentals/accounting/financial-statements) and see revenue, don't picture a full bank account. Picture *work that has been done and billed*. The cash may already be here, may be arriving next month, or may never arrive at all if someone fails to pay.
## Why profit isn't cash
***
The same gap explains accounting's most famous head-scratcher: **a profitable business can still run out of cash.**
Profit is revenue minus expenses — and under accrual, both can include things where no cash has moved yet. So your Income Statement can show a healthy profit while your bank account runs dry, simply because customers are slow to pay. The reverse happens too: you can have plenty of cash (a big upfront payment) yet little profit.
That's exactly why there are two different reports. The Income Statement shows **profit**; the [Cash Flow Statement](/en/fundamentals/accounting/financial-statements) shows the **actual cash**. As the saying goes: *profit is an opinion, cash is a fact.* You need both to understand a business.
## In short
***
* **Cash accounting** records money only when it changes hands; **accrual accounting** records it when it's earned or incurred, even before the cash moves.
* Under accrual, **revenue means "earned," not "in the bank"** — so an Income Statement reflects work done and billed, not your account balance.
* Because of that gap, **profit and cash are not the same thing** — a profitable business can still be short of cash, which is why the profit and cash-flow reports are kept separate.
**Next up**
You now know every piece. See how they run together, period after period, in [The accounting cycle](/en/fundamentals/accounting/accounting-cycle).
# Assets, liabilities & equity
Source: https://docs.lerian.studio/en/fundamentals/accounting/assets-liabilities-equity
The accounting equation explained with everyday analogies — what you own, what you owe, what's yours, and why Assets = Liabilities + Equity always balances.
Every business — and honestly, every person — can describe its financial life with three words: **assets**, **liabilities**, and **equity**. Get these three, and the single most important idea in accounting falls into place.
Let's use a house.
## The three words, in plain terms
***
* **Assets** — everything you **own** that has value. Your house, your car, the cash in your account, the money customers still owe you.
* **Liabilities** — everything you **owe** to someone else. Your mortgage, your car loan, an unpaid bill.
* **Equity** — what's **actually yours** once the debts are paid off. It's the leftover.
Say your house is worth \$300,000 (an asset). You still owe \$200,000 on the mortgage (a liability). The part that's truly yours — your **equity** — is \$100,000.
That's the whole idea — **what you own, minus what you owe, is what's yours:**
$$
Assets - Liabilities = Equity
$$
| Term | Plain meaning | House example |
| ------------- | ------------------------ | -------------------------- |
| **Asset** | What you own | The \$300,000 house |
| **Liability** | What you owe | The \$200,000 mortgage |
| **Equity** | What's left over for you | The \$100,000 that's yours |
## The equation that ties them together
***
You just saw the equation one way. Move `Liabilities` to the other side and you get the **same idea** flipped around — the form accountants actually use, called the **accounting equation**:
$$
\underset{\$300{,}000\ \text{house}}{\text{Assets}} \;=\; \underset{\$200{,}000\ \text{mortgage}}{\text{Liabilities}} \;+\; \underset{\$100{,}000\ \text{yours}}{\text{Equity}}
$$
It's the same sentence written two ways. The first asks *what's truly mine?* The second flips it to show *how everything I own was funded* — nothing changes but the order.
Everything the business **owns** was paid for in one of two ways: with **money it borrowed** (liabilities) or with **money that's its own** (equity). So the total value of what you own always equals the sum of those two sources.
## Why it always balances
***
Here's the part people miss: the equation can **never** be out of balance, by design. If something changes on one side, something else has to change to keep it even.
* **You buy a \$20,000 car with a loan.** Assets go up by \$20,000 (the car). Liabilities go up by \$20,000 (the loan). Still balanced.
* **You pay off \$5,000 of that loan with cash.** Assets drop by \$5,000 (cash leaves). Liabilities drop by \$5,000 (debt shrinks). Still balanced.
* **The business earns \$10,000 in profit.** Assets go up by \$10,000 (cash). Equity goes up by \$10,000 (it's yours to keep). Still balanced.
Every real-world event touches the equation in a way that keeps both sides equal. That's not a coincidence — it's the rule that makes accounting trustworthy. If your books *don't* balance, you know immediately that something was recorded wrong.
## In short
***
* **Assets** are what you own, **liabilities** are what you owe, and **equity** is what's left over for you.
* The **accounting equation** — *Assets = Liabilities + Equity* — just says everything you own was funded either by debt or by your own stake.
* It **always balances**: every change on one side forces a matching change elsewhere. A broken balance is a sign of an error.
**Next up**
You've got what a business owns, owes, and keeps. The other half of the picture is what it earns and spends — meet [Revenue, expenses & costs](/en/fundamentals/accounting/revenue-and-expenses), and how profit feeds back into equity.
# Chart of accounts
Source: https://docs.lerian.studio/en/fundamentals/accounting/chart-of-accounts
What a chart of accounts is, why it keeps financial data organized, how it ties accounting together, and how it maps to the account hierarchy in Midaz.
If accounting is a giant filing cabinet of money movements, the **chart of accounts** is the labels on the drawers. It's the organized list of every "bucket" a business uses to sort its transactions — and without it, the records would be an unsearchable pile.
## What it actually is
***
A **chart of accounts** is simply a structured list of all the accounts a business uses to record its financial activity. Each account is a category — a place to put a certain kind of transaction.
Think of how you might organize your own money into labeled envelopes: *Rent*, *Groceries*, *Salary*, *Savings*. A chart of accounts is the same idea, just more complete and consistent. Every transaction gets filed into one of these named buckets, so you always know what it was for.
## How it's organized
***
The accounts are grouped to match the concepts you've already met. Most charts follow the same five families:
| Group | What goes here | Examples |
| --------------- | -------------------------- | ---------------------------------- |
| **Assets** | What the business owns | Cash, equipment, money owed to you |
| **Liabilities** | What the business owes | Loans, unpaid bills |
| **Equity** | What's left for the owners | Owner's stake, retained profit |
| **Revenue** | Money earned | Sales, service income |
| **Expenses** | Money spent | Rent, salaries, supplies |
The first three feed the [Balance Sheet](/en/fundamentals/accounting/financial-statements); the last two feed the Income Statement. So the chart of accounts is the bridge between raw transactions and the reports everyone reads.
In practice, a simple one looks like this — each family gets its own range of numbers, with individual accounts slotted inside:
```
1000 Assets
1010 Cash
1020 Accounts receivable
1030 Equipment
2000 Liabilities
2010 Loans payable
2020 Taxes payable
3000 Equity
3010 Owner's equity
3020 Retained earnings
4000 Revenue
4010 Product sales
4020 Service income
5000 Expenses
5010 Rent
5020 Salaries
5030 Supplies
```
The numbers aren't decoration — they keep accounts in a fixed order and leave room to add new ones (a `1040` for inventory, say) without disturbing the rest.
## Why it matters
***
A good chart of accounts is what makes financial data **usable**:
* **Consistency** — the same kind of expense always lands in the same place, so reports are reliable.
* **Clarity** — anyone can see, at a glance, where money is going.
* **Scale** — whether you have ten transactions or ten million, they all have a clear home.
Without it, [double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) entries would have nowhere consistent to land, and the [financial statements](/en/fundamentals/accounting/financial-statements) couldn't be assembled. It's the quiet structure that makes everything else work.
## How it connects in Lerian
***
In accounting, the chart of accounts is the structure that organizes your records. In a **ledger system**, that same idea becomes an **account hierarchy** — a tree that groups balances, movements, and the business entities behind them:
```mermaid theme={null}
flowchart TD
A["Chart of accounts"]:::root --> B["Assets"]:::family
A --> C["Liabilities"]:::family
A --> D["Equity"]:::family
A --> E["Revenue"]:::family
A --> F["Expenses"]:::family
B --> B1["Cash"]:::leaf
B --> B2["Accounts receivable"]:::leaf
B --> B3["Equipment"]:::leaf
F --> F1["Rent"]:::leaf
F --> F2["Salaries"]:::leaf
F --> F3["Supplies"]:::leaf
classDef root fill:#f3f4f6,stroke:#6b7280,color:#111827
classDef family fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef leaf fill:#eff6ff,stroke:#93c5fd,color:#1e3a8a
```
In **Midaz**, that hierarchy lives inside a Ledger, so you can group and nest accounts to mirror how a real business is structured. [Accounting in Lerian](/en/fundamentals/accounting/accounting-in-lerian) shows exactly how this — and the other accounting ideas — map onto the product.
**See also in Core Banking**
This same structure becomes an account hierarchy in a ledger — see [The building blocks](/en/fundamentals/core-banking/building-blocks) and [Designing your ledger plan](/en/fundamentals/core-banking/designing-your-ledger-plan).
## In short
***
* A **chart of accounts** is the organized list of buckets a business sorts its transactions into.
* It groups accounts into **assets, liabilities, equity, revenue, and expenses** — the bridge between raw entries and readable reports.
* It's what keeps financial data **consistent, clear, and scalable**, and in **Midaz** it becomes a flexible account **hierarchy** that mirrors how a real business is organized.
**Next up**
Once every transaction is filed into the right account, those totals roll up into reports. Meet them in [Financial statements](/en/fundamentals/accounting/financial-statements).
# Debits and credits
Source: https://docs.lerian.studio/en/fundamentals/accounting/debits-and-credits
What debit and credit really mean in a banking system — the two ends of a money movement, and why one balance is a wallet to you and a liability to the ledger.
Two words trip up almost everyone the first time they read a banking system: **debit** and **credit**. They sound like accounting jargon, but they name something simple — the two ends of a money movement. A debit is where money comes **from**; a credit is where it **goes**.
## Debit is out, credit is in
***
Every movement of money leaves one account and arrives in another. Those two ends have names:
* A **debit** is the account money moves **out of** — the **source**.
* A **credit** is the account money moves **into** — the **destination**.
When you send R\$100 to a merchant, your account is **debited** (money out) and the merchant's account is **credited** (money in). One movement, two sides.
```mermaid theme={null}
flowchart LR
S["Your account (source)"]:::debit
M(("Movement R$100")):::bal
D["Merchant account (destination)"]:::credit
S -->|"debit · R$100 out"| M
M -->|"credit · R$100 in"| D
classDef debit fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef credit fill:#fde68a,stroke:#d97706,color:#78350f
classDef bal fill:#f3f4f6,stroke:#6b7280,color:#111827
```
This is exactly how your bank statement already reads: money leaving your account shows as a debit, money arriving shows as a credit.
## Your wallet is not the whole story
***
Here's where the everyday intuition and the accounting meaning part ways — and it's the single most common source of confusion.
In your **wallet**, the math is obvious: money in is good, money out is less. When your bank says it **credited** your account, your balance went **up**; a **debit** took money away. So it's tempting to read *credit = add, debit = subtract*.
But that's your wallet's point of view. The **ledger** that holds your money sees the same balance another way. To the institution, your balance isn't something it owns — it's money it **owes you**. Whether a debit or credit grows or shrinks a balance depends on that balance's persisted direction, not account type alone.
So keep the two ideas apart:
* **Wallet thinking** asks: did *my* number go up or down?
* **Accounting thinking** asks: which account did this movement leave (**debit**), and which did it arrive in (**credit**)?
The accounting meaning is the reliable one, and it never changes: **a debit is the source, a credit is the destination.** Whether that raises or lowers a given balance is a *separate* question answered by that balance's persisted direction.
## Assets and liabilities: the two sides of the ledger
***
Each balance in a Ledger has a **direction**, and that direction — not an account category alone — decides whether a debit or credit changes its available amount.
In Midaz, a balance direction is `debit` or `credit`:
* A balance with direction **`debit`** increases its available amount on a debit and decreases it on a credit.
* A balance with direction **`credit`** increases its available amount on a credit and decreases it on a debit.
An account type can supply a default direction, but Midaz applies the direction persisted on the balance. For every **[asset](/en/midaz/assets)** you define — BRL, USD, a loyalty point — the Ledger automatically keeps an external account named after it (`@external/BRL`, `@external/USD`), and you can define your own named external accounts too.
Same R\$100, two truths: it's an **asset** in your wallet and a **liability** on the institution's Ledger. Both are right — they're just the two sides of the same movement. [The outside world](/en/fundamentals/core-banking/external-accounts-and-reconciliation) follows the asset side across the boundary.
## The iron rule: debits always equal credits
***
Here is what makes the whole system trustworthy: **total debits always equal total credits.** Every movement is recorded on both sides at once — the same amount out of the source and into the destination — so the two sides match exactly. If they don't, something is wrong, and the books say so.
Take that R\$100 payment. R\$100 leaves your account as a debit and R\$100 arrives in the merchant's account as a credit. One event, two entries, totals equal:
```mermaid theme={null}
flowchart LR
D["Your account DEBIT R$100"]:::debit
C["Merchant account CREDIT R$100"]:::credit
D --- BAL{{"Total debits R$100 = Total credits R$100"}}:::bal
C --- BAL
classDef debit fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef credit fill:#fde68a,stroke:#d97706,color:#78350f
classDef bal fill:#dcfce7,stroke:#16a34a,color:#14532d
```
The money didn't appear or vanish — it moved from one account to another, and the matching debit and credit prove it. This is the engine behind **double-entry bookkeeping**: every movement written down twice, once as it leaves and once as it arrives.
An **[operation](/en/midaz/operations)** is the smallest Midaz record of a balance effect. A non-pending transfer can produce a source debit and destination credit; pending or canceled flows can also include `ON_HOLD` or `RELEASE` operations.
**See also in Core Banking**
See what debit and credit mean from the ledger's side in [How money is recorded](/en/fundamentals/core-banking/double-entry-explained).
## In short
***
* A **debit** is the account money moves **out of** (the source); a **credit** is the account money moves **into** (the destination). That meaning never changes.
* Your **wallet** and the **ledger** read the same balance differently: to you it's your money; to the institution it's a **liability** — money it owes you.
* Which side a debit or a credit grows depends on the balance's persisted **direction**: `debit` increases on a debit; `credit` increases on a credit.
* **Total debits always equal total credits**, so money is never created or lost — it only moves.
**Next up**
Debits and credits only make sense as a pair. See how they work together in [Double-entry bookkeeping](/en/fundamentals/accounting/double-entry-bookkeeping).
# Double-entry bookkeeping
Source: https://docs.lerian.studio/en/fundamentals/accounting/double-entry-bookkeeping
Why every transaction touches two accounts so nothing gets lost or invented — the idea behind double-entry bookkeeping, with simple everyday examples.
**Double-entry bookkeeping** sounds technical, but the idea is something you already understand: **money never just vanishes or appears — it always moves from somewhere to somewhere else.** Double-entry is simply the habit of recording *both* ends of that movement, every single time.
## One event, two sides
***
Think about handing a friend \$10 for lunch. Two things happened at once: **your** wallet went down by \$10, and **their** wallet went up by \$10. One event, two effects.
Accounting insists you write down **both**. Every transaction touches **at least two accounts** — one giving, one receiving. That's why it's called *double*-entry. The **value** recorded on one side must be matched by equal value recorded on the other — it isn't always physical cash moving, but the two amounts always agree.
So a transaction is never a single line floating on its own. It's **one event with two connected effects**, and recording both is what keeps everything in balance:
```mermaid theme={null}
flowchart LR
A["One business event"]:::event --> B["Effect 1 one account changes"]:::effect
A --> C["Effect 2 another account changes"]:::effect
B --> D["Equal value on both sides"]:::equal
C --> D
D --> E["Books stay balanced"]:::balanced
classDef event fill:#f3f4f6,stroke:#6b7280,color:#111827
classDef effect fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef equal fill:#fde68a,stroke:#d97706,color:#78350f
classDef balanced fill:#dcfce7,stroke:#16a34a,color:#14532d
```
This is the same balance rule from [debits and credits](/en/fundamentals/accounting/debits-and-credits): one account gets a debit, another gets a matching credit, and the totals agree.
## Why bother recording it twice?
***
Recording both sides feels like extra work. It's actually the whole point — it's a built-in lie detector.
* **Nothing gets lost.** If money left an account, it *must* show up somewhere else. You can't misplace it without the books refusing to balance.
* **Nothing gets invented.** Money can't appear out of thin air. For it to land somewhere, it had to come from somewhere.
* **Errors reveal themselves.** If the two sides don't match, you know instantly that something's wrong — before it becomes a real problem.
A single-entry list (just "money in, money out") can't do any of this. Double-entry turns the record into something you can **trust and prove** — which is exactly why banks and ledger systems are built on it.
## A simple example
***
**Paying rent — \$1,000 in cash:**
```mermaid theme={null}
flowchart LR
Cash["Cash (an asset) − $1,000 money left"]:::cash
Rent["Rent Expense + $1,000 where it went"]:::rent
Cash -->|"$1,000 moves"| Rent
classDef cash fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef rent fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
```
| Account | Effect |
| ------------------- | --------------------------------------------------- |
| **Cash** (an asset) | Goes **down** \$1,000 — money left |
| **Rent expense** | Goes **up** \$1,000 — that's what the money was for |
The \$1,000 didn't disappear. It moved from your cash into the cost of rent — two accounts, one matched movement, a clear source and a clear destination.
Money coming *in* works the same way in reverse: take a \$2,000 customer payment and **Cash** goes up while **Revenue** goes up to record where it came from. You'll write both of these out properly — debit and credit, side by side — in [Journal entries](/en/fundamentals/accounting/journal-entries). This page is only about *why* both sides have to exist; the next one is about *how* to write them.
## How it keeps the equation balanced
***
Remember **Assets = Liabilities + Equity**? Double-entry is what keeps that equation true after every transaction. Because each event is recorded with equal and opposite effects, the two sides of the equation can never drift apart. The bookkeeping habit and the balancing rule are really the same idea, viewed from two angles.
**See also in Core Banking**
Core banking is built on this same both-sides rule — see [How money is recorded](/en/fundamentals/core-banking/double-entry-explained).
## In short
***
* **Double-entry** records both sides of every transaction — where money came from and where it went.
* It exists so **nothing gets lost or invented**, and so errors show up immediately.
* Every entry uses a matching **debit and credit**, which keeps *Assets = Liabilities + Equity* in balance no matter what happens.
**Next up**
That's the rule. Next, see how you actually write a transaction down — step by step — in [Journal entries](/en/fundamentals/accounting/journal-entries).
# Financial statements
Source: https://docs.lerian.studio/en/fundamentals/accounting/financial-statements
The three reports that summarize a business's money — the Balance Sheet, Income Statement, and Cash Flow Statement — and what each answers, in plain language.
All those carefully recorded transactions add up to something useful: **financial statements**. These are the summaries that turn thousands of tiny entries into a picture anyone can read. There are three main ones, and each answers a different question about the business.
You don't need to know how to *build* them — just what each one **tells you**.
## The three statements at a glance
***
| Statement | The question it answers | Think of it as |
| ----------------------- | ------------------------------------- | -------------------- |
| **Balance Sheet** | What do we own and owe *right now*? | A snapshot |
| **Income Statement** | Did we make a profit *over a period*? | A highlight reel |
| **Cash Flow Statement** | Where did the cash actually *move*? | A bank-account story |
## The Balance Sheet — a snapshot in time
***
The **Balance Sheet** is a photograph of the business at a single moment. It lists everything the company **owns** (assets), everything it **owes** (liabilities), and what's **left over for the owners** (equity). It's the [accounting equation](/en/fundamentals/accounting/assets-liabilities-equity) — *Assets = Liabilities + Equity* — printed as a report. If you want to know how healthy a business is *today*, this is where you look.
## The Income Statement — profit over time
***
The **Income Statement** (also called the *Profit & Loss* or *P\&L*) covers a stretch of time — a month, a quarter, a year. It starts with the **money earned** (revenue), subtracts the **money spent** (expenses), and shows whether the business ended up with a **profit or a loss**. Where the Balance Sheet is a snapshot, this is the story of what happened *between* two snapshots.
## The Cash Flow Statement — where the cash moved
***
The **Cash Flow Statement** tracks **cash and cash equivalents** (shortened to *cash* on this page) going in and out over a period. This matters because a business can look profitable on paper yet still run out of cash — for example, if customers haven't paid their bills yet. Under accrual accounting, revenue is recognized when it is earned and expenses when they are incurred, not when cash moves; cash-flow reporting tracks whether the business has the liquidity to pay its bills.
## How they fit together
***
The three aren't separate — they're three views of the same reality:
```mermaid theme={null}
flowchart TD
A["Income Statement Did we make a profit?"]:::is --> B["Profit changes equity"]:::link
D["Cash Flow Statement Where did cash move?"]:::cf --> E["Cash balance changes"]:::link
B --> C["Balance Sheet What do we own & owe now?"]:::bs
E --> C
classDef is fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef cf fill:#fef3c7,stroke:#d97706,color:#78350f
classDef bs fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef link fill:#f3f4f6,stroke:#6b7280,color:#111827
```
* The **Income Statement** shows whether you earned a profit.
* That profit flows into **equity** on the **Balance Sheet**.
* The **Cash Flow Statement** explains why the cash on that Balance Sheet went up or down.
Read together, they tell you whether a business is profitable, solid, and able to pay its bills — the three things anyone lending money or making decisions needs to know.
**See also in Core Banking**
See how a ledger's records are proved against the outside world in [The outside world](/en/fundamentals/core-banking/external-accounts-and-reconciliation).
## In short
***
* The **Balance Sheet** is a snapshot of what you own and owe right now.
* The **Income Statement** shows whether you made a profit over a period.
* The **Cash Flow Statement** tracks where cash and cash equivalents moved — because profit and cash aren't the same thing.
**Next up**
You just saw that profit and cash aren't the same thing. See exactly why — and when each one gets recorded — in [Accrual vs. cash accounting](/en/fundamentals/accounting/accrual-vs-cash).
# Journal entries
Source: https://docs.lerian.studio/en/fundamentals/accounting/journal-entries
How you actually record a transaction, step by step — from debits and credits to writing it down, with T-accounts and real examples like paying rent.
You've met [debits and credits](/en/fundamentals/accounting/debits-and-credits) and [double-entry](/en/fundamentals/accounting/double-entry-bookkeeping) as ideas. Now comes the practical bit: **how do you actually write a transaction down?** The answer is the **journal entry** — the basic unit of recording, the place where every concept finally touches paper.
## What a journal entry is
***
A **journal entry** is the written record of a single transaction. It names the accounts involved, says how much value was recorded, and marks which side — debit or credit — each account is on.
Think of it as the sentence accounting uses to describe an event. Every entry has the same three parts:
* **Which accounts** are affected (at least two).
* **How much** the amount is.
* **Which side** each account sits on — debit (left) or credit (right).
And it follows one unbreakable rule you already know: **total debits must equal total credits.** If they don't, the entry is wrong before you've even finished writing it.
## The steps, every single time
***
Recording any transaction is the same short routine. Walk through it slowly the first few times and it becomes automatic.
1. **What happened?** Describe the event in plain words — "we paid rent," "a customer paid us."
2. **Which accounts does it touch?** Every event hits at least two. One gives, one receives.
3. **Does each account go up or down?** Decide the direction for each.
4. **Translate up/down into debit/credit.** Use the account type to know which side that means.
5. **Check the balance.** Debits on the left, credits on the right — and the two totals must match.
```mermaid theme={null}
flowchart TD
A["1 · What happened?"]:::step --> B["2 · Which accounts are affected?"]:::step
B --> C["3 · Did each account go up or down?"]:::step
C --> D["4 · Translate into debit or credit"]:::step
D --> E["5 · Check: debits = credits"]:::check
classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef check fill:#dcfce7,stroke:#16a34a,color:#14532d
```
Step 4 is the one that trips people up, so keep the cheat sheet from [debits and credits](/en/fundamentals/accounting/debits-and-credits) handy:
| Account type | To increase it | To decrease it |
| ------------------------------ | -------------- | -------------- |
| **Assets** (what you own) | Debit | Credit |
| **Liabilities** (what you owe) | Credit | Debit |
| **Equity** (what's yours) | Credit | Debit |
| **Revenue** (money earned) | Credit | Debit |
| **Expenses** (money spent) | Debit | Credit |
## T-accounts in practice
***
A **T-account** is just a way to picture one account as a big letter **T** — debits on the left, credits on the right. A journal entry is what you get when you write down the matching halves of two (or more) T-accounts at once.
```
Cash (an asset) Rent expense
----------------------- -----------------------
Debit (+) | Credit (-) Debit (+) | Credit (-)
----------------------- -----------------------
| $1,000 $1,000 |
----------------------- -----------------------
```
The left side of one account lines up with the right side of another. That mirror is the entry. Now let's write a couple for real.
## Example 1 — paying rent in cash
***
**What happened:** the business paid \$1,000 for rent.
* It touches two accounts: **Cash** and **Rent expense**.
* **Cash** goes *down* — money left the business. Cash is an asset, so a decrease is a **credit**.
* **Rent expense** goes *up* — that's what the money was for. Expenses increase with a **debit**.
Written as a journal entry:
| Account | Debit | Credit |
| ---------------- | ------- | ------- |
| **Rent expense** | \$1,000 | |
| **Cash** | | \$1,000 |
Debits (\$1,000) equal credits (\$1,000). The entry balances. It reduces cash and recognizes rent expense for the period; both effects are on the record.
## Example 2 — receiving payment for a cash sale
***
**What happened:** a customer paid the business \$2,000 for a cash sale.
* It touches **Cash** and **Revenue**.
* **Cash** goes *up* — money arrived. Cash is an asset, so an increase is a **debit**.
* **Revenue** goes *up* — that's where the money came from. Revenue increases with a **credit**.
| Account | Debit | Credit |
| ----------- | ------- | ------- |
| **Cash** | \$2,000 | |
| **Revenue** | | \$2,000 |
Again, debits equal credits, and the money has a clear source and destination. Notice that **Cash was debited this time and credited last time** — same account, opposite directions, depending on whether money came in or went out. If the payment settles an earlier invoice, the credit is to Accounts Receivable rather than Revenue.
**See also in Core Banking**
In a ledger system a journal entry becomes a transaction made of operations — see [How money moves](/en/fundamentals/core-banking/how-money-moves) and [The building blocks](/en/fundamentals/core-banking/building-blocks).
## In short
***
* A **journal entry** is how you actually record a transaction: which accounts, how much, and which side each is on.
* Follow the same steps every time — name the event, find the two accounts, decide up or down, translate to debit/credit, then check that the two sides balance.
* The iron rule never changes: **debits must equal credits**, which is what keeps every entry — and the whole ledger — trustworthy.
**Next up**
Every entry posts to an account — but where does that list of accounts come from? Meet the [Chart of accounts](/en/fundamentals/accounting/chart-of-accounts).
# Revenue, expenses & costs
Source: https://docs.lerian.studio/en/fundamentals/accounting/revenue-and-expenses
What revenue, expenses, and costs really are, told apart in plain language — the words that drive the Income Statement and why the difference matters.
Two words show up everywhere in accounting: **revenue** and **expenses**. They're the money a business *earns* and the money it *spends*. Get these two straight and the [Income Statement](/en/fundamentals/accounting/financial-statements) — the report that says whether you made a profit — suddenly reads like a simple subtraction.
## Revenue — the money you earn
***
**Revenue** is the amount a business earns from doing what it does: selling a product, providing a service, charging for a subscription. It is recognized when earned, which may differ from when cash is received.
Picture a bakery. Every loaf it sells brings in revenue. Add up a day's sales and that total is the day's revenue. It's the inflow that everything else gets measured against.
One thing to hold onto: revenue is about **what you earned**, not necessarily the cash sitting in your account today. That subtlety has its own page — [accrual vs. cash](/en/fundamentals/accounting/accrual-vs-cash) — but for now, just think of revenue as *money earned from the work*.
## Expenses — the money you spend to earn it
***
An **expense** is a cost recognized in a period to keep the business running and earn revenue: rent, salaries, electricity, flour for the bakery. It may or may not involve a cash payment in that same period.
Expenses aren't a bad thing — they're the fuel. You can't sell bread without buying flour and paying the baker. The point of accounting isn't to avoid expenses; it's to know exactly what they are so you can tell whether the revenue was worth it.
## Cost — close to expense, but not identical
***
People use **cost** and **expense** as if they mean the same thing, and in casual talk they nearly do. The small distinction worth knowing:
* A **cost** is what you *pay* to get something — the price of acquiring a resource. Buying an oven for the bakery has a cost.
* An **expense** is a cost *counted against the revenue of a period* — the portion that's "used up" in earning this stretch's income. The flour baked into today's bread is an expense today.
In short: a **cost** becomes an **expense** once it's used up in the work of earning revenue. The oven is a cost when you buy it; the slice of its wear-and-tear that helped make this month's bread becomes an expense this month.
| Term | Plain meaning | Bakery example |
| ----------- | -------------------------------------------- | ------------------------------------------ |
| **Revenue** | Money earned from the work | Selling loaves |
| **Cost** | What you pay to acquire something | Buying the oven |
| **Expense** | A cost used up to earn this period's revenue | The flour baked today, the rent this month |
Don't over-think the cost-versus-expense line — just know that "cost" leans toward *acquiring*, and "expense" leans toward *using up to earn revenue*. Follow one bag of flour through the bakery and the whole chain falls into place:
```mermaid theme={null}
flowchart LR
A["Buy flour cost"]:::cost --> B["Bake the bread expense"]:::expense
B --> C["Sell the bread revenue"]:::revenue
C --> D["Revenue − expenses profit or loss"]:::profit
classDef cost fill:#fef9c3,stroke:#ca8a04,color:#713f12
classDef expense fill:#fed7aa,stroke:#ea580c,color:#7c2d12
classDef revenue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef profit fill:#dcfce7,stroke:#16a34a,color:#14532d
```
## Why they matter to the Income Statement
***
These two words are the entire engine of the [Income Statement](/en/fundamentals/accounting/financial-statements):
* Start with **revenue** — the amount recognized as earned.
* Subtract **expenses** — the costs recognized for that period.
* What's left is **profit** (or, if expenses were bigger, a **loss**).
A month at the bakery, in one line:
$$
\underbrace{\$1{,}000}_{revenue} - \underbrace{\$700}_{expenses} = \underbrace{\$300}_{profit}
$$
This is a simplified view. Revenue minus expenses tells you whether the business reported profit or loss over a period; a full statement can group and present those amounts in more detail.
And that profit doesn't just disappear — it flows into **equity**. Earn a profit and, before any distributions to owners, the owners' stake in the business grows; run a loss and it shrinks on the same basis. That's the quiet link back to [Assets, liabilities & equity](/en/fundamentals/accounting/assets-liabilities-equity): the Income Statement is the story of *how* equity changed over a period.
## In short
***
* **Revenue** is the amount a business earns from its work; **expenses** are costs recognized to earn that revenue, whether or not cash moves in the same period.
* A **cost** is the amount incurred to acquire something; it becomes an **expense** once it's used up in earning a period's revenue.
* In a simplified view, the **Income Statement** is **revenue minus expenses** — the difference is profit or loss.
**Next up**
Now you know the five account families. Next, see how every change to them gets written down — as [Debits and credits](/en/fundamentals/accounting/debits-and-credits).
# What is accounting?
Source: https://docs.lerian.studio/en/fundamentals/accounting/what-is-accounting
The core idea behind accounting — a trustworthy record of money in, money out, and what a business owns versus what it owes. No formulas, just the concept.
**Accounting** is the practice of keeping a clear, honest record of a business's money — and everything of value it owns or owes, not just the cash on hand. Think of it as a diary that answers three questions at any moment: **What came in? What went out? And where do we stand right now?**
That last question is the important one. A business doesn't just want to know how much cash is in the drawer today — it wants to know what it **owns**, what it **owes**, and whether the two add up. Accounting is how it keeps that picture accurate, day after day.
## The three things every business tracks
***
Strip away the jargon and accounting is really watching three flows:
* **Money coming in** — sales, payments from customers, loans received, money the owners put in.
* **Money going out** — rent, salaries, supplies, loan repayments.
* **What's owned vs. what's owed** — the cash, equipment, and money others owe you, balanced against the debts you still have to pay.
A good record aims to keep these connected. If money moved, it should be written down; if something is owned or owed, it should be accounted for. Controls and reconciliation help identify missing or inconsistent records.
## Why it has to be trustworthy
***
Imagine running a shop where you *think* you have money but aren't sure. You can't pay suppliers with a guess. You can't prove to a bank that you're worth lending to. You can't tell if you're actually making a profit.
Accounting uses records and controls to make financial information checkable. A balanced entry is an arithmetic check: it confirms the recorded sides match, but does not by itself prove authorization, classification, or completeness.
| Without good accounting | With good accounting |
| ---------------------------------- | ------------------------------------------------------ |
| You guess how much you have | Your records show how much you have and why |
| Money can quietly go missing | Controls and reconciliation help catch missing records |
| You can't prove anything to others | Auditors and banks can verify it |
| Profit is a mystery | Profit is a number you can see |
**See also in Core Banking**
The same trustworthy-record promise is what banking platforms are built to keep — see [What is core banking?](/en/fundamentals/core-banking/what-is-core-banking).
## In short
***
* **Accounting** is the trustworthy record of money coming in, money going out, and what a business owns versus owes.
* Balanced entries are an important arithmetic control, while reconciliation and other controls help test completeness and accuracy.
* That focus on checkable records is why accounting underpins banking, ledgers, and every serious financial system.
**Next up**
Now that you know *what* accounting tracks, see *why* it matters the moment software starts holding money in [Why accounting matters](/en/fundamentals/accounting/why-accounting-matters).
# Why accounting matters?
Source: https://docs.lerian.studio/en/fundamentals/accounting/why-accounting-matters
The "so what?" for builders — why double-entry is a system requirement, why balancing guarantees correctness, and why financial systems rely on it.
You already know *what* accounting is: an honest record of money in, money out, and what's owned versus owed. This page is about why that matters for the thing you're actually building — a ledger, a wallet, a payments flow, a banking platform. The short version: **the moment software starts holding other people's money, it inherits accounting's rules whether it names them or not.**
So this isn't a detour into finance theory. The same ideas that keep a shopkeeper's books honest are the ones that keep your system from quietly losing — or inventing — a customer's balance.
## Money is a promise, and promises need a ledger
***
A balance in an app isn't cash sitting in a box. It's a **promise**: a claim that some amount belongs to someone and can be moved or withdrawn. The only thing making that promise real is the record behind it. Lose the record, or let it drift, and the money effectively doesn't exist — or worse, exists twice.
That's why every financial product, underneath the UI, is a record-keeping problem first. The interesting parts — transfers, fees, settlement, statements — all sit on top of one question that has to be answered correctly every single time: **who owns what, right now?** Accounting is simply the discipline that has been answering that question reliably for centuries.
## Why double-entry is a system requirement, not a convention
***
Imagine a customer moves \$50 from one wallet to another. If the first wallet goes down by \$50 *and* the second goes up by \$50, matching recorded totals let the system check its internal record of that transfer. If only one side gets recorded, the mismatch is a signal to investigate rather than proof that the business event was correct.
That's why double-entry exists. It gives a transaction a checkable constraint: the recorded sides must match.
Encode that as a rule and you get an important consistency control:
* **A movement records its participating accounts.** The record identifies the accounts affected by the transaction.
* **The recorded sides must equal.** Matching totals make the entry arithmetically checkable.
* **A mismatch surfaces immediately.** If the recorded sides do not match, the transaction can be rejected before the inconsistency compounds.
Midaz applies this directly: it rejects a transaction when its resolved source, destination, and send totals differ. That check is valuable, but it does not prove authorization, account selection, classification, valuation, or complete business correctness.
## Balance is a consistency control
***
Here's the part worth internalizing as a builder. Balanced records make one part of a transaction continuously testable: the recorded amounts agree.
Matching debits and credits detects a recorded amount mismatch. *Assets = Liabilities + Equity* is a statement-level accounting equation, not a runtime assertion Midaz applies after each operation. Records can balance while an account is wrong, a transaction is unauthorized, a value is misclassified, or another required record is missing.
Financial systems still rely on this control. Recording both sides gives balance a useful tripwire: when it breaks, the recorded amounts are inconsistent and the system must investigate before proceeding.
Put end to end, that's the chain in one line — balanced records keep the amounts behind a balance consistent; reconciliation and separate controls are still needed to trust it fully:
```mermaid theme={null}
flowchart LR
A["User sees a balance"]:::step --> B["System keeps ledger records"]:::step
B --> C["Each movement has two sides"]:::step
C --> D["Books stay balanced"]:::step
D --> E["Recorded amounts are consistent"]:::trust
classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef trust fill:#dcfce7,stroke:#16a34a,color:#14532d
```
## Financial systems are accounting systems wearing different clothes
***
Strip a core banking platform down to its foundation and you find a ledger. Strip the ledger down and you find double-entry accounting, enforced by code instead of by a clerk with a pen. The product vocabulary changes — *accounts*, *transactions*, *operations*, *balances* — but the bones are the same accounting ideas, now running at scale and in real time.
That's why this matters for what you build:
* **The data model is accounting.** Accounts, entries, and balances aren't an implementation detail bolted onto a payments app — they *are* the app's source of truth.
* **The safety properties use accounting controls.** A balanced posting detects mismatched recorded amounts; authorization, classification, and completeness require additional controls.
* **The hard problems are accounting at scale.** Handling many transfers at once, keeping every copy of the data in agreement, and proving what happened long after the fact all come down to keeping that balanced record correct across millions of movements.
Learn the accounting once and a lot of system design stops feeling arbitrary. The constraints you keep bumping into aren't the platform being fussy — they're centuries-old rules for not losing track of money, finally written down as code.
**See it in the product**
These ideas have direct counterparts in Lerian and Midaz — accounts, transactions, operations, and balanced movements. See how they map in [Accounting in Lerian](/en/fundamentals/accounting/accounting-in-lerian), or step into the [Core banking fundamentals](/en/fundamentals/core-banking/what-is-core-banking).
## In short
***
* The moment software holds money, it inherits accounting's rules — a balance is a promise, and the record behind it is what makes that promise real.
* **Double-entry is a consistency control**: every recorded transaction has participating accounts and matching recorded sides.
* **Balance is not a complete correctness guarantee**. It detects amount mismatches; authorization, classification, and completeness need separate controls.
* Strip any serious financial system down and you find an accounting system at its core, now enforced by code instead of by hand.
**Next up**
Time to meet the equation everything rests on. Start with [Assets, liabilities & equity](/en/fundamentals/accounting/assets-liabilities-equity).
# The building blocks
Source: https://docs.lerian.studio/en/fundamentals/core-banking/building-blocks
The five core nouns of core banking — Organization, Ledger, Asset, Account, and Balance — each in one sentence, in the order they depend on each other.
Walk into a library and the structure is obvious before anyone explains it. There's the library itself, the catalog that organizes everything, the shelves, what's actually sitting on each shelf right now, and the *kind* of thing a shelf holds. A core banking system is built from five pieces that line up the same way.
You don't need to memorize them. You just need to see how each one rests on the one before it.
## They stack in order
***
Each block depends on the one above it: a shelf sits in a library, a balance sits in an account. Here they are, top to bottom.
* **Organization** — your company, the outer boundary of everything you run. *The library building itself.* All your records live inside it and belong to it.
* **Ledger** — one self-contained book of records. *The catalog.* You can keep more than one — say, one per business line or per region — and each is its own balanced, independent set of books.
* **Asset** — the *kind* of value being tracked: a currency like USD, loyalty points, anything countable. *The category of thing on a shelf.* Tracking the unit separately from the amount is what lets the system know that 100 of one thing isn't 100 of another.
* **Account** — a container tied to one asset. A non-external account can have one or more keyed balance records; an external account has exactly one `default` balance. *A single shelf.* Every movement of money has accounts on both sides.
* **Balance** — one keyed record of the amount an account holds, including available and held amounts. *What's actually on the shelf at this moment.*
An **Organization** can hold many **Ledgers**; a **Ledger** holds many **Accounts**; an **Account** is always tied to one **Asset** and can carry one or more keyed **Balance** records. Read top to bottom, each block is the home of the next.
```mermaid mermaid theme={null}
flowchart TB
O["Organization your company"]
L["Ledger one book of records"]
Acc["Account holds value, sends & receives"]
Bal["Balance the amount held right now"]
As["Asset the kind of value"]
O --> L
L --> Acc
Acc --> Bal
As -.-|"defines the unit"| Acc
classDef box fill:#e8f0fe,stroke:#4285f4,color:#333;
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class O,L,Acc,As box;
class Bal bal;
```
## A balance is a result, not a setting
***
Here's the one idea worth slowing down on. A **balance** is not a number you type in and adjust by hand. It's the **result** of every movement into and out of the account, added up. Money arrives, money leaves, and the balance is simply where that leaves you.
That's why the books stay trustworthy: a balance can't be quietly edited, only *moved into the shape it has* by recorded movements with two sides each.
A balance isn't always one single number you can spend. Part of it can be **available** — yours to use now — while part is **held**, set aside for a movement that hasn't finished. We'll unpack available versus held when we look at how money moves.
## In short
***
* The five building blocks stack in order: **Organization** → **Ledger** → **Account** → **Balance**, with an **Asset** defining what an account holds.
* An **Organization** is your company; a **Ledger** is one self-contained book; an **Account** holds and moves value through keyed **Balance** records. Non-external accounts can have multiple records; external accounts have one `default` record.
* An **Asset** is the *kind* of value — tracked separately so amounts of different things never get confused.
* A **balance** is a **result** of recorded movements, never a number you set by hand. Each balance record can track **available** and **held** amounts and its own sending and receiving settings.
These five nouns are the vocabulary for everything else. Once they feel natural, movements, holders, and fees are just things you *do* with them.
**See it in Lerian**
See the building blocks in practice: [Core entities](/en/midaz/core-entities), [Ledgers](/en/midaz/ledgers), [Accounts](/en/midaz/accounts), [Balances](/en/midaz/balances), and [Assets](/en/midaz/assets).
# Designing your ledger plan
Source: https://docs.lerian.studio/en/fundamentals/core-banking/designing-your-ledger-plan
How to model your own business as a set of accounts — a simple, worksheet-style method for designing a chart of accounts before you build anything.
By now you know the pieces: accounts, balances, movements, the rules that govern them. This is the page where you put them to work on *your* business. The goal is to move from "I understand the terms" to "I can model what my company actually does".
Here's the mindset shift: a **ledger plan** — sometimes called a **chart of accounts** — is not a list of accounts you rush to create. It's a *deliberate classification* of the accounts your business needs, and how they relate. Designing it well, up front, saves you from painful restructuring later.
## Think rooms before you build
***
Imagine designing a house. Before you pour any concrete, you decide what each room is for — kitchen, bedroom, office — and who's allowed in. You don't start laying bricks and figure out the floor plan afterward.
A ledger plan works the same way. You decide what each account is *for*, what rules it follows, and how money flows between rooms — **before** you create anything. The rest of this page is a simple, four-step method for doing exactly that.
## Step 1: List your real-world actors
***
Start away from the software entirely. On paper, list every real-world party that touches money in your business. Don't worry about structure yet — just name them.
A typical list looks like this:
* **Customers** — the people or companies who hold value with you
* **Treasury** — your own operating funds
* **Revenue** — money you earn
* **Fees** — charges you collect on movements
* **Settlement** — money in transit to or from the outside world
* **Expenses** — money you pay out
This is your raw material. Every business has its own version of this list.
## Step 2: Decide what needs its own truth
***
Not every actor needs its own account. The test is simple: **does this thing need its own separate, trustworthy balance — its own truth?**
If you need to know, at any moment, exactly how much value sits with this actor, it needs its own account. If it's just a label or a detail of something else, it doesn't.
A good rule of thumb: if you'd ever want to ask "how much is in *here* right now?" — that's an account. If two things should never share a balance, they should never share an account.
## Step 3: Classify and group with three questions
***
This is the heart of the method. For each account you kept, ask **three universal questions**. Together they tell you how to structure and group your accounts.
| Ask yourself | What it's about |
| ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Do these accounts need a **rule that validates** what they are or how they behave? | Classification — enforcing that an account is a certain *kind*, with its own constraints. |
| Do I need to **slice them for reporting**? | Labeling — tagging accounts so you can later filter and report by group (region, tier, department). |
| Do I need to **group them operationally** around a customer or wallet? | Operational grouping — bundling several accounts that belong together for one owner. |
These three questions are independent. An account might need all three, or just one. Keep them separate in your mind:
```mermaid mermaid theme={null}
flowchart TD
A["An account in your plan"] --> Q1{"Needs a rule that validates its kind?"}
Q1 -->|yes| C["Give it a classification"]
A --> Q2{"Needs to be sliced for reporting?"}
Q2 -->|yes| L["Give it a reporting label"]
A --> Q3{"Belongs to a customer or wallet group?"}
Q3 -->|yes| G["Put it in an operational group"]
classDef q fill:#fff8e1,stroke:#f4b400,color:#333;
class Q1,Q2,Q3 q;
```
The big mistake here is collapsing these three into one. *Classification* (what an account is allowed to be) is not the same as a *reporting slice* (how you'd group it in a report), which is not the same as an *operational group* (which accounts belong to one customer). Treating them as one tangled thing is how ledger plans become unmanageable.
## Step 4: Sketch the movements
***
Finally, draw the arrows. For each kind of movement your business makes, sketch which account is the source and which is the destination. This is where your plan comes alive — and it's exactly the input you'll need when you later define reusable rules for those movements.
## A worked example: a simple wallet business
***
Say you run a wallet app. Customers load money in, send it to each other, and you take a small fee on transfers. Walk the method:
**Actors:** customers, your fee/revenue account, and a settlement account for money entering from the outside world.
**Own truth?** Each customer wallet needs its own balance — yes. Fees collected need their own balance — yes. Settlement needs its own balance to mirror the outside world — yes.
**Three questions:**
* *Validation?* Customer wallets behave differently from your fee account, so they're a different **kind** — classify them.
* *Reporting slice?* You want to report customers by country — add a **label** for region.
* *Operational group?* A premium customer might hold several wallets — **group** them together.
**Movements:** a transfer moves value from one customer wallet to another, plus a small part into the fee account. Money entering the app moves from settlement into a customer wallet.
```mermaid mermaid theme={null}
flowchart LR
S["Settlement (outside world)"] -->|load| C1["Customer wallet A"]
C1 -->|transfer| C2["Customer wallet B"]
C1 -->|fee part| F["Fee / revenue"]
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class C1,C2,F,S bal;
```
That's a complete ledger plan for a small business — sketched before touching any tool.
## Common first-timer mistakes
***
* **Over-classifying.** Creating a dozen account kinds when two would do. Start coarse; refine only when a real rule demands it.
* **Mixing reporting into structure.** Don't bake a reporting slice (like "region") into an account's *kind*. Keep how you classify separate from how you report.
* **Treating the plan as a list.** Rushing to create accounts before sketching movements leaves you reorganizing later.
* **Skipping the "own truth" test.** Giving everything its own account — or cramming unrelated things into one — both cause pain.
## In short
***
* A **ledger plan** is a deliberate classification of the accounts your business needs — not a list to rush.
* Design it like rooms in a house: decide purpose and access **before** you build.
* The method: (1) list real-world actors, (2) keep the ones that need their **own truth**, (3) classify and group them with the **three questions**, (4) sketch the movements.
* Keep the three questions — *validation*, *reporting slice*, *operational grouping* — separate. Collapsing them is the classic mistake.
**See it in Lerian**
Those three questions map onto how Lerian models accounts:
* *Validation rule?* → **Account Types**
* *Reporting slice?* → **Segments**
* *Operational grouping?* → **Portfolios**
See it all together in [Accounting in Midaz](/en/midaz/accounting-in-midaz), then put your plan to work with the console [Setup path](/en/midaz/console/midaz-console-setup-path) and [Concepts map](/en/midaz/console/midaz-console-concepts-map).
# How money is recorded
Source: https://docs.lerian.studio/en/fundamentals/core-banking/double-entry-explained
Why every movement of money has two sides, what debit and credit really mean in a ledger, and how double-entry keeps every transaction balanced and auditable.
If you've ever looked at a banking system and felt lost the moment someone said "debit" and "credit", this page is for you. You don't need an accounting background. You just need one idea — and once it clicks, almost everything else in core banking falls into place.
Here it is: **money never appears out of nowhere, and it never simply vanishes. It moves.** Every time money moves, it leaves one place and arrives at another. Recording *both* sides of that movement is the whole idea behind **double-entry accounting**.
## Money always comes from somewhere
***
Think about pouring water between two buckets. The water that leaves the first bucket is exactly the water that fills the second. Nothing is created, nothing is lost — it just changes place. You can always check that the amounts match.
A core banking system records money the same way. Every movement has two parts:
* **Where it came from** — the source
* **Where it went** — the destination
If a customer sends R\$100 to a merchant, the system doesn't just say "the merchant has R\$100 more". It records the *full story*: R\$100 **left** the customer and R\$100 **arrived** at the merchant. Two sides of one movement, always equal.
This is why it's called double-entry: every movement is written down **twice** — once as it leaves, once as it arrives. A single-sided record would let money quietly appear or disappear. Double-entry makes that impossible.
## The two sides have names: debit and credit
***
The two sides of a movement have traditional names. Don't let them intimidate you — they're just labels for "out of" and "into":
* A **debit** is the entry on the account that money moves **out of**.
* A **credit** is the entry on the account that money moves **into**.
That's it. When the customer pays the merchant, the system records a **debit** on the customer's side and a **credit** on the merchant's side. Same amount, opposite directions.
You'll sometimes see debit and credit described as "decrease" and "increase", but that's not always true — it depends on the kind of account. The reliable way to think about it is always the same: **debit is the source, credit is the destination.**
## Why both sides are always required
***
Because every movement is recorded on both sides, the totals always have to match. The sum of everything that left equals the sum of everything that arrived. If they don't balance, something is wrong — and the system won't let the movement through.
```mermaid theme={null}
flowchart LR
A["Customer account (source)"] -->|"– R$100 · debit"| T(("Movement R$100"))
T -->|"+ R$100 · credit"| B["Merchant account (destination)"]
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class A,B bal;
```
This single rule — *the two sides must balance* — is what makes a financial system trustworthy. It means the records can never silently lose money or invent it. Every value in the system can be traced back to a movement that has a clear source and a clear destination.
Each individual part of a movement — a single debit or a single credit — is called an **operation**. A movement of money is built from operations that balance out. You'll see this word a lot, so it's worth remembering: **an operation is one part of a movement.**
## In short
***
* Money doesn't appear or disappear — it **moves** from a source to a destination.
* **Double-entry** means every movement is recorded on both sides: a **debit** (out of) and a **credit** (into).
* The two sides always carry the same amount, so the books **always balance** — that's what keeps the system trustworthy.
* A single debit or credit is called an **operation**.
Keep this mental model close. Everything that follows — accounts, balances, transactions, fees — is just this idea, applied.
**See it in Lerian**
See these ideas in practice: [Transactions](/en/midaz/transactions) and the [Glossary](/en/glossary).
# The outside world
Source: https://docs.lerian.studio/en/fundamentals/core-banking/external-accounts-and-reconciliation
The doorway between your Ledger and the outside world — how external accounts work and how reconciliation proves your records match reality across systems.
Everything so far has lived neatly inside your Ledger. But money doesn't stay inside — it arrives from banks, card networks, and other systems, and it leaves to them too. So how does a self-contained Ledger connect to the world outside? And once it does, how do you *prove* your records still match what really happened out there? Two ideas answer this, and they're really one story.
Picture a building with a single front door. Everyone and everything coming in or going out passes through it. The door doesn't store anything itself — it just marks the boundary between inside and outside. Your Ledger has a door like this, and it's worth understanding before you trust the numbers on either side of it.
## External accounts: the doorway to the outside
***
An **external account** is the doorway between your Ledger and the world beyond it — banks, card networks, other systems. Money entering your Ledger comes *from* an external account; money leaving goes *to* one. Every crossing of your boundary passes through it.
In Midaz, the default external account has `debit` direction. When R\$100 flows *in* to a customer, Midaz uses that external account as the source: its debit increases the available amount, balancing the credit on the receiving account. The direction does not mean the external account has a negative balance.
Think of the building's front door again. The external account is that door. Its direction tells Midaz how to apply a debit or credit; it does not label a balance as missing money.
```mermaid mermaid theme={null}
flowchart LR
E["External account (the doorway)"] -->|"+ R$100 in"| A["Customer account (inside the Ledger)"]
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class A bal;
classDef ext fill:#eef2ff,stroke:#6366f1,color:#333;
class E ext;
```
## Reconciliation: proving the books match reality
***
Once you have a boundary, you need to check that what you recorded inside matches what the outside world recorded. That check is **reconciliation**: proving your internal records line up with external records — bank statements, processor reports, settlement files.
You compare the two, line by line, and confirm they agree. When they don't, reconciliation is how you catch it:
* **Missing** movements — something happened outside that your Ledger never recorded.
* **Duplicated** movements — the same thing recorded twice.
* **Mismatched** movements — amounts or details that don't line up.
When do you do it? It depends on your business — some reconcile **continuously** as records arrive, others at the **end of each day**. Either way, it's the natural counterpart to having a boundary: the moment you connect to the outside world, you take on the job of proving the two sides still agree.
Reconciliation is like **counting the till against the receipts** at the end of a shift. The cash drawer is your Ledger; the receipts are the outside record. Counting them against each other proves the books match what actually happened — and flags anything that doesn't.
## In short
***
* An **external account** is the doorway between your Ledger and the outside world — money in comes from it, money out goes to it.
* In Midaz, the default external account has `debit` direction, so a debit increases its available amount as the source of an inflow.
* **Reconciliation** is proving your internal records match external ones (bank statements, processor reports).
* It catches **missing**, **duplicated**, and **mismatched** movements, run **continuously** or at **end of day**.
**See it in Lerian**
See these ideas in practice: [Transactions](/en/midaz/transactions) and the [Glossary](/en/glossary).
# Who owns what: holders, accounts & aliases
Source: https://docs.lerian.studio/en/fundamentals/core-banking/holders-accounts-aliases
Holders, accounts, and aliases untangled — what the ledger actually owns, who stands behind it in the real world, and the friendly names that point to it.
You've seen accounts hold balances and movements flow between them. But who *owns* those accounts? And why do banking systems keep tossing around words like "alias" and "alias account" as if they were obviously different things? This page untangles all of it.
Imagine a row of numbered safe-deposit boxes in a vault. The box itself just holds what's inside it — it doesn't know or care who you are. To make that vault useful, you also need a record of *who* the box belongs to, and a friendlier way to refer to it than a long number. Those are three separate jobs, and core banking keeps them separate on purpose.
## The account is the truth
***
An **account** is the balance container — the truth that lives inside the Ledger. It records exactly how much of an asset it holds, built up from every movement in and out. That's all it does, and that's deliberate: the account stays clean and reliable because it carries no messy real-world identity data.
In the vault, the account is the numbered box. It holds value; it doesn't hold your name, your documents, or your phone number.
## The holder is the person or company
***
A **holder** is the real-world owner standing behind the account — **the person or company you register**. This is the customer record: name, document number, contact details. Registering these owners is its own job, often called *cadastro de pessoas* — keeping track of the people and companies your business deals with.
Why keep the holder separate from the account? Two reasons:
* One owner can hold **many accounts**. The owner is registered once; the accounts attach to them.
* The Ledger stays trustworthy. Identity data changes often and is sometimes sensitive — you don't want it tangled into the records that prove your balances.
In the vault, the holder is the person named on the contract. One person can rent several boxes.
## Aliases: friendly names for accounts
***
Accounts are identified by long, unique IDs — accurate, but no fun to type or remember. So you give them friendlier names. Here's where two similar-sounding terms get confused — they do different things:
* An **alias** is a human-friendly handle for an account — a nickname like `marketing-fund` so you don't juggle a long ID.
* An **alias account** is a record that ties an owner's real-world banking details (bank, branch, account number) to an underlying Ledger account.
So an *alias* is just a convenient label pointing at one of your own accounts. An *alias account* is a bridge — it connects outside banking details to the right account inside your Ledger.
Back to the vault. The **alias** is the nickname sticker on the box ("Marketing fund") so staff don't need the box number. The **alias account** is the front-desk index card that links a customer's external bank details to which box is theirs.
```mermaid mermaid theme={null}
flowchart LR
H["Holder (registered person/company)"] --> AA["Alias account (real banking details)"]
AA --> ACC["Account (balance in the Ledger)"]
AL["Alias (friendly nickname)"] -.points to.-> ACC
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class ACC bal;
```
## Why bother with all this separation
***
Splitting these roles apart looks like extra work, but it buys you real flexibility:
* One holder, **many accounts**, without duplicating the owner each time.
* Friendly **aliases** so people and systems reference accounts without long IDs.
* **Alias accounts** that connect to multiple banks and external systems — without ever polluting the Ledger's clean record of value.
The Ledger keeps the truth. Everything else points at it.
## In short
***
* An **account** is the balance container — the truth inside the Ledger, free of identity data.
* A **holder** is the real-world owner you register — the person or company (*cadastro de pessoas*). One holder can own many accounts.
* An **alias** is a friendly nickname pointing at one of your accounts.
* An **alias account** is a record linking real-world banking details to a Ledger account.
* Keeping these separate buys flexibility while keeping the Ledger clean.
**See it in Lerian**
See these ideas in practice: [Accounts](/en/midaz/accounts) and [Alias accounts](/en/midaz/crm/alias-accounts).
# How money moves
Source: https://docs.lerian.studio/en/fundamentals/core-banking/how-money-moves
What counts as one movement of money, what its parts are called — transactions, operations, and holds — and where money sits before it finally settles.
You already know that money never appears or vanishes — it moves from a source to a destination, and both sides get recorded. This page is about the *movement* itself: what one complete movement is, what its pieces are called, and the part most people find confusing — where the money sits while a movement is still in progress.
Picture handing cash across a counter. The moment the cash leaves your hand and lands in the other person's, the whole exchange happened as one event. A banking system treats movement the same way: one complete handover, recorded in full, all at once.
## One movement, recorded in full
***
A **transaction** is one complete movement of value — the whole event, start to finish. When a customer sends R\$100 to a merchant, that single transaction *is* the R\$100 changing hands.
But you've already seen that every movement has two sides. So a transaction is built from smaller pieces:
* An **operation** is one part of the movement — a single debit or a single credit.
* A transaction is made of operations that **balance**: everything debited equals everything credited.
The R\$100 transfer is one transaction made of two operations — a debit of R\$100 leaving the customer, and a credit of R\$100 arriving at the merchant. Same amount, opposite directions, recorded together.
Keep this word handy: an **operation is one part of a movement**. A transaction is the whole movement; the operations are its parts. You'll meet this word everywhere in core banking.
## The shapes a movement can take
***
The simplest movement has one source and one destination, but a single transaction can spread across more accounts — and however many parts it has, it always balances. The common shapes:
* **One-to-one** — one source, one destination. A plain transfer: customer out, merchant in.
* **One-to-many** — one source, several destinations. The customer pays R\$103: R\$100 goes to the merchant and R\$3 to a fee account. Three operations, still balanced (R\$103 out = R\$100 + R\$3 in).
* **Many-to-one** — several sources, one destination. A few accounts pooling money into a single fund.
* **Many-to-many** — several sources and several destinations at once, like a marketplace splitting incoming payments across sellers and fees.
The rule never changes, no matter how many parts there are: **everything that leaves must equal everything that arrives.**
Not every ledger supports all four shapes. Simpler systems may allow only one-to-one (or one-to-many) movements, while others handle the full many-to-many case in a single transaction — so what's possible depends on the ledger you use.
A transaction is **all-or-nothing**. Either every operation succeeds together, or none of them happen. The books can never be left half-recorded — this property is called **atomicity**.
## Money in transit: holds and two-phase movements
***
Here's the part that trips up almost everyone: where does the money sit *before* a transaction is final?
Many movements don't settle the instant they start. The system first **reserves** the amount — it's set aside, still in the account, but no longer free to spend. Then the transaction either **commits** (settles for good) or **cancels** (releases the reservation). This is a **two-phase** movement: reserve first, then commit or cancel.
To make sense of this, an account's money is described in two parts:
* **Available balance** — what you can actually spend right now.
* **Held balance** — what's reserved for a movement that hasn't finished. Still yours, just frozen in place until it settles or releases.
Think of a hotel pre-authorization on your card. At check-in the hotel **reserves** an amount — it isn't gone, but you can't spend it. At check-out the charge **commits**, or the hold is **canceled** and the money frees up. Same money, two phases.
```mermaid mermaid theme={null}
flowchart LR
R["Reserve amount held"] -->|"settles"| C["Commit moves to destination"]
R -->|"released"| X["Cancel returns to available"]
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class R,C,X bal;
```
## Spending control
***
Once you can see money as *available* versus *held*, controlling spending becomes natural. At any moment, an account's money sits in one of three states:
* **Available** — free to spend.
* **Held** — reserved for a movement in progress (in transit).
* **Blocked** — frozen on purpose, set aside so it can't move at all.
On top of that, movement can be capped by **limits** — a ceiling on how much can flow. Together, these explain the question people always ask: *why can't this account spend?* Usually it's because the money is held, blocked, or over a limit — not missing.
## In short
***
* A **transaction** is one complete movement of value — the whole event.
* An **operation** is one part of it: a single debit or credit. A transaction is operations that balance.
* A movement can range from **one-to-one** to **many-to-many**; whatever its shape, it's **all-or-nothing** and always balances.
* Money in transit is **held**, not gone: a two-phase movement **reserves**, then **commits** or **cancels**.
* An account's money is **available**, **held**, or **blocked**, and can be capped by **limits** — that's spending control.
**See it in Lerian**
See these ideas in practice: [Transactions](/en/midaz/transactions), [Operations](/en/midaz/operations), and [Balances](/en/midaz/balances).
# The rules that govern movement
Source: https://docs.lerian.studio/en/fundamentals/core-banking/routes-and-fees
The reusable transaction routes that keep each kind of money movement recorded correctly, and why a fee is just a few more operations on the same journey.
You know how money moves and how it's recorded. But who decides the *rules*? When a payment happens, something has to make sure the right kinds of accounts are involved and the right parts get recorded — every single time, the same way. And when a fee shows up, where does it actually come from? This page demystifies both.
Think of a paper form built for one kind of request. Someone designed it once — the boxes it has, the line for who's allowed to sign, the rule for how it's filed. After that, any clerk just fills in the details, and every request of that kind comes out identical — no guessing. That's the idea behind a route: set the rule once, and it's applied the same way every time.
## A route is a reusable rule for one kind of movement
***
A **route** is a reusable rule for one *kind* of movement. It answers three questions, once, so you don't answer them again on every transaction:
* **Who can be the source?** Which accounts are allowed to send.
* **Who can be the destination?** Which accounts are allowed to receive.
* **Which parts get recorded?** Which debits and credits the movement should produce.
Define the rule once, and every transaction of that kind follows it. A "customer payment" route spells out that a customer account sends, a merchant account receives, and exactly which operations to write.
Why does this exist? So the same kind of movement is **always recorded correctly**, and **only valid accounts** take part. No clerk fills the form in wrong, because the form won't let them.
A route is like a **form template** for one kind of transaction. It lays out who's allowed and how the movement is booked, so every movement of that kind comes out identical. A fee, as you'll see, is just an extra line on that form.
## Fees are just more operations
***
Fees feel mysterious, but they're not magic — and they're *not* "subtract a number and move on". Remember that a transaction is made of **operations** (each one part — a single debit or credit). A fee is simply *more operations* in the same transaction, sending value into a fee or revenue account.
Take the one-to-many example from before. A customer pays **R\$103**:
* **R\$100** lands at the **merchant**.
* **R\$3** lands in a **fee account**.
That's one transaction with three operations. The customer's R\$103 leaving balances exactly against R\$100 + R\$3 arriving. Nothing is invented or lost — the fee is real money moving to a real account, recorded like any other part.
```mermaid mermaid theme={null}
flowchart LR
C["Customer – R$103"] --> R{{"Route (rule + parts)"}}
R -->|"+ R$100"| M["Merchant"]
R -->|"+ R$3"| F["Fee account"]
classDef bal fill:#fff8e1,stroke:#f4b400,color:#333;
class M,F bal;
```
Because the fee is part of the same transaction, it's all-or-nothing too: the merchant gets paid and the fee is collected together, or neither happens.
## When you'd reach for routes and fees
***
You don't need a route for a one-off movement. You reach for one when a *kind* of movement repeats and must always be booked the same way — payments, transfers, settlements. And you add a **fee** whenever a movement should also send a slice of value into a revenue or cost account. Defining these deliberately up front is part of planning your Ledger, which comes next.
## In short
***
* A **route** is a reusable rule for one kind of movement: who can send, who can receive, and which parts get recorded.
* Routes keep every movement of a kind **recorded correctly**, with **only valid accounts** taking part.
* A **fee** isn't magic — it's just **more operations** moving value into a fee or revenue account (customer R\$103 = merchant R\$100 + fee R\$3).
* Because the fee is part of the same transaction, it's **all-or-nothing** with the rest.
**See it in Lerian**
See these ideas in practice: [Accounting Routes entities](/en/midaz/transaction-routing-entities) and the [Fees Engine](/en/midaz/fees/fees-engine-overview).
# What is core banking?
Source: https://docs.lerian.studio/en/fundamentals/core-banking/what-is-core-banking
A plain definition of core banking, what it manages — accounts, balances, transactions — and the five responsibilities we use to explain a modern core system.
A **core banking system** is the central platform a financial institution runs on. It manages **accounts, balances, transactions, the customers behind them, and money moving in and out** — all in one place, as a single source of truth.
Underneath all of it sits one idea that ties everything together: a trustworthy record of **who owns what** and **what moved**. The other jobs all connect to it — feeding it, moving value through it, reading it, proving it's right. That's what makes banking *banking*: money can never be quietly lost or invented. You don't need a banking background to follow along — this page just gives you the lay of the land.
## Why it's harder than it looks
***
Keeping that record right is deceptively hard, and it's the reason most of the system exists:
* **It must always balance** — money can't appear or disappear, ever.
* **It has to stay correct at scale** — millions of movements, all consistent with each other.
* **It must be provable** — to auditors and regulators, long after the fact.
## How we'll explain it: five jobs
***
There are many ways to describe a core banking platform. In this guide we use **five core responsibilities** — it's how we organize the topic for learning, not an official taxonomy.
| Job | What it does |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Record value** | Keeps the trustworthy record of who owns what, balanced so nothing is lost or invented. Everything else builds on this. |
| **Move value** | Brings money in, sends it out, and transfers it between accounts. |
| **Identify** | Registers the people and companies behind the accounts. |
| **Report** | Turns the raw record into statements and reports you — and regulators — can read. |
| **Trace & prove** | Keeps an auditable trail of everything that happened. |
The **Ledger** is the foundation — but it's only **one of the five**. A complete core banking system is all five working together, not a Ledger with extras bolted on. The other four build on the Ledger once value is being recorded, and capabilities like **fees** attach to it to extend it.
You adopt these pieces one at a time, not as a single machine you switch on. The upside: the same handful of ideas — accounts, balances, movements — run through all of them. Learn those once, here, and the rest of the documentation gets much easier to follow.
## In short
***
* **Core banking** is the central platform that manages accounts, balances, transactions, customers, and payments — all on one trustworthy record of who owns what and what moved.
* Keeping that record correct at scale, and provable after the fact, is the hard part — and the reason for most of what the system does.
* To learn it, we use **five responsibilities** — **record**, **move**, **identify**, **report**, and **trace & prove**. The **Ledger** is the foundation, but core banking is all five, not the Ledger alone.
**See it in Lerian**
See how these jobs map to real solutions in the [Core banking vision](/en/core-banking-vision) and [Products and plugins](/en/products-and-plugins).
# Where to go next
Source: https://docs.lerian.studio/en/fundamentals/core-banking/where-to-go-next
How everything you learned about core banking shows up in Lerian and Midaz — and where to go next to start building, integrating, and running transactions.
You've gone from "lost the moment someone said debit and credit" to modeling your own business as a ledger plan. That's the whole foundation — here's how it shows up in Lerian, and where to start.
## How the concepts show up in Lerian
***
Everything you just learned has a home in Lerian:
* **Recording value** — the accounts, balances, and assets you learned about, and the double-entry that keeps them in check, all live in [Midaz](/en/midaz/what-is-midaz), the Ledger. It's the foundation everything else builds on.
* **Moving value** — transfers, operations, and holds are recorded as [transactions](/en/midaz/transactions) in Midaz. When value crosses the boundary with the outside world, payment integrations like [Pix](/en/rails/pix/pix-overview) and [TED](/en/rails/ted/ted-overview) carry it — but plenty of movement is simply recorded in the Ledger. The routes and fees you saw come from Midaz's [transaction routing](/en/midaz/transaction-routing-entities) and the [Fees Engine](/en/midaz/fees/fees-engine-overview).
* **Identifying who owns what** — the holders and friendly aliases behind your accounts live in [CRM](/en/midaz/crm/crm-overview).
* **Reading the record back** — Midaz produces [account statements](/en/midaz/building-account-statements) straight from the ledger, and [Reporter](/en/reporter/what-is-reporter) builds richer reports and templates for audits, analytics, and decisions.
* **Tracing and proving** — [Tracer](/en/tracer/what-is-tracer) gives you the auditable trail across everything that happened.
And when you design your own ledger plan, the three questions — validation, reporting slice, operational grouping — map to **Account Types, Segments, and Portfolios** in Midaz.
## Ready to start?
***
There are two ways to put this into practice — pick what fits your team:
Integrate programmatically — start here, then go deeper with the Guides and API Reference for the integration details.
Prefer a UI? Do the whole setup by hand in the Lerian Console — no code required.
Need a term? The [Glossary](/en/glossary) defines every word from this tab. Keep this tab bookmarked — and see how the pieces come together in [Building a complete core banking](/en/building-a-complete-core-banking).
# Where to start
Source: https://docs.lerian.studio/en/fundamentals/introduction
A quick map of the Fundamentals tab — two plain-language series, Core Banking and Accounting, and which one to read first for what you want to learn.
The **Fundamentals** tab is where you build intuition before touching anything Lerian-specific. It holds two short, plain-language series that build a foundation for core banking from two angles: accounting explains trustworthy financial records, while Core Banking explains how a complete banking platform applies them.
Explains the language underneath it: assets, liabilities, debits, credits, and how the books stay balanced.
Explains a complete core banking platform: its ledger foundation, accounts, balances, value movement, customer identity, reporting, and auditability.
They sit side by side because they answer different questions. One shows you the *machine*; the other shows you the *grammar* that machine is built on.
## Which one first
***
| If you want to… | Start with |
| -------------------------------------------------------------------------------------- | ------------------------------------- |
| Understand how a complete core banking platform works, including its ledger foundation | **Core Banking** |
| Understand the accounting language behind it | **Accounting** |
| Understand both | **Accounting**, then **Core Banking** |
If you're unsure, start with **Accounting**. The vocabulary it teaches makes everything in Core Banking — and the rest of the docs — easier to follow.
# Getting started
Source: https://docs.lerian.studio/en/getting-started
Begin with Lerian — evaluate the platform, choose a deployment model, and run your first double-entry transaction with Midaz to see the ledger in action.
Whether you want to evaluate the platform or start building, this is the place to begin.
**New to core banking?** If terms like *Ledger*, *debit and credit*, or *chart of accounts* aren't yet second nature, start with the fundamentals first — they'll make everything here click into place.
**[Begin with Core Banking Fundamentals](/en/fundamentals/core-banking/what-is-core-banking)**
## Evaluating the platform
***
Follow these steps to understand the platform's capabilities, architecture, and deployment options.
Learn what problems the platform solves and who we built it for — in business terms.
**[Why Lerian →](/en/why-lerian)**
Lerian organizes around four pillars — **Ledger**, **Transactional Services**, **Governance**, and **Connectivity** — that form a composable financial platform.
**[Read the Core Banking vision →](/en/core-banking-vision)**
Decide between Lerian Cloud (SaaS), fully managed by Lerian, or self-hosted BYOC based on your compliance requirements and operational maturity.
**[Compare deployment models →](/en/deployment-models)**
Explore real-world use cases — digital banking, global portfolios, Pix, marketplace payouts, and more.
**[Browse use cases →](/en/midaz/use-cases)**
## Building with Lerian
***
Follow these steps to go from zero to your first double-entry transaction. Choose the path that matches your deployment model.
Your Lerian onboarding team provides you with API credentials (client ID and secret) and the base URL for your environment.
Use the Access Manager Auth API to exchange your credentials for a JWT access token. This token contains your tenant context — you don't need to manage a tenant ID explicitly.
**[Learn how to authenticate →](/en/platform/access-manager/using-access-manager)**
Include the token as a Bearer token in the `Authorization` header of every request. Your calls are automatically scoped to your tenant.
**[Make your first API call →](/en/reference/quick-start-guide)**
Follow the same API flow as any Midaz deployment — create an organization, a ledger, assets, and accounts. The platform handles tenant isolation transparently.
**[Follow the recommended workflow →](/en/midaz/recommended-workflow)**
The API surface is identical across deployment models. Lerian Cloud and BYOC Multi-Tenant require a Bearer token and scope requests to the authenticated tenant. A permitted non-production BYOC Single-Tenant deployment can have authentication disabled. Learn more about [multi-tenancy](/en/multi-tenancy).
Decide how you want to run the platform. BYOC gives you full control over data, security, and networking — in your own cloud or on-premises.
**[Compare deployment models →](/en/deployment-models)**
Lerian organizes around four pillars — **Ledger**, **Transactional Services**, **Governance**, and **Connectivity** — that form a composable financial platform. Understanding this helps you model integrations correctly from day one.
**[Read the Core Banking vision →](/en/core-banking-vision)**
Midaz is Lerian's double-entry core ledger. Install it when you need to model accounts and record transactions; other products remain modular and can be adopted independently.
**[Install Midaz →](/en/midaz/midaz-setup)**
With Midaz running, create the essential building blocks: an organization, a Ledger, assets, and accounts.
**[Make your first API call →](/en/reference/quick-start-guide)**
Move beyond isolated API calls and follow the end-to-end operational workflow — from resource setup to transaction routing and balance management.
**[Follow the recommended workflow →](/en/midaz/recommended-workflow)**
## What's next?
***
With your Ledger running and your first transactions created, pick the capability you want to add next.
Run your first automated reconciliation between a bank statement and your Ledger.
Set up spending limits and validation rules to evaluate transactions before they proceed and return an ALLOW, DENY, or REVIEW decision for your authorization flow to act on.
Create a report template and generate your first financial report from a configured data source. For Midaz data, connect Reporter to Midaz first.
# Glossary
Source: https://docs.lerian.studio/en/glossary
Key financial, ledger, and platform terms explained for non-technical audiences — a quick reference for language used across Lerian and Midaz docs.
This glossary defines the core financial and platform terms used throughout Lerian's documentation. It is designed to help product managers, sales teams, and anyone new to financial infrastructure quickly understand the language used in our guides.
***
## A
***
### Account
The fundamental unit of value storage in a ledger. An account holds a balance denominated in a specific asset (e.g., BRL, USD). Accounts can represent customer accounts, internal settlement accounts, fee collection accounts, or any other container for tracking value.
### Account Type
A classification that defines the nature and purpose of an account — such as checking, savings, escrow, or settlement. Account types enable proper transaction routing and ensure that business rules are applied consistently across account categories.
### Asset
A unit of value tracked in the ledger. Assets can be traditional currencies (BRL, USD, EUR), digital currencies (BTC, ETH), or custom units like loyalty points or academic credits. Every account is linked to exactly one asset.
### Atomic (Atomicity)
A property of transactions where all operations either succeed together or fail together — there is no partial execution. If a transaction involves three operations (a debit, a credit, and a fee), either all three happen or none do. This prevents incomplete financial events.
### Audit trail
A chronological record of every action and transaction in the system. Audit trails are immutable — once recorded, they cannot be modified or deleted. This is essential for regulatory compliance (SOX, GLBA) and dispute resolution.
***
## B
***
### Balance
The current amount of an asset held in an account. Midaz tracks two balance amounts: **available** (can be used immediately) and **on hold** (reserved for pending transactions).
### Blast radius
The scope of impact if a failure, misconfiguration, or security incident occurs. In multi-tenancy contexts, blast radius refers to how many tenants are affected when something goes wrong in one tenant's scope. DATABASE mode minimises blast radius because each tenant has a fully isolated database; a failure or data corruption affects only that tenant. SCHEMA mode has a wider blast radius because tenants share the same database process. See also: **DATABASE mode**, **SCHEMA mode**, **Tenant isolation**.
### BYOC (Bring Your Own Cloud)
A deployment model where you run Lerian software on your own infrastructure — whether public cloud (AWS, GCP, Azure), private cloud, or on-premises. This gives you full control over data residency, security, and scaling. See also: **SaaS**.
***
## C
***
### CEL (Common Expression Language)
A lightweight, type-safe expression language that Tracer uses for writing business rules. CEL lets analysts and compliance teams define validation logic — such as "if transaction amount exceeds 10,000 and account type is personal, flag for review" — without writing application code or requiring deployments.
### Core banking
A complete banking platform that combines a ledger foundation with the capabilities required to operate financial products: accounts and balances, value movement, customer identity, reporting, governance, connectivity, and auditability. A ledger is foundational, but it is not the whole core banking platform.
### Child account
A sub-account created under a parent account, typically used for budgeting or organizational purposes. For example, a customer's savings account might have child accounts for "Vacation Fund" and "Emergency Fund," each tracking its own balance while rolling up to the parent.
***
## D
***
### Double-entry accounting
An accounting principle where every financial movement is recorded as at least two operations: a debit from one account and a credit to another. This ensures the system always balances — the total of all debits equals the total of all credits. Midaz enforces double-entry automatically on every transaction.
### DSL (Domain-Specific Language)
A simplified programming language designed for a specific domain. In Flowker, the DSL lets teams define workflow steps, conditions, and integrations using a structured format — without writing general-purpose code.
***
## E
***
### ERP (Enterprise Resource Planning)
A business management software system — such as SAP, Oracle, or NetSuite — that integrates core processes like accounting, procurement, and inventory. ERPs are common data sources for reconciliation with Matcher.
### External account
A special account in Midaz (prefixed with `@external/`) that represents value entering or leaving the system from outside. For example, when a student enrolls in a course, credits move from `@external/available-courses` into the student's account. External accounts act as boundaries between your ledger and the outside world.
***
## F
***
### Fail closed
A safety posture where a system denies access by default when it cannot verify a required condition, rather than allowing access and risking a breach. In Lerian multi-tenancy, tenant resolution is fail-closed: if the tenant cannot be identified from the JWT, or if tenant configuration cannot be retrieved, the request is rejected — never served against a default, shared, or another tenant's data. See also: **Tenant isolation**, **Multi-tenancy**.
***
## H
***
### Helm Chart
A pre-configured deployment package for Kubernetes. Helm Charts contain all the instructions needed to install, configure, and upgrade software in a Kubernetes cluster. Lerian provides Helm Charts for deploying products in BYOC (self-hosted) environments.
***
## I
***
### Idempotency
The property that an operation produces the same result whether executed once or multiple times. In financial systems, this prevents duplicate transactions — if a payment request is accidentally sent twice, idempotency ensures the money only moves once.
### Immutable (Immutability)
A property meaning "cannot be changed or deleted after creation." In Midaz, all transactions and audit records are immutable — once recorded, they exist permanently. This guarantees data integrity for compliance and auditing.
***
## K
***
### Kubernetes
A platform for running and managing software applications at scale across multiple servers. Kubernetes automates deployment, scaling, and recovery — ensuring applications stay available even when individual servers fail. Required for BYOC deployments of Lerian products.
***
## L
***
### Ledger
The core financial book that records all transactions, balances, and operations for an organization. Think of it as the single source of truth for a business unit's finances. An organization can have multiple ledgers for different purposes (e.g., retail operations, treasury).
***
## M
***
### Metadata
Additional information attached to entities like accounts, transactions, or organizations. Metadata is stored as key-value pairs and can hold any custom data your business needs — such as a customer's external ID, a transaction's reference number, or a department code. It does not affect ledger logic but enriches records for reporting and integration.
### Multi-tenancy
An operating model where one deployment serves independent customer contexts — called tenants — with product-specific request scoping and storage isolation. Lerian Cloud operates multi-tenant; BYOC multi-tenancy is available only for products and entitlements that support it. Tenant Manager uses `dedicated` and `shared` for PostgreSQL modes described as `DATABASE` and `SCHEMA`. See also: **Tenant**, **Tenant isolation**.
***
## O
***
### Onboarding domain
The part of Midaz responsible for structuring your financial ecosystem — creating organizations, ledgers, assets, accounts, portfolios, and segments. This is the "setup" phase before any transactions occur.
### Operation
The atomic unit of movement in the ledger — a single debit or credit. Every transaction is composed of one or more operations, providing precise traceability for compliance and reporting.
### Operation Route
A reusable template that defines the rules for one "leg" of a financial transaction — specifying which accounts participate, in what direction (source/destination), and under what accounting annotation.
### Organization
The top-level business entity in Midaz — typically a bank, fintech, or subsidiary. Each organization holds its own ledgers, configurations, and access controls.
***
## P
***
### Pix
Brazil's instant payment system, operated by the Central Bank of Brazil (BACEN). Pix enables real-time transfers 24/7 between individuals, businesses, and government entities. In the Lerian ecosystem, the Pix plugin connects Midaz to the Pix network, handling payment initiation, settlement, and fee management.
### Portfolio
A grouping of accounts belonging to the same customer, business unit, or purpose. For example, a single client may hold checking, savings, and investment accounts across different currencies — a portfolio ties them together for a unified view.
### Plugin
An optional extension that adds functionality to a specific Lerian product. Examples include Pix (instant payments), TED (bank transfers), and Payments (boletos and bill payment). Plugins run inside a host product's architecture rather than standalone — today, all available plugins extend Midaz.
***
## R
***
### Reconciliation
The process of comparing two sets of records — for example, your internal ledger against bank statements — to verify they match. Matcher automates this process by applying matching rules and flagging discrepancies.
***
## S
***
### Lerian Cloud (SaaS)
Lerian's fully managed deployment model. Lerian operates the licensed platform modules and underlying infrastructure; customers use the supported product APIs and configuration surfaces. See also: **BYOC**.
### Segment
A classification applied to accounts that share characteristics — like customer tiers (VIP, Student, Business) or regional divisions. Segments enable differentiated rules, pricing, and benefits across your customer base.
### Service-centric architecture
An architectural model in which configuration and infrastructure are organised around services rather than tenants. In Lerian multi-tenancy, a Tenant entity holds identity metadata, while a Service entity holds infrastructure configuration (database credentials, connection details, isolation mode). This separation means the same tenant can have different isolation modes across different products, and a service can be reconfigured independently of the tenant's identity. See also: **Multi-tenancy**, **Tenant isolation**.
### Settlement
The process of finalizing a financial transaction — transferring the actual funds between institutions. Settlement accounts in Midaz act as clearing points where money is held temporarily before being moved to its final destination (e.g., via Pix or TED to the central bank).
### SOX (Sarbanes-Oxley Act)
A U.S. federal law that mandates strict financial record-keeping and auditing standards for publicly traded companies. SOX compliance requires immutable audit trails and accurate financial reporting — capabilities that Midaz and Matcher provide by design.
### Source-available
A licensing model where source code is publicly accessible under terms that restrict some uses. Within the Lerian portfolio, **Midaz** and **Fetcher** are source-available under the Elastic License 2.0 (ELv2); they are not generic open-source products. Other Lerian products, including Reporter, Matcher, Flowker, Tracer, and plugins, are closed source and available to licensed customers.
### Streaming Hub
Lerian's event delivery edge. Streaming Hub consumes platform events from the internal event stream and fans them out to external systems through signed webhooks, message queues, or a pull API — handling delivery retries and signature verification along the way. It is a closed-source Lerian product, available to licensed customers.
***
## T
***
### Tenant
A logically isolated customer context in a multi-tenant deployment. Its data model, authentication claims, routing, and backing resources are defined by the supported product service; do not assume one product's tenant model applies to another. See also: **Multi-tenancy**, **Tenant isolation**.
### Tenant isolation
The controls that prevent one tenant's supported product service, data, configuration, and operations from being used by another. For PostgreSQL services managed through Tenant Manager, `dedicated` corresponds to a database per tenant and `shared` to a schema per tenant in a shared database. Authentication and routing controls remain product-specific. See also: **Multi-tenancy**, **Tenant**.
### Terraform
An infrastructure automation tool that provisions and manages cloud resources — servers, databases, networks, security groups — through declarative configuration files. Lerian provides Terraform templates for setting up BYOC infrastructure on AWS, GCP, or Azure.
### TED (Transferência Eletrônica Disponível)
A Brazilian electronic bank transfer system for high-value or same-day transfers between financial institutions. Unlike Pix, TED operates only during business hours. In the Lerian ecosystem, the TED plugin connects Midaz to the TED network.
### Transaction
A movement of value between accounts. Transactions can involve multiple operations (debits and credits) and are processed atomically — either all operations succeed, or none do. This ensures no partial financial events occur.
### Transaction domain
The part of Midaz responsible for executing financial movements — processing transactions, updating balances, and maintaining audit trails.
### Accounting Route (Transaction Route)
A complete transaction blueprint composed of multiple Operation Routes. For example, a "Pix Transfer" route defines all the debits and credits involved, ensuring double-entry compliance every time that transaction type is executed. Called **Accounting Route** in the Lerian Console and product docs; exposed as `transactionRoute` in the API.
***
## W
***
### Webhook
An automated notification sent from one system to another when a specific event occurs. For example, Matcher can send a webhook to your JIRA instance when a reconciliation exception is found, automatically creating a ticket for your team to investigate.
# Welcome
Source: https://docs.lerian.studio/en/index
Explore Lerian Docs — guides, platform capabilities, API references, and integration tutorials covering Midaz, plugins, and the wider Lerian ecosystem.
Build core banking on primitives you control.
Guides, platform capabilities, and API references for the entire Lerian ecosystem — from the double-entry ledger up.
Where the platform meets Brazil's payment system. Reach the rails through Lerian's own messaging with BACEN, or through an interface with a connectivity partner. Either way, you keep your ledger, your data, and your business logic. What are Brazil Rails? →
# About Midaz
Source: https://docs.lerian.studio/en/midaz/about-midaz
Explore Midaz's Ledger service, its Onboarding and Transaction domains, and its embedded CRM and Fees capabilities.
Midaz is a source-available core ledger. Financial institutions use it to build, operate, and scale their transactions on a modular foundation. You keep full control of your stack.
Midaz's Ledger service has two core domains: **Onboarding** and **Transaction**. It also embeds CRM for holders and instruments, and Fees. Together, they provide the components to configure, execute, and govern financial flows.
This domain-driven architecture ensures that configuration (Onboarding) and execution (Transaction) remain clearly separated yet fully integrated — enabling performance, traceability, and business agility.
## Domains and APIs
***
### Onboarding domain
In the **Onboarding Domain**, institutions structure their financial operations. They configure the essential entities of their ledger. This domain sets the rules, accounts, and relationships that govern how the ledger records and interprets transactions.
#### Components of the Onboarding Domain
* **Organizations**: A business entity that runs on Midaz — such as a bank, fintech, or subsidiary. Each organization holds its own ledgers, configurations, and access controls. One platform then supports multi-entity operations.
* **Ledgers**: The core financial book of an organization. Ledgers track every balance, transaction, and operation. They protect the integrity of all financial data. Think of a ledger as the single source of truth for a business unit's finances.
* **Assets**: The units of value that the ledger tracks — such as BRL, USD, BTC, or loyalty points. With this flexibility, institutions manage traditional currencies, digital assets, and custom reward programs in the same system.
* **Account Types**: Classifications that define the nature and purpose of accounts — for example, checking, savings, escrow, or settlement. With `validateAccountType` enabled, a new non-external Account's `type` must match a registered value. Operation Routes separately validate accounts during route processing.
* **Accounts**: The fundamental units of value storage — where money, or any asset, actually lives. Each account always links to an Asset. You can group accounts into segments or portfolios to reflect your business structure.
* **Portfolios**: A group of accounts that belong to the same customer, business unit, or purpose. For example, one client may hold checking, savings, and investment accounts in different currencies. A portfolio ties them together for a unified view.
* **Segments**: Groups of accounts that share traits, such as customer tiers or regional divisions — for example, VIP or Student. Segments help you enforce different rules, prices, or benefits across your customer base.
The Onboarding Domain configures how your financial world is structured, ensuring consistency before any transaction takes place.
### Transaction Domain
After you configure the Onboarding Domain, the **Transaction Domain** takes over. It powers the real-time execution, movement, and reconciliation of funds.
#### Components of the Transaction Domain
* **Balances**: The current balance of any account, including its available and on-hold amounts. This real-time snapshot supports validations, pre-transaction checks, and customer-facing displays.
* **Operation Routes**: Reusable templates that define the rules for each leg of a financial transaction. A route specifies which accounts participate, in what direction (source or destination), and under what accounting annotation. Every transaction type then follows consistent business rules.
* **Operations**: The atomic unit of movement in the ledger — a single debit or credit. The ledger breaks every financial movement into operations. This gives a complete audit trail and precise traceability for compliance and reporting.
* **Accounting Routes**: Complete transaction blueprints that combine multiple Operation Routes. For example, a Pix Transfer route defines all the debits and credits involved. When you enable Accounting Route validation, it enforces double-entry compliance for that transaction type. The API exposes this as the `transactionRoute` resource.
* **Transactions**: The actual movement of value between accounts. A transaction can involve multiple **Operations**, such as fees, taxes, or splits. This supports complex financial flows, like marketplace payouts or multi-party settlements, in a single atomic event.
The Transaction Domain ensures that every movement is processed accurately, transparently, and in compliance with business and accounting rules configured upstream.
## In short
***
Midaz is a domain-driven ledger platform for reliability, flexibility, and governance.
| Domain | Purpose | Key APIs |
| :-------------- | :----------------------------------------------------------- | :------------------------------------------------------------------------------------------- |
| **Onboarding** | Structure and configuration of the financial ecosystem. | Organizations, Ledgers, Assets, Account Types, Accounts, Portfolios, Segments |
| **Transaction** | Execution, movement, and accounting of financial operations. | Balances, Accounting Routes (`transactionRoute`), Transactions, Operation Routes, Operations |
With this architecture, financial institutions structure their data with precision and operate at scale. You keep full control and transparency over your financial ecosystem.
# Account types
Source: https://docs.lerian.studio/en/midaz/account-types
Register Account Types to enforce classification per Ledger, validate the type field on account creation, and align account behavior with your accounting rules.
Account Types classify the accounts in your ledger. Each [**Account Type**](/en/reference/midaz/create-an-account-type) has a name, a description, and a unique key value.
You register the Account Types that match your accounting structure. When you enable Account Type validation, the ledger checks the `type` field of every new non-external Account against these registered types.
## How Account Type validation works
### Enabling Account Type validation
You enable Account Type validation per ledger through the [Ledger Settings API](/en/midaz/ledgers#ledger-settings). Send a `PATCH` request to the Ledger Settings endpoint:
```json PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings theme={null}
{
"accounting": {
"validateAccountType": true
}
}
```
Settings changes take effect immediately — no redeployment required. You can update them at any time through the API.
### Behavior of the `type` field in Accounts API
When you create an Account, the `type` field behavior depends on the validation setting:
* **Validation disabled (default):** You must provide a `type`. It accepts any string value up to 256 characters. The ledger does not compare it to registered Account Types.
* **Validation enabled:** The `type` field must match the `keyValue` of a registered Account Type. Midaz stores new `keyValue` values in lowercase and compares the value case-insensitively. Use lowercase values in requests and configuration. If it does not match, the ledger rejects the account with a validation error. External accounts skip this check.
Before you enable Account Type validation, review your existing accounts. The `type` field of an existing Account cannot be changed, so create a matching registered Account Type or recreate an Account whose `type` must change.
### The `keyValue` field
The `keyValue` field identifies an Account Type. It has these constraints:
* **Immutable:** You set `keyValue` when you create the Account Type. You cannot change it afterward. The Update Account Type endpoint does not accept this field.
* **Unique per ledger:** Each `keyValue` must be unique within a ledger.
* **Used for validation:** When you enable `validateAccountType` in Ledger Settings, the ledger compares the `type` field of each non-external Account case-insensitively with a registered `keyValue`. The ledger rejects any non-external Account with an unregistered `type`.
## Managing Account Types
***
You manage Account Types through the API or the Lerian Console.
* [Create an Account Type](/en/reference/midaz/create-an-account-type) — Register a new Account Type for your Ledger.
* [List Account Types](/en/reference/midaz/list-account-types) — Retrieve all Account Types configured in your Ledger.
* [Retrieve an Account Type](/en/reference/midaz/retrieve-an-account-type) — Get detailed information about a specific Account Type.
* [Update an Account Type](/en/reference/midaz/update-an-account-type) — Modify the definition of an existing Account Type.
* [Delete an Account Type](/en/reference/midaz/delete-an-account-type) — Remove an Account Type that is no longer in use.
**See also**
* [Transaction Routing](/en/midaz/transaction-routing-entities) — Operation Routes use `ruleType: account_type` to validate an account's `type` against the route's configured `account.validIf` values.
* [Accounting Entries](/en/midaz/accounting-entries) — How Midaz resolves debit and credit rubrics for the accounts these types classify.
# Accounting Entries
Source: https://docs.lerian.studio/en/midaz/accounting-entries
Map transaction actions and route directions to accounting classifications, and annotate every operation with its accounting code and description.
Accounting Entries (also known as **Rubricas**) map a transaction action and route direction to an accounting `code` and `description`. They annotate an operation after route resolution; Operation Route rules and transaction legs determine the participating accounts.
## What are Accounting Entries
***
A **rubric** maps a transaction action and route direction to an accounting classification. Instead of computing each classification by hand, you register rubrics once. Midaz then resolves them automatically as it processes transactions.
Each rubric carries:
* **`code`** — an accounting code (for example, `1.1.1.001`).
* **`description`** — a human-readable label for the entry (e.g., `Customer checking — outbound`).
* A set of **action mappings** — one entry per action type, each with its own debit and/or credit rubric.
When the engine processes a transaction with route validation (`accounting.validateRoutes`) enabled and a matching rubric registered, it resolves the rubric for each operation. It records the resulting **`routeCode`** and **`routeDescription`** on the operation. This gives you a complete audit trail from transaction to operation to rubric. Your teams can trace exactly which accounting rule applied to each movement.
Configure rubrics per action on each Operation Route. For `direct` and `commit` actions, **Source** routes require the **debit** rubric and **Destination** routes require the **credit** rubric. Dedicated `block` and `unblock` rubrics are optional; when absent, Midaz resolves the `direct` rubric for those actions. Source-side `hold` and `cancel` actions require **both** rubrics, and `overdraft` requires **both** on every supported route type. **Bidirectional** routes always require **both**.
## The 8 action types
***
Each action represents a distinct transactional event. The first five actions cover the transaction lifecycle. The last three cover overdraft, block, and unblock movements. A single rubric can map different debit and credit classifications for each action. Every stage of an operation then receives the right accounting annotation.
| Action | Identifier | Description |
| :------------ | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Direct** | `direct` | Immediate, single-step debit/credit between two accounts, with no intermediate stages (e.g., a fee or adjustment). |
| **Hold** | `hold` | Reserves funds by creating a pending movement (moves value from `available` to `on_hold` on the source account). |
| **Commit** | `commit` | Confirms a previously held amount, releasing the `on_hold` value to the destination account. |
| **Cancel** | `cancel` | Cancels/reverses a hold, returning the `on_hold` value to the `available` balance on the source account. |
| **Revert** | `revert` | Reverses a completed `direct` transaction by creating a counter-transaction that undoes the original. |
| **Overdraft** | `overdraft` | Classifies overdraft movements — the **debit** rubric marks overdraft usage (the deficit grows) and the **credit** rubric marks repayment (the deficit shrinks). Both rubrics are required when this entry is configured. |
| **Block** | `block` | Optionally classifies a fund-block movement that freezes value on an account (for example, an `asset-freeze`). |
| **Unblock** | `unblock` | Optionally classifies the release of previously blocked funds back to the `available` balance. |
**Overdraft** classifies companion operations that the engine generates automatically during overdraft usage and repayment. For every supported route type and direction, configure both the debit and credit rubrics. **Block** and **unblock** can use dedicated rubrics for operations that the block and unblock transaction endpoints produce; without them, those actions use the `direct` rubric. When needed, register dedicated rubrics for these actions in the same way as for the other actions.
Each action can point to different debit and credit accounting classifications within the same rubric. Map only the actions a route uses. If you enable strict validation (below), cover every action your transactions emit.
## Configuring Accounting Entries
***
You register rubrics through the API as part of your Operation Routes. The `accountingEntries` block on a route defines one entry per action. Each entry carries its `debit` and/or `credit` rubric:
```json accountingEntries theme={null}
{
"accountingEntries": {
"direct": {
"debit": {
"code": "1.1.1.001",
"description": "Customer checking — outbound"
},
"credit": {
"code": "1.1.1.002",
"description": "Customer checking — inbound"
}
},
"hold": {
"debit": {
"code": "1.1.1.001",
"description": "Customer checking — reserve"
},
"credit": {
"code": "2.1.1.001",
"description": "Pending settlement — hold"
}
}
}
}
```
You manage these entries through the Operation Route endpoints — see [Create an Operation Route](/en/reference/midaz/create-an-operation-route) and [Update an Operation Route](/en/reference/midaz/update-an-operation-route). For the full configuration flow, refer to [Transaction Routing](/en/midaz/transaction-routing-entities#4-configure-accounting-entries-actions).
## Validation modes
***
Midaz reacts to a missing rubric based on the Ledger's accounting settings. Two distinct gates control this behavior:
### Default (graceful)
By default (`accounting.validateRoutes` disabled), Midaz does not resolve rubrics at all: the transaction proceeds normally and the `routeCode` and `routeDescription` fields stay empty (nil) for every operation. It raises no error.
### Strict (opt-in)
Set `accounting.validateRoutes` to `true` in the [Ledger Settings](/en/midaz/ledgers#ledger-settings) to enforce route validation. In strict mode, a requested action with no routes in the transaction-route cache returns `0157 ErrNoRoutesForAction`. `0117 ErrAccountingRouteNotFound` applies when an operation route ID is absent from that cache.
```json PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings theme={null}
{
"accounting": {
"validateRoutes": true
}
}
```
In strict mode, do not treat `0117 ErrAccountingRouteNotFound` as the error for every unmapped action: it applies when an operation route ID is absent from the transaction-route cache. A requested action with no routes in that cache returns `0157 ErrNoRoutesForAction`.
Use **strict mode** in production ledgers where every transaction type needs an accounting classification. The graceful default helps while you onboard routes. In production, it can silently leave movements without a classification.
When Midaz finds a matching rubric, it annotates the operation with two fields:
* **routeCode** — the `code` of the resolved `AccountingRubric` for that action and direction.
* **routeDescription** — the description of the resolved rubric, populated alongside `routeCode`.
# Accounting
Source: https://docs.lerian.studio/en/midaz/accounting-in-midaz
Get an overview of accounting in Midaz: Chart of Accounts, Account Types, Accounting Entries, and Operations that keep every posting balanced and traceable.
Midaz records accounting for real-time financial systems. It uses strict double-entry bookkeeping. It supports digital products, embedded finance, and high-volume operations. Midaz is ledger-centric: it enforces every balance, movement, and posting at the ledger level. You get traceability, auditability, and consistency by default.
This page is the **entry point for the Accounting section**. It introduces the core primitives and shows how they fit together. It also points you to the reference pages and to a hands-on walkthrough of accounting end to end.
## How the primitives relate
***
Midaz turns business events into balanced ledger postings. A small set of primitives builds on each other:
#### Chart of Accounts
This is the structure of balances your product needs. Midaz has no dedicated Chart of Accounts API. Instead, you model it on a ledger from assets, accounts, segments, portfolios, and account types. The `code` field on an Accounting Entry (for example `1.1.1.001`) carries the traditional account number for each posting.
#### Account Types
These are reusable classifications that you assign to accounts. Each account type has a name, a key value, and a description. You use account types to group accounts in your financial structure.
#### Accounting Entries (Rubricas)
A route does not need an entry for every transaction action. When it emits an action, the entry must satisfy the route-specific debit and credit requirements. Each configured entry contains the debit and/or credit rubric required by its operation route; every present rubric has a `code` and description. With route validation (`accounting.validateRoutes`) enabled, Midaz stamps the matching rubric onto each operation, based on the operation's action and direction.
#### Operations
A transaction produces ledger movements. An **Operation** is a single leg of a transaction — one debit or one credit on a specific account. Midaz validates that the original transaction’s source, destination, and send totals agree. Its returned operation set is not always a debit/credit pair: pending creation and cancellation process only source-side legs. Each operation carries a `transactionId`, an `amount`, and an `assetCode`. When route validation (`accounting.validateRoutes`) is enabled on the ledger and the operation route defines accounting entries, the operation also carries a `routeCode` and a `routeDescription` from the resolved rubric.
With `accounting.validateRoutes` enabled, when an operation route defines an accounting entry, Midaz resolves a rubric for the operation's action and direction. It writes the rubric `code` to the operation's `routeCode` and links the operation with a `transactionId`. Together, these fields give a complete audit trail: transaction → operation → rubric. Your teams trace which accounting rule applied to each movement, without extra lookups.
At a glance, the flow is:
Midaz validates that the original transaction’s source, destination, and send totals agree. Lifecycle responses do not always contain both directions: pending creation and cancellation process only source-side legs. Each movement stays traceable for audit and compliance.
New to accounting in Midaz? Start with the **[Accounting Walkthrough](/en/midaz/accounting-walkthrough)**. It gives a step-by-step path from the chart of accounts to a working Pix payment example.
# Accounting walkthrough
Source: https://docs.lerian.studio/en/midaz/accounting-walkthrough
An end-to-end, hands-on guide to designing and implementing accounting in Midaz — from chart of accounts to a working Pix payment example.
This guide shows you how to implement accounting in Midaz from start to finish. It assumes you are a developer. You want enough accounting context to model a real product, not a full accounting textbook. By the end, you understand how the primitives fit together. You can also wire up a complete Pix payment with correct double-entry postings.
For the conceptual overview and links to each reference page, see **[Accounting](/en/midaz/accounting-in-midaz)**.
## 1. Double-entry accounting fundamentals
***
Midaz uses strict double-entry bookkeeping. The rules are simple but non-negotiable:
* Every transaction **must** have at least one debit and one credit.
* Total debits **must equal** total credits.
* Every movement impacts the ledger in a balanced way.
This guarantees no drift in balances, precise audit trails, and regulation-ready financial statements. In practice, you rarely do double-entry by hand in Midaz. You model your accounts and routing once. The engine enforces balance on every transaction.
Think in terms of *where value comes from* (the debit side / source) and *where it goes* (the credit side / destination). Every Midaz operation lands on one side of that equation.
## 2. Chart of Accounts in Midaz
***
In traditional accounting, the Chart of Accounts (CoA) defines account categories (Assets, Liabilities, Equity, Income, Expenses), their hierarchy, and how to classify movements.
Midaz does not have a dedicated Chart of Accounts API. The CoA is not a resource you create or retrieve. It is the result of how you combine assets, accounts, segments, portfolios, and account types. The `code` field on Accounting Entries (e.g. `1.1.1.001`) is where traditional account numbering appears in practice. Each `code` annotates a posting with its classification.
Midaz lets you mirror or adapt a CoA digitally using a small set of primitives:
* **Assets** define *what* moves — currencies (BRL, USD), points/miles, crypto tokens, or internal units of value — with decimal precision and regulatory metadata.
* **Accounts** are balance containers. Each has an asset code and type; it can also belong to a portfolio and segment. An **alias** (e.g. `@external/BRL`) identifies each account and keeps routing intuitive.
* **Segments** categorize and isolate accounts (customer vs. internal funds, business units, multi-tenant separation).
* **Portfolios** group accounts that share a purpose or belong to the same entity.
To map a CoA in Midaz, you decide which balances you need and classify them with Account Types. You then organize them with segments and portfolios on a ledger, and assign `code` values on your Accounting Entries to match your numbering scheme.
### A recommended process
List the balances you need: customer balances, internal accounts, reserve/settlement accounts, fee and revenue accounts. This is your CoA blueprint.
Create an Account Type per conceptual category — e.g. `CASH`, `SETTLEMENT`, `FEE_REVENUE`, `FEE_EXPENSE`, `TREASURY`.
Segments separate business domains (`CUSTOMER_FUNDS`). Portfolios manage ownership and grouping (`customer_12345_wallet`).
For each logical balance, create a ledger account (customer BRL account, treasury account, provider fee expense account, merchant settlement account).
## 3. Setting up Account Types
***
Account Types classify accounts by nature and purpose. With `validateAccountType` enabled, a new non-external Account's `type` must match a registered `keyValue`; an Operation Route can separately use `ruleType: account_type` to validate an account during route processing. Account Types do not themselves define permitted operations, internal or external status, or reconciliation rules.
A typical setup for a payments product:
* **CASH** → liquid customer funds
* **SETTLEMENT** → funds awaiting clearing
* **FEE\_REVENUE** → fees collected
* **FEE\_EXPENSE** → provider fees
* **TREASURY** → internal operations
To enable Account Type validation, understand the `type` field, and manage Account Types via the API, see **[Account Types](/en/midaz/account-types)**.
### Understanding balance buckets
Before you wire up two-phase flows, understand that each balance tracks funds in distinct fields:
* **`available`** — funds you can spend or send right now. Debits and credits to a balance move this number.
* **`onHold`** — funds that a pending `hold` reserves and does not yet commit. Midaz moves them out of `available`, but they still belong to the account until you commit or cancel the hold.
* **`overdraftUsed`** — overdraft consumed by the balance, when overdraft is enabled.
The three fields carry decimal strings with exact precision (for example, `"12.50"`). There is no separate `scale` field to interpret. See [Transaction amount](/en/midaz/amount) for the decimal value model.
The two-phase actions move value between `available` and `onHold` on the source balance:
| Action | `available` | `onHold` |
| ---------- | ------------------------------------------------ | ------------------------------ |
| **Direct** | Debited (source) / credited (destination) | unchanged |
| **Hold** | ↓ decreased on source | ↑ increased on source |
| **Commit** | credited on destination | ↓ released from source |
| **Cancel** | ↑ returned to source | ↓ released back to `available` |
| **Revert** | restored on both sides via a counter-transaction | unchanged |
For the full balance model — multiple balances per account, permission flags, overdraft, and history — see **[Balances](/en/midaz/balances)**.
## 4. Defining Accounting Entries (Rubricas)
***
**Accounting Entries** (Rubricas) map a transaction action and route direction to an accounting `code` and `description`. You register rubrics once instead of computing accounting classifications by hand for each movement. When `accounting.validateRoutes` is enabled on the ledger and a matching rubric is configured, Midaz annotates each operation with the resulting `routeCode` and `routeDescription`; Operation Route rules and transaction legs determine the participating accounts.
You configure rubrics **per action** on each Operation Route, inside the `accountingEntries` block. For `direct` and `commit` actions, source routes require the **debit** rubric and destination routes require the **credit** rubric. Dedicated `block` and `unblock` rubrics are optional; when they are not configured, Midaz resolves the `direct` rubric for those actions. Source-side `hold` and `cancel` actions require **both** rubrics, `overdraft` requires **both** on every supported route type, and bidirectional routes always require **both**.
### The five transaction-lifecycle actions
| Action | Code | What it does |
| ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Direct** | `direct` | Immediate, single-step posting with one debit and one or more credits, or one credit and one or more debits; no intermediate stages (e.g. a fee or adjustment). |
| **Hold** | `hold` | Reserves funds by creating a pending movement (`available` → `onHold` on the source). |
| **Commit** | `commit` | Confirms a previously held amount, releasing `onHold` to the destination. |
| **Cancel** | `cancel` | Cancels a hold, returning `onHold` value to `available` on the source. |
| **Revert** | `revert` | Reverses an `APPROVED` transaction via a counter-transaction when its Operation Routes are bidirectional. |
Each action can point to different debit/credit accounting classifications within the same rubric. Every stage of an operation then receives the right accounting annotation. You register these mappings through the Operation Route endpoints — see [Create an Operation Route](/en/reference/midaz/create-an-operation-route).
```json theme={null}
{
"accountingEntries": {
"direct": {
"debit": { "code": "1.1.1.001", "description": "Customer cash-out" },
"credit": { "code": "2.1.1.001", "description": "External settlement" }
}
}
}
```
For the full model, see **[Accounting Entries (Rubricas)](/en/midaz/accounting-entries)**.
## 5. Transaction routing
***
Routing is a two-layer system that resolves at runtime:
* **Operation Routes** define the accounting logic for each leg of a transaction: which accounts to debit or credit, balance keys, and validation rules. They carry the **Accounting Entries (rubricas)** above.
* **Transaction Routes** define the business event that triggers accounting (`PIX_CASH_OUT`, `WALLET_TRANSFER`, `BANK_SLIP_SETTLEMENT`, …) and combine Operation Routes into a balanced financial event.
When you submit a transaction, Midaz resolves the matching Transaction Route. It then resolves each Operation Route and its rubric for the current action. Before it records anything, Midaz runs four checks. It confirms that balances exist, that debits do not exceed the available balance, that assets match, and that the ledger stays balanced.
For route structure, fields, the operation-type validation matrix, and API behavior, see **[Transaction Routing](/en/midaz/transaction-routing-entities)**.
## 6. End-to-end example — a Pix payment
***
Let's tie it together with a simple Pix cash-out: a customer sends BRL out of their wallet to an external account.
Create the customer account on your ledger. Midaz creates `@external/BRL` automatically together with the `BRL` Asset; do not create it yourself.
* `customer_12345_brl` — Account Type `CASH`, asset `BRL`
* `@external/BRL` — the automatically created external settlement account for funds leaving the ledger
Enable `accounting.validateRoutes` on the ledger. Then, on the Operation Route for the customer leg (source), register the `direct` debit rubric. On the external leg (destination), register the `direct` credit rubric:
```json theme={null}
{
"accountingEntries": {
"direct": {
"debit": { "code": "1.1.1.001", "description": "Pix cash-out — customer" },
"credit": { "code": "2.1.1.001", "description": "Pix cash-out — external settlement" }
}
}
}
```
This block is a combined illustration of both rubrics. Register only the `debit` field on the Source route and only the `credit` field on the Destination route.
Submit a transaction against the `PIX_CASH_OUT` Transaction Route. Move, say, `100.00 BRL` from `customer_12345_brl` to `@external/BRL`.
Midaz records two balanced operations:
* **Debit** `customer_12345_brl` `100.00 BRL`, `routeCode: 1.1.1.001`
* **Credit** `@external/BRL` `100.00 BRL`, `routeCode: 2.1.1.001`
Both share the same `transactionId`, giving you a full trail from transaction → operation → rubric.
For a two-phase flow (hold → commit/cancel), register the `hold`, `commit`, and `cancel` rubrics on the route. Submit the corresponding actions. Each stage resolves its own rubric.
## 7. Validation modes
***
Midaz controls route validation with the `accounting.validateRoutes` setting on each ledger. The default is `false`. In this graceful mode, Midaz skips route validation. It leaves the `routeCode` and `routeDescription` fields empty on every operation and raises no error. Graceful mode is convenient while you onboard routes.
In production, set `accounting.validateRoutes` to `true` in the [Ledger Settings](/en/midaz/ledgers#ledger-settings). Strict mode then validates the routes on every transaction:
```json theme={null}
{
"accounting": {
"validateRoutes": true
}
}
```
In strict mode, a requested action with no routes in the transaction-route cache returns `0157 ErrNoRoutesForAction`. `0117 ErrAccountingRouteNotFound` applies when an operation route ID is absent from that cache. When a route resolves, Midaz stamps `routeCode` and `routeDescription` from its rubric.
Use **strict mode** (`validateRoutes: true`) in production ledgers where every transaction type needs an accounting classification. Keep the graceful default only while you onboard routes.
## Next steps
***
* Review the **[Accounting](/en/midaz/accounting-in-midaz)** overview and reference pages for full field-level detail.
* Once your ledger produces structured postings, see **[Lerian Reporter](/en/reporter/what-is-reporter)** to transform ledger events into reconciliation files, financial statements, and COSIF-aligned regulatory outputs.
# Accounts
Source: https://docs.lerian.studio/en/midaz/accounts
Use Accounts as the core financial unit of a Midaz Ledger, tied to a single Asset and recording every debit, credit, and balance for a customer or product.
An Account is the core financial unit of a Midaz Ledger. Each Account links to one Asset and records every debit, credit, and balance for that Asset. In banking terms, an Account is a financial product, such as a checking account, a savings account, or a loan account.
Midaz does not limit how many accounts you create. Create as many as your structure needs.
## Account structure
***
* **Account > Ledger**: You create an Account within a Ledger. The Ledger tracks and consolidates all balances and operations.
* **Account > Portfolio**: You can group Accounts into [**Portfolios**](/en/midaz/portfolios) to represent customer clusters, product lines, or business units.
* **Account > Asset**: Each Account links to a **single Asset**. The Asset defines the type of value the Account holds, such as BRL, USD, BTC, or loyalty points.
* **Account > Account Type**: When you enable Account Type validation, each non-external Account must use a registered Account Type. You register Account Types for your business classification.
## Key characteristics
***
* Each Account links to exactly one Asset type.
* Each Account has a unique identifier within a Ledger.
* Every transaction records debits and credits between Accounts.
## Multiple accounts per customer
***
A single customer often holds more than one balance. Midaz models each balance as its own Account, not as labels on a shared one. The guiding rule: **create a separate account whenever a balance needs its own truth.**
The same customer might hold balances that behave differently:
* **Different nature** — a main balance, a benefit balance, or a promotional balance.
* **Different operational rules** — a court-ordered or blocked account that accepts inflows but restricts outflows.
* **Separate statement and reconciliation** — a product sub-account or pocket that you track on its own.
When the balance, ledger, statement, or rule differs, each one becomes its own Account. You classify it by an [Account Type](/en/midaz/account-types), group it under the customer with a [Portfolio](/en/midaz/portfolios), and tie it to identity through [CRM](/en/midaz/crm/crm-overview). One account with labels works until the balances diverge. Separate accounts keep each balance accurate from the start.
For the full reference architecture — one customer, many accounts, with step-by-step examples — see [Midaz for multi-account customers](/en/midaz/midaz-for-multi-account-customers).
## External Account
***
External Accounts in Midaz represent accounts outside your organization's structure. They track money that enters or leaves your ledger, usually to and from users, partners, or financial providers.
External accounts have these characteristics:
* **Hold the counterparty balance** for money that enters or leaves your ledger.
* **Can represent an external negative position.** When an external account uses overdraft, its derived position can be negative; the persisted `Available` balance stays at zero and Midaz tracks usage in `OverdraftUsed`.
* **The Ledger creates a canonical external Account automatically** when you create an Asset.
* **The canonical external Account follows a clear naming pattern**: `@external/`, such as `@external/BRL`.
In practice, these accounts act as the bridge between your system and the outside world. They record inflows and outflows at the ledger boundary.
**Do not try to delete or change an external account.** Midaz blocks these operations to keep the Ledger accurate and traceable.
### External account codes
The canonical external Account created with an Asset follows the naming pattern `@external/`. The asset code in that alias acts as the lookup key. You can retrieve this canonical Account and its balances with convenience endpoints that accept only the asset code:
* `GET .../accounts/external/{code}` — Retrieve the external account for an asset code (for example, `BRL` resolves to `@external/BRL`).
* `GET .../accounts/external/{code}/balances` — Retrieve the balances for that external account.
These endpoints are shortcuts for the canonical external Account. They prepend `@external/` to the code you provide, then perform an alias-based lookup. The result is identical to a query by that full alias.
### Entity ID (external system reference)
The `entityId` field exists on any account, not only external accounts. It links the account to a record in an external system, such as a core banking platform, a CRM, or a partner system.
* **Not the same as alias**: You use the alias in transactions, and it must be unique within a ledger. The `entityId` is only a reference for your integration, and Midaz does not use it to move value.
* **Optional**: Set it when you create the account or when you update it. The maximum length is 256 characters.
* **Use case**: When your system already has an account identifier, such as `EXT-ACC-12345`, store it in `entityId`. You can then map between Midaz and your source of truth.
```json JSON theme={null}
{
"name": "User Checking Account",
"assetCode": "BRL",
"alias": "@user/checking_123",
"entityId": "EXT-ACC-12345",
"type": "checking"
}
```
## Parent Account ID
***
The **Parent Account ID** links two accounts within Midaz. You define the relationship based on your business logic.
You can use it for a traditional parent-child structure or for another relationship that your business needs.
## Account aliases
***
An alias replaces a complex account ID with a readable label. This makes accounts easier to identify.
* **For example**: Instead of the ID `3172933b-50d2-4b17-96aa-9b378d6a6eac`, you can use `@username_1`.
### Use the Account Alias in Transactions
When you create a transaction, always use the **account alias** in the `account` field. Do not use the account ID.
An alias is **optional** when you create a non-external Account. If you skip it, Midaz uses the account ID as the alias. A user-created external Account requires an alias. Every account then has a unique alias.
## Managing Accounts
***
You can manage your Accounts through the API or the Lerian Console.
### Via API
* [Create an Account](/en/reference/midaz/create-an-account) — Open a new Account linked to an Asset.
* [List Accounts](/en/reference/midaz/list-accounts) — View all Accounts in your workspace.
* [Retrieve an Account](/en/reference/midaz/retrieve-an-account) — Get details of a specific Account.
* [Retrieve an Account by Alias](/en/reference/midaz/retrieve-an-account-by-alias) — Get details of a specific Account by its alias.
* [Retrieve an External Account](/en/reference/midaz/retrieve-an-external-account) — Get details of a specific External Account by its asset code.
* [Update an Account](/en/reference/midaz/update-an-account) — Edit the metadata or settings of an existing Account.
* [Delete an Account](/en/reference/midaz/delete-an-account) — Delete a specific Account.
**Transfer any remaining balance to another account before you delete an account.** Midaz does not delete an account or sub-account that still holds a balance.
### Via Lerian Console
You can view, create, edit, and delete Accounts on the Accounts page. This page is in the Midaz module of the Lerian Console.
[**Learn more in the Managing Accounts guide.**](/en/midaz/console/managing-accounts)
# Accounts
Source: https://docs.lerian.studio/en/midaz/accounts-overview
Learn how to create, group, and classify Midaz Accounts using Portfolios and Segments to match your customers, product lines, and business unit structure.
Accounts are the core financial units in Midaz. This section covers how to create, organize, and classify Accounts. You group them to match your business structure, from individual balances to customer-level groupings.
## Content
***
Here, you'll find:
* [**Accounts**](/en/midaz/accounts): The core financial unit in a Ledger. Each Account uses one Asset and records its debits, credits, and balances. Midaz also supports External Accounts to track value that moves in or out of your organization.
* [**Portfolios**](/en/midaz/portfolios): Groups of Accounts that belong to one entity, such as a customer or business unit. A Portfolio works like a wallet that holds that entity's Accounts across different Assets.
* [**Segments**](/en/midaz/segments): A classification for Accounts that share characteristics. You group Accounts into Segments to organize them by tier or type, such as premium card tiers or student accounts.
# Transaction amount
Source: https://docs.lerian.studio/en/midaz/amount
Use the transaction field that belongs to the endpoint version. Midaz does not use one global amount field across every transaction API.
Amount fields apply to transaction-creation bodies, not every transaction endpoint. v1 creation payloads use decimal `value`; the API accepts a JSON number or string, although a string avoids client-side IEEE-754 rounding. v2 `direct`, `hold`, `block`, and `unblock` creation endpoints require string `amount`. Commit, cancel, and revert are ID-only lifecycle endpoints and have no amount request body.
Do not combine field conventions from different transaction versions. Use the API reference for the specific operation you call.
# Architecture
Source: https://docs.lerian.studio/en/midaz/architecture
Explore Midaz's modular architecture, centered on the Ledger service and its supporting infrastructure.
**In a nutshell**: Midaz runs the modular Ledger service and a separate Tracer service. Deploy and scale each service and its supporting infrastructure for your environment. For a business-oriented overview, see [About Midaz](/en/midaz/about-midaz).
Midaz can run in public or private cloud environments. Select and operate the infrastructure that fits your deployment requirements.
Under the hood, Midaz uses a **modular architecture**. Ledger is organized around domain boundaries, with supporting infrastructure for storage, caching, messaging, and streaming; Tracer runs as a separate service.
## Modular architecture
***
The diagram below (*Figure 1*) shows how the pieces fit together.
### 1. Cluster layer (customer management)
The customer manages this layer. It provides the Kubernetes foundation that runs Midaz. Enable these services for a smooth and secure deployment:
* **Service**: Networking abstraction to expose and route services.
* **AutoScaler**: Automatically adjusts the number of pods.
* **HPA (Horizontal Pod Autoscaler)**: Scales applications based on metrics.
* **Secrets**: Secure management of sensitive data.
* **ConfigMap**: Externalized configuration management.
This layer governs and coordinates all components within the cluster.
### 2. Front-end layer
The front-end layer provides the user interface for system interaction (admin panel, dashboards, and more).
**Important transition**: newer releases of Midaz no longer include a bundled console in the repository. **[Lerian Console](/en/platform/console/about-lerian-console)** now provides the visual interface. It is a unified platform that includes Midaz as one product module.
If you use an older version of Midaz, you may still have the standalone **Midaz Console**. This component is now in maintenance mode. It receives critical security patches only. Migrate to Lerian Console for the latest features and improvements.
### 3. Back-end layer
The Ledger back-end is a **modular monolith** that holds the core responsibilities of the Midaz ledger. Midaz also runs Tracer as a separate service.
Ledger brings together four core components:
* **Onboarding domain**: manages organizations, ledgers, assets, portfolios, segments, and accounts.
* **Transactions domain**: handles financial transactions, operations, and asset movements.
* **CRM**: is embedded in the Ledger process.
* **Fees**: is embedded in the Ledger process.
This approach keeps clear domain boundaries. It also simplifies deployment and lowers operational complexity.
### 4. Data layer
This layer holds the databases and infrastructure components that support application data and event flows:
* **PostgreSQL**: separate `onboarding` and `transaction` databases, each configured with primary and replica connections.
* **Valkey**: A Redis-compatible key-value store for caching or fast data retrieval.
* **MongoDB**: Stores unstructured and semi-structured data, such as entity metadata.
* **RabbitMQ**: Message broker for asynchronous communication between services.
* **Redpanda**: Kafka-compatible broker for Ledger event streams. Ledger publishes configured events to it only when `STREAMING_ENABLED=true` and streaming brokers are configured.
Persistence is deployment-specific. The bundled Compose configuration does not mount a data volume for Redpanda; configure production storage and retention for each service.
### 5. Helm Chart layer
Midaz uses **Helm Charts** (specifically `lerianstudio/helm`) to deploy and configure the infrastructure.
The chart connects the code to Kubernetes. It deploys each component the same way across environments.
For more details, see the [Deploying using Helm](/en/platform/helm/midaz/midaz-installation) page.
## How the layers work together
***
Each layer has a focused role, with no unnecessary complexity. The system scales and adapts as your needs change.
* Kubernetes coordinates how the layers communicate, for secure and reliable interactions across the system.
* Helm keeps deployments and configurations in sync, so scaling and updates stay straightforward.
Because the platform is modular, extending it is simple. You can add new components as needed without disruption to what already runs.
# Assets
Source: https://docs.lerian.studio/en/midaz/assets
Define the currencies, cryptocurrencies, and tokenized instruments your Accounts hold — multi-asset support across a single Ledger.
Assets represent the financial instruments or currencies that Accounts hold. Each Account holds exactly one Asset.
Assets can include:
* Currencies (e.g., BRL, USD, EUR)
* Cryptocurrencies (e.g., BTC, ETH)
* Non-currency assets (e.g., gold, loyalty points, tokenized securities)
Midaz keeps each Asset independent within the Ledger. Balances stay clearly defined.
### Multi-Asset
Finance today spans more than one currency or asset class. Midaz supports traditional currencies, cryptocurrencies, commodities, and tokenized assets in one system. Use it for multi-currency wallets, cross-border payments, or digital asset portfolios.
## Asset structure
***
* **Asset > Account**: Each Account holds a single Asset.
* **Asset > Ledger > Account**: You define Assets at the Ledger level. You create Accounts from the available Assets.
## Key characteristics
***
* You must define each Asset before you use it.
* Each Asset has a unique identifier, such as a currency code.
* Midaz stores asset rates, but the Ledger transaction flow does not use them to convert values. The source and destination assets of a transaction must match.
When you create an Asset, Midaz automatically creates an [External Account](/en/midaz/accounts) if one does not exist. The External Account backs every transaction that enters or leaves the Ledger.
The External Account uses the naming convention `@external/`, for example, `@external/BRL`.
## Managing Assets
***
You can manage Assets through the API or the Lerian Console.
### Via API
* [List Assets](/en/reference/midaz/list-assets) — View all Assets in your workspace.
* [Retrieve an Asset](/en/reference/midaz/retrieve-an-asset) — Get details of a specific Asset.
* [Create an Asset](/en/reference/midaz/create-an-asset) — Register a new Asset in the Ledger.
* [Update an Asset](/en/reference/midaz/update-an-asset) — Edit the properties of an existing Asset.
* [Delete an Asset](/en/reference/midaz/delete-an-asset) — Mark an Asset as deleted so it can no longer be used.
**Important**
Do not use an External Account as an asset-retirement destination: deleting an Asset soft-deletes its External Account(s) and does not migrate or clear balances. Midaz currently has no balance-migration workflow in asset deletion; define and execute an approved retirement or settlement process before deletion.
### Via Lerian Console
You can view, create, edit, and delete Assets on the Assets page in the Midaz module of the Lerian Console.
[**Learn more in the Managing Assets guide.**](/en/midaz/console/managing-assets)
# Async transaction processing
Source: https://docs.lerian.studio/en/midaz/async-transaction-processing
Async transaction processing validates the call fast, then persists the write through RabbitMQ, for lower API latency and higher throughput.
## Why this matters
***
A client sends a transaction. Midaz must do two things: validate it and persist the result. In synchronous mode, both steps run in the same request. The client waits for every write to reach the database before it gets a response.
This model is simple and predictable, but it has a ceiling. At high volume, database writes become the bottleneck. Each transaction holds a connection, waits for locks, and competes for I/O.
Async mode breaks that dependency. Midaz validates the transaction, returns the response at once, and persists the data in the background through RabbitMQ. The client gets faster responses. With async processing enabled, the Bulk Recorder batches inserts by default; set `BULK_RECORDER_ENABLED=false` to persist queued messages individually.
For broader scaling guidance, see [Scalability strategies](/en/midaz/scalability-strategies).
## How it works
***
### Synchronous mode (default)
Midaz validates the transaction and writes it directly to PostgreSQL in the same request cycle. The API sends its response only after every database operation completes.
Here's the full flow, step by step:
1. **The client sends** a `POST /transaction` to the Midaz API.
2. **The API validates the request** — it runs request-shape validation, balance checks, and limit enforcement here.
3. **The API writes to PostgreSQL** — it persists the transaction and its operations in the same request cycle.
4. **PostgreSQL confirms the write** — it commits all records.
5. **The API returns `201 Created`** to the client with the created transaction. The response leaves the server only after the database confirms everything. The response carries the transient `CREATED` status; Midaz promotes the transaction to `APPROVED` asynchronously after balance processing. Do not treat the `201` as final approval; wait for the status to reach `APPROVED` before you treat the transaction as settled.
**Characteristics:**
* Response time includes database write latency.
* Each transaction is an independent database operation.
* Simpler to reason about. The response shows exactly what Midaz persists.
Even in synchronous mode, Midaz updates balances atomically in Redis during the request. Redis is authoritative for balances. The write above persists the transaction and its operations, not the Postgres balance rows. The always-on balance-sync worker reconciles those rows (see [Balance synchronization](#balance-synchronization)).
### Asynchronous mode
Midaz validates the transaction the same way. Instead of a direct database write, Midaz publishes a message to RabbitMQ. A background consumer picks up the message and handles persistence separately.
Here's the full flow, step by step:
1. **The client sends** a `POST /transaction` to the Midaz API.
2. **The API validates the request** — it runs request-shape validation, balance checks, and limit enforcement exactly as in synchronous mode.
3. **The API publishes the transaction payload to RabbitMQ** instead of a direct database write.
4. **The API returns `201 Created`** to the client as soon as the queue accepts the message — the client does not wait for database persistence. The response carries the transient `CREATED` status; Midaz promotes the transaction to `APPROVED` asynchronously after balance processing. Do not treat the `201` as final approval; wait for the status to reach `APPROVED` before you treat the transaction as settled.
5. **RabbitMQ delivers the message** to a background consumer, decoupled from the API request.
6. **The consumer writes to PostgreSQL** — it persists the transaction and its operations from the queued message. The balance-sync worker coordinates balance updates and keeps balances consistent in both modes (see the **Balance synchronization** section).
**Characteristics:**
* Response time excludes database write latency — the client waits only for validation and queue publish.
* Midaz serializes messages with MessagePack for compact, efficient transport.
* Background consumers write to the database at their own pace, with retries; batch inserts require an enabled Bulk Recorder.
The validation step is identical in both modes. Balance checks, request-shape validation, and limit enforcement — all of that happens before the API responds, regardless of processing mode. The difference is only in *when* the data hits the database.
## Built-in resilience
***
If RabbitMQ is unavailable when async mode tries to publish a message, Midaz attempts a direct database write. If that write fails, Midaz returns the database error.
This means:
* During a queue outage, Midaz attempts to write directly to the database.
* The client can receive an error if the fallback database write fails.
* Midaz logs the queue failure and, if the direct write also fails, the fallback-write failure so your operations team can investigate them.
During a queue outage, latency can increase because writes go straight to the database. Monitor your RabbitMQ health to keep async mode active.
## Enabling async mode
***
Set one environment variable in the ledger application:
```bash Environment variable theme={null}
RABBITMQ_TRANSACTION_ASYNC=true
```
With `false` (the default), all transactions use synchronous processing and persist directly to PostgreSQL. The current Ledger bootstrap still initializes RabbitMQ and wires its consumer.
With `true`, the ledger publishes transaction payloads to the configured RabbitMQ exchange. A background consumer then handles persistence.
## RabbitMQ configuration
***
Async mode uses the following RabbitMQ settings (all in the ledger `.env`):
| Variable | Description | Default |
| :------------------------------------------------ | :-------------------------------------- | :--------------------------------------------------- |
| `RABBITMQ_TRANSACTION_ASYNC` | Enable async processing. | `false` |
| `RABBITMQ_HOST` | RabbitMQ server hostname. | `midaz-rabbitmq` |
| `RABBITMQ_PORT_HOST` | AMQP protocol port. | `3003` |
| `RABBITMQ_PORT_AMQP` | Management API port. | `3004` |
| `RABBITMQ_DEFAULT_USER` | Producer credentials (user). | `transaction` |
| `RABBITMQ_DEFAULT_PASS` | Producer credentials (password). | — |
| `RABBITMQ_CONSUMER_USER` | Consumer credentials (user). | `consumer` |
| `RABBITMQ_CONSUMER_PASS` | Consumer credentials (password). | — |
| `RABBITMQ_NUMBERS_OF_WORKERS` | Number of consumer worker goroutines. | `5` |
| `RABBITMQ_NUMBERS_OF_PREFETCH` | Messages prefetched per worker. | `10` |
| `RABBITMQ_TRANSACTION_BALANCE_OPERATION_EXCHANGE` | Exchange name for transaction messages. | `transaction.transaction_balance_operation.exchange` |
| `RABBITMQ_TRANSACTION_BALANCE_OPERATION_KEY` | Routing key. | `transaction.transaction_balance_operation.key` |
| `RABBITMQ_TRANSACTION_BALANCE_OPERATION_QUEUE` | Queue name. | `transaction.transaction_balance_operation.queue` |
The consumer uses separate credentials (`RABBITMQ_CONSUMER_USER` / `RABBITMQ_CONSUMER_PASS`) from the producer. This follows the principle of least privilege — the consumer only needs read access to the queue.
## Balance synchronization
***
A dedicated balance-sync worker coordinates balance updates. It uses Redis as a coordination layer. This worker runs in both synchronous and asynchronous modes. It keeps balances consistent even when multiple consumers process messages at the same time.
| Variable | Description | Default |
| :------------------------------ | :---------------------------------------------------------- | :------ |
| `BALANCE_SYNC_BATCH_SIZE` | Number of balance updates to batch before flushing. | `50` |
| `BALANCE_SYNC_FLUSH_TIMEOUT_MS` | Maximum wait time (ms) before flushing an incomplete batch. | `500` |
| `BALANCE_SYNC_POLL_INTERVAL_MS` | How often (ms) the worker checks for pending updates. | `50` |
The balance-sync worker runs automatically in both modes. You need no extra setup beyond an available Redis instance.
## RabbitMQ circuit breaker
***
When you enable async mode, Midaz depends on RabbitMQ for transaction persistence. A built-in circuit breaker protects against broker outages. It monitors the health of the RabbitMQ connection and fails fast when the broker goes down. This prevents request pileups and cascading failures.
The circuit breaker is active on the single-tenant RabbitMQ path. Multi-tenant RabbitMQ uses tenant connection management instead. The circuit breaker follows the standard three-state model:
* **Closed** (normal): requests flow through to RabbitMQ. The breaker counts failures.
* **Open** (tripped): the breaker does not contact RabbitMQ. Midaz bypasses the broker and attempts a direct database write for each async transaction; if that write fails, Midaz returns the database error. A background health checker monitors the broker and attempts recovery.
* **Half-open** (probing): the breaker lets a limited number of requests through to test RabbitMQ recovery. If they succeed, the circuit closes. If they fail, it reopens.
The circuit opens when either condition is true:
* The number of consecutive failures reaches the threshold, OR
* The failure ratio exceeds the configured percentage within the counting window
### Circuit breaker configuration
| Variable | Description | Default |
| :----------------------------------------------- | :------------------------------------------------------------------------------- | :------ |
| `RABBITMQ_CIRCUIT_BREAKER_CONSECUTIVE_FAILURES` | Consecutive failures before the circuit opens. | `15` |
| `RABBITMQ_CIRCUIT_BREAKER_FAILURE_RATIO` | Failure percentage (0–100) that triggers open state. | `50` |
| `RABBITMQ_CIRCUIT_BREAKER_MIN_REQUESTS` | Minimum requests before evaluating the failure ratio. | `10` |
| `RABBITMQ_CIRCUIT_BREAKER_INTERVAL` | Time window (seconds) for counting failures. Counters reset after each interval. | `120` |
| `RABBITMQ_CIRCUIT_BREAKER_TIMEOUT` | How long (seconds) the circuit stays open before transitioning to half-open. | `30` |
| `RABBITMQ_CIRCUIT_BREAKER_MAX_REQUESTS` | Requests allowed through in half-open state to probe recovery. | `3` |
| `RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_INTERVAL` | How often (seconds) the background health checker pings RabbitMQ. | `30` |
| `RABBITMQ_CIRCUIT_BREAKER_HEALTH_CHECK_TIMEOUT` | Timeout (seconds) for each health check ping. | `10` |
When the circuit is open, Midaz attempts direct database writes for async transactions. If a direct write fails, Midaz returns the database error; the fallback does not guarantee transaction delivery during broker outages.
For most production deployments, the defaults work well. Tune `CONSECUTIVE_FAILURES` and `TIMEOUT` if your RabbitMQ cluster has known recovery patterns. For example, lower the timeout if your broker recovers within seconds. Increase consecutive failures if you see transient network blips.
## How async mode connects to Bulk Recorder
***
Async mode and the [Bulk Recorder](/en/midaz/bulk-recorder) are complementary features that work together:
1. **Async mode** decouples the API response from persistence — transactions go to RabbitMQ instead of directly to PostgreSQL.
2. **Bulk Recorder** optimizes how the consumer writes those messages to the database — it batches multiple messages into single bulk inserts.
Bulk Recorder is active with async mode unless you explicitly set `BULK_RECORDER_ENABLED=false`. Without async mode, transaction persistence uses direct database writes and there is no transaction queue to batch.
| Configuration | Processing behavior |
| :-------------------------------------------- | :-------------------------------------------------- |
| Async `false` | Direct database write per transaction (synchronous) |
| Async `true`, Bulk Recorder `false` | Queue-based, one message processed at a time |
| Async `true`, Bulk Recorder enabled (default) | Queue-based, messages batched for bulk inserts |
## When to use async mode
***
**Use async mode when:**
* You need lower API response times for transaction creation.
* Your workload involves high transaction volumes (hundreds+ per second).
* You run batch operations like mass payouts or settlements.
* You want to decouple your API tier from database performance.
**Keep synchronous mode when:**
* You need direct, request-bound transaction persistence.
* Transaction volume is low to moderate.
* You want a successful API response to mean that Midaz already persisted the data.
* You work in a development or testing environment where simplicity matters more than throughput.
You can switch between modes at any time. Change `RABBITMQ_TRANSACTION_ASYNC` and restart the ledger application. You need no data migration, because the transaction format is the same in both paths.
# Balance Overdraft
Source: https://docs.lerian.studio/en/midaz/balance-overdraft
Enable controlled Balance Overdraft in Midaz with automatic split operations, credit repayment priority, and limits for BNPL or settlement accounts.
Balance Overdraft lets you debit a balance beyond its available funds. The primary balance never goes negative. Midaz tracks the deficit as **OverdraftUsed** and splits the operation between the primary balance and an internal companion balance. When credits arrive, Midaz repays the overdraft first. Any remainder flows to Available.
This mechanism supports credit lines, BNPL, settlement accounts, earned wage access, and any product that needs controlled negative positions.
## Balance direction
***
Balances carry a `direction` field that defines how debits and credits affect the balance:
| Direction | Behavior | Typical use |
| --------- | --------------------------------- | ------------------------------------ |
| `credit` | Debit decreases, credit increases | Checking accounts, wallets, reserves |
| `debit` | Debit increases, credit decreases | Loans, overdraft tracking, payables |
At creation, Midaz uses an explicit `direction` when provided, then the Account Type's `defaultDirection`. If neither is set, external Accounts use `debit`; all other Accounts use `credit`.
You set direction at creation time. It is **immutable**. The overdraft companion balance (described below) always uses `direction=debit`.
## Balance settings
***
The `settings` object on a balance controls overdraft behavior:
| Field | Type | Description |
| ----------------------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| `allowOverdraft` | boolean | Enables overdraft on this balance |
| `overdraftLimitEnabled` | boolean | Gates whether a limit is enforced |
| `overdraftLimit` | string (decimal) | Maximum overdraft amount. Required when `overdraftLimitEnabled` is `true`. Must be greater than `0`. |
The `settings` object also carries `balanceScope`. It identifies a transactional balance (the default) or a system-managed internal balance, such as the overdraft companion. You can set `balanceScope: "transactional"` when you create or update a public balance. You cannot set `balanceScope: "internal"` through the public API.
## Configuration modes
***
### No overdraft (default)
The standard behavior. Midaz rejects any debit that exceeds the available balance.
```json JSON theme={null}
{
"key": "checking",
"assetCode": "BRL",
"settings": {
"allowOverdraft": false
}
}
```
### Unlimited overdraft
The derived position can go negative without a cap. The persisted `Available` balance remains at `0`, while Midaz tracks the deficit as `OverdraftUsed`. Use this for settlement or pool accounts, where negative positions are normal and you reconcile them externally.
```json JSON theme={null}
{
"key": "settlement",
"assetCode": "USD",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": false
}
}
```
### Limited overdraft
The derived position can go negative up to a defined limit. The persisted `Available` balance remains at `0`, while Midaz tracks the deficit as `OverdraftUsed`. This is the most common mode for consumer credit products.
```json JSON theme={null}
{
"key": "checking",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "5000.00"
}
}
```
When `overdraftLimitEnabled` is `true`, you must set `overdraftLimit` to a positive decimal string. If you omit it or set it to `"0"`, Midaz returns error `0172 - ErrInvalidBalanceSettings`.
## How overdraft works
***
### Operation split
When a debit transaction exceeds the available funds, Midaz automatically splits the operation:
1. The debit consumes all remaining Available and floors it at **0**.
2. Midaz accrues the excess as **OverdraftUsed** on the primary balance.
3. If Midaz finds the internal `"overdraft"` balance (described below), it creates a companion operation. This operation records the liability as a double-entry debit. If it cannot find that balance, Midaz skips the companion operation; the primary balance still accrues **OverdraftUsed**.
**Example:** Balance has Available = 300. A debit of 500 arrives.
| Step | Available | OverdraftUsed | Description |
| ------ | --------- | ------------- | ----------------------------------------------------- |
| Before | 300 | 0 | Normal state |
| After | 0 | 200 | 300 consumed from Available, 200 accrued as overdraft |
The transaction succeeds as a single atomic operation. The caller does not need to handle the split — Midaz does it automatically.
If you configure a limit, Midaz checks the resulting OverdraftUsed against `overdraftLimit` **before** it processes the transaction. If the result exceeds the limit, Midaz rejects the transaction with error `0167 - ErrOverdraftLimitExceeded`.
### Automatic repayment (refund split)
When a credit arrives and `OverdraftUsed > 0`, Midaz prioritizes repayment:
1. Midaz applies the credit to **OverdraftUsed** first and reduces the debt.
2. Any remaining amount after OverdraftUsed reaches 0 flows to **Available**.
3. If Midaz finds the internal `"overdraft"` balance, a companion operation on it records the repayment. If it cannot find that balance, Midaz skips the companion operation; the credit still repays **OverdraftUsed** on the primary balance.
**Example:** OverdraftUsed = 200, Available = 0. A credit of 350 arrives.
| Step | Available | OverdraftUsed | Description |
| ------ | --------- | ------------- | --------------------------------- |
| Before | 0 | 200 | Overdraft active |
| After | 150 | 0 | 200 repaid, 150 goes to Available |
Repayment is automatic. You cannot bypass it. Midaz reduces overdraft positions as early as possible, which keeps the balance healthy.
### Cancelling a pending overdraft transaction
When you cancel a `PENDING` transaction that drew overdraft:
1. The cancel reverses the original hold and any overdraft drawn during the pending window. `OverdraftUsed` returns to its value before the hold.
2. If Midaz finds the internal `"overdraft"` balance, a companion `CREDIT` operation on it shrinks the liability by the exact amount drawn. If it cannot find that balance, Midaz skips the companion operation; the primary cancel still restores `OverdraftUsed`.
3. When Midaz creates the companion credit, it applies the primary cancel and the companion credit in the **same atomic batch**, so the two balances do not drift.
While the internal `"overdraft"` balance exists, Midaz keeps it in step with the primary balance across the hold, commit, and cancel phases of any pending transaction that touches overdraft.
## Position
***
Every balance response includes a computed `position` block. It gives a real-time view of the balance state:
| Field | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `available` | Derived `position.available`. It can be negative when the persisted balance has `Available = 0` and `OverdraftUsed > 0`. |
| `onHold` | Mirrors `Balance.OnHold` — funds reserved by pending operations. |
| `overdraftLimitAvailable` | Remaining non-negative overdraft headroom. It is `"0"` when a configured limit is fully used and is omitted when overdraft is unlimited. |
Do not cache the `position` block for accounting purposes. Midaz never persists it — it computes the block at query time from the current balance state.
## Companion balance
***
When you update a balance to set `allowOverdraft` to `true` for the first time, Midaz auto-provisions a **companion balance** under the same account. The companion balance records the liability side of the double-entry. Midaz creates it once per account and reuses it across every overdraft draw and repayment.
| Property | Value | Why |
| ---------------- | ------------- | ---------------------------------------------------------------------- |
| `key` | `"overdraft"` | Reserved system key |
| `direction` | `debit` | The companion tracks a liability — debits grow it, credits shrink it |
| `scope` | `internal` | Blocks direct user operations |
| `allowSending` | `true` | Required for DEBIT operations on the companion (overdraft draws) |
| `allowReceiving` | `true` | Required for CREDIT operations on the companion (overdraft repayments) |
This balance is **fully system-managed**:
* You cannot create, modify, or delete it through the public API.
* Midaz **reserves** the key `"overdraft"`. A request that creates a balance with this key returns error `0170 - ErrReservedBalanceKey`.
* It mirrors the liability as a proper double-entry record, so the ledger stays balanced.
The `scope: "internal"` value blocks direct user operations, regardless of the permission flags above. Midaz rejects any direct operation on this balance with error `0168 - ErrDirectOperationOnInternalBalance`. The companion moves only through system-driven overdraft enrichment.
## Overdraft state on operations
***
Every operation exposes the overdraft state on the `balance` and `balanceAfter` blocks. The `overdraftUsed` field records the overdraft consumed before and after the operation. This gives a complete audit trail without a separate balance query.
For operations that do not touch overdraft, both values are `"0"`.
System-managed companion operations on the `"overdraft"` balance use `type: "OVERDRAFT"` (uppercase). The `direction` field carries the lifecycle: `"debit"` for a draw, `"credit"` for a repayment.
```json Primary debit drawing overdraft theme={null}
{
"type": "DEBIT",
"direction": "debit",
"amount": { "value": "500" },
"accountAlias": "@user123",
"balanceKey": "checking",
"balance": {
"available": "300",
"onHold": "0",
"version": 1,
"overdraftUsed": "0"
},
"balanceAfter": {
"available": "0",
"onHold": "0",
"version": 2,
"overdraftUsed": "200"
}
}
```
```json Companion overdraft draw theme={null}
{
"type": "OVERDRAFT",
"direction": "debit",
"amount": { "value": "200" },
"balanceKey": "overdraft",
"balance": {
"available": "0",
"onHold": "0",
"version": 1,
"overdraftUsed": "0"
},
"balanceAfter": {
"available": "200",
"onHold": "0",
"version": 2,
"overdraftUsed": "200"
}
}
```
Both the primary and the companion operation share the same `overdraftUsed` before/after pair. They mirror the primary balance's overdraft transition, so the lifecycle is visible from either row. The internal `snapshot` JSONB column on the `operations` table stores the same values for indexing and historical reconstruction. This column is not part of the public JSON wire. The values surface on `balance.overdraftUsed` and `balanceAfter.overdraftUsed` instead. Midaz can add future system-generated context to the snapshot without breaking the public contract.
Companion operations inherit the primary operation's `routeId`. For each overdraft-capable route, configure both the `debit` and `credit` rubrics of the `overdraft` entry; Midaz requires both. Midaz resolves `routeCode` and `routeDescription` from the rubric that matches the companion's direction.
## Overdraft events
***
At runtime, Midaz enables overdraft-event publication unless `RABBITMQ_OVERDRAFT_EVENTS_ENABLED` is explicitly `false`. The bundled example environment sets the flag to `false`; a deployment that starts from that example publishes no overdraft events until you set it to `true`.
```bash Environment theme={null}
# The bundled example disables overdraft-event publication. Runtime enables it unless the flag is explicitly false.
RABBITMQ_OVERDRAFT_EVENTS_ENABLED=false
# Optional: route overdraft events to a dedicated exchange.
# When unset, the broker's default exchange is used.
RABBITMQ_OVERDRAFT_EVENTS_EXCHANGE=transaction.overdraft_events.exchange
```
### Event types
| Event | Description |
| ------------------- | ------------------------------------------------------------------------ |
| `overdraft.drawn` | Overdraft was consumed — OverdraftUsed increased |
| `overdraft.repaid` | Overdraft was partially repaid — OverdraftUsed decreased but remains > 0 |
| `overdraft.cleared` | Overdraft was fully repaid — OverdraftUsed reached 0 |
### Example event payload
```json JSON expandable theme={null}
{
"source": "midaz",
"eventType": "balance",
"action": "overdraft.drawn",
"timestamp": "2026-04-28T14:30:00.000000Z",
"version": "v3.0.0",
"organizationId": "0198575d-f9fd-702b-bb15-fa4c980b32c7",
"ledgerId": "0198575d-fa0b-7ac7-8b7d-9d3ab7dccafc",
"payload": {
"accountId": "0198575f-a8f9-7924-a6d7-8122f2c77ddd",
"transactionId": "019b2c3d-4e5f-6789-0123-456789abcdef",
"amount": "200",
"overdraftBalance": "200",
"timestamp": "2026-04-28T14:30:00.000000Z"
}
}
```
Use overdraft events to trigger downstream workflows — interest accrual, customer notifications, risk alerts, or automatic collection processes.
## Use cases
***
### Checking account overdraft (cheque especial)
Classic consumer credit. The checking account's derived position can go negative up to a pre-approved limit; the persisted `Available` balance remains at `0` and the outstanding amount is tracked as `OverdraftUsed`.
```json JSON theme={null}
{
"key": "checking",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "2000.00"
}
}
```
### Buy Now, Pay Later (BNPL)
A BNPL provider issues a purchase credit against the customer's balance. This creates an immediate overdraft position that the customer repays in installments.
```json JSON theme={null}
{
"key": "bnpl",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "10000.00"
}
}
```
### Earned Wage Access / Salary advance
Employees draw against future earnings. Payroll credits clear the overdraft position when they arrive.
```json JSON theme={null}
{
"key": "salary-advance",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "3000.00"
}
}
```
### Marketplace receivables advance
Sellers receive an advance on future receivables. Midaz repays the overdraft automatically as sales settlements arrive.
```json JSON theme={null}
{
"key": "receivables",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "50000.00"
}
}
```
### Settlement / Pool accounts (unlimited mode)
Settlement and pool accounts routinely go negative during intraday processing. Unlimited overdraft avoids artificial rejections while you reconcile the position by end-of-day.
```json JSON theme={null}
{
"key": "settlement-pool",
"assetCode": "USD",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": false
}
}
```
### Revolving credit lines (B2B)
Businesses draw and repay from a revolving credit facility. The overdraft limit represents the total credit line.
```json JSON theme={null}
{
"key": "credit-line",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "500000.00"
}
}
```
### Insurance pre-financing
Insurers pre-finance claims before premium collection cycles close. The overdraft covers the gap between payout and collection.
```json JSON theme={null}
{
"key": "claims-prefin",
"assetCode": "BRL",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "100000.00"
}
}
```
### Loyalty programs (advanced points)
Customers redeem points before they earn them. The overdraft tracks the point deficit and clears as customers accrue new points.
```json JSON theme={null}
{
"key": "loyalty-points",
"assetCode": "POINTS",
"settings": {
"allowOverdraft": true,
"overdraftLimitEnabled": true,
"overdraftLimit": "10000"
}
}
```
## Protection rules
***
Overdraft introduces several immutability and access constraints to maintain ledger integrity:
* **Direction is immutable.** Once you set a balance's `direction` at creation, you cannot change it.
* **Internal balances block writes.** You cannot create, delete, or update the `"overdraft"` companion balance through the public API — a PATCH returns error `0175`.
* **Reserved keys.** Midaz reserves the key `"overdraft"` for the system-managed companion balance.
* **Disabling overdraft preserves outstanding debt.** You can set `allowOverdraft: false` while `OverdraftUsed > 0` to block future draws, while incoming credits still repay the existing debt.
* **Limit cannot drop below usage.** If `OverdraftUsed = 200`, Midaz rejects `overdraftLimit: "100"` with error `0173`, so repay below the new ceiling first or set a higher limit.
* **Optimistic concurrency.** Balance updates use version-based concurrency control, and Midaz rejects a stale write with error `0174` — retry with the latest version.
For the complete catalog of overdraft-related error codes (0167–0175), see the [Midaz error list](/en/reference/midaz/error-list).
## Next steps
***
* Learn about [Balances](/en/midaz/balances) — the foundation that overdraft builds on.
* Understand [Operations](/en/midaz/operations) to trace how overdraft splits appear in the ledger.
* Set up the [Event Publisher](/en/midaz/event-publisher) to consume overdraft lifecycle events.
* Explore [Transactions](/en/midaz/transactions) for the full picture of double-entry accounting in Midaz.
# Balances
Source: https://docs.lerian.studio/en/midaz/balances
Track multiple Balances per Account to segment funds — investment reserves, credit limits, and operational funds without extra Accounts.
A **Balance** represents the value that a specific account holds in Midaz. It reflects the result of all operations — debits and credits — over time. Each balance belongs to one asset, such as BRL, USD, or BTC.
## Multiple balances
***
A single account can hold several balances. A unique key identifies each one. This lets institutions segment funds without creating multiple accounts for the same customer.
External accounts cannot have multiple balances. **Each external account holds exactly one balance.**
Typical use cases include:
* Investment reserves
* Credit limits
* Collateral (blocked) funds
* Day-to-day operational funds
This approach (*Figure 1*) increases flexibility. It keeps the double-entry model — debit and credit — intact for accounting consistency, traceability, and transparency.
If a transaction does not provide a `balanceKey`, Midaz uses the account's default balance.
### Balance key
A `key` field identifies each balance uniquely within the account.
* **Maximum length**: 100 characters, with no whitespace.
* **Default key**: `"default"`. Midaz creates the default balance automatically when the account is created.
* **Uniqueness**: Each key must be unique per account. A request to create a balance with a key that already exists on the account returns an error.
* **In transactions**: If a transaction does not specify a `balanceKey`, Midaz uses the balance with key `"default"`.
You set the `key` at creation time and cannot change it later. Choose descriptive keys like `"credit"`, `"collateral"`, or `"savings"` to make your balance model self-documenting.
### Permission flags
Each balance has two independent permission flags that control whether it can participate in transactions:
| Flag | Type | Description |
| ---------------- | ------- | --------------------------------------------------- |
| `allowSending` | boolean | Whether funds can be sent **from** this balance |
| `allowReceiving` | boolean | Whether funds can be received **into** this balance |
These flags are **per-balance** — they apply to one balance, not to the whole account. Both default to `true` when you do not set them.
**Common use cases:**
* **Freeze a balance**: Set both `allowSending` and `allowReceiving` to `false` to prevent any movement.
* **Receive-only balance**: Set `allowSending` to `false` to block outbound transfers and still accept inflows.
* **Send-only balance**: Set `allowReceiving` to `false` to prevent new funds from entering this balance.
You can set both flags when you create a balance. You can also update them independently through the [Update a Balance](/en/reference/midaz/update-a-balance) endpoint. If an update request omits a flag, its current value stays unchanged.
Midaz reads permission flags during transaction validation. A PATCH that changes only `allowSending` or `allowReceiving` does not rewrite an existing Valkey entry, so do not assume the immediately next cache-hit transaction will observe the change. Changes never alter operations already processed.
## Usage examples
***
* **User Wallet (BRL)**: A digital wallet that shows an available balance of R\$500.
* *Use case*: Show the balance in a mobile banking app and validate funds before a payment.
* **Settlement Account (USD)**: A liquidity provider account with a USD balance of \$120,000.
* *Use case*: Make sure daily treasury operations keep enough buffer for FX settlements.
* **Blocked Balance (BRL)**: An account balance reserved as collateral.
* *Use case*: Prevent the use of funds until a loan closes or the borrower meets the conditions.
A blocked (collateral) balance restricts funds at the **balance level**. Midaz holds the value in a separate balance and combines it with permission flags to keep the funds unavailable. This differs from a [Block transaction](/en/midaz/transactions#blocking-and-unblocking-funds), which records a ledger movement with `BLOCK`-typed operations. A Block transaction flags funds for reasons like a compliance hold. Use a collateral balance for a standing operational restriction. Use a Block transaction when you need an auditable ledger entry.
## Balance structure
***
* **Balance > Account**: Each Balance belongs to an Account, which holds and moves value.
* **Balance > Asset**: Each Balance uses a specific Asset, such as BRL or BTC.
* **Balance > Ledger**: Balances exist within a Ledger, which enables multi-book environments.
* **Balance > Key**: Each Balance has a unique key within the account (e.g., `default`, `credit`, `collateral`).
*Figure 2* shows an example of the structure.
A balance is more than just a number. It includes metadata about the state of funds, such as pending operations and effective availability.
## Key characteristics
***
* **Real-time tracking**: Midaz updates balances with every confirmed operation.
* **Multiple balances per account**: Accounts can hold several balances, each with its own rules.
* **Single source of truth**: Balances reflect the net sum of all operations on the account.
* **Query by context**: You can list balances within an organization and ledger, retrieve them by account ID or alias, and retrieve an External Account’s balance by asset code. The balance-list endpoints do not filter by a generic asset code or `balanceKey`.
* **Supports external accounts**: You can retrieve balances for internal or external accounts, such as liquidity pools or partners.
## Using balances in transactions
***
The following transaction endpoints accept a `balanceKey` field to specify which balance to use:
* [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json)
* [Create an Inflow Transaction](/en/reference/midaz/create-an-inflow-transaction)
* [Create an Outflow Transaction](/en/reference/midaz/create-an-outflow-transaction)
* [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation)
If a request does not provide a `balanceKey`, Midaz uses the account's default balance.
### New fields in responses
* `balanceKey` - Appears in transactions and operations to show which balance the transaction used.
* `key` - Appears in balances to identify each balance uniquely.
Always use the `balanceKey` consistently across requests and responses. This avoids mismatches when accounts hold multiple balances.
## Cache key changes (Valkey)
***
Balances in the cache (Valkey) include the `balanceKey`.
### Previous format
```json theme={null}
::
```
### New format
```json theme={null}
balance:{transactions}:::#
```
The key carries the `balance:{transactions}:` prefix, and the `balance_key` is appended to the account alias with a `#` separator. A tenant namespace can prefix the key further in multi-tenant deployments.
Update direct Valkey readers to construct `balance:{transactions}:::#`, including `#default` for the default balance. Reads using the previous key format miss the current cache entry.
## Overdraft
***
Balances support **overdraft** — the ability to debit a balance beyond its available funds. When you enable overdraft, Midaz tracks the deficit as `overdraftUsed`. Midaz also handles the operation split and repayment automatically.
Two fields support this feature:
* **`direction`** — For an automatically created default balance, direction is `credit` for non-external accounts and `debit` for External Accounts. Additional balances may set direction at creation; it cannot change afterward.
* **`settings`** — Controls overdraft behavior: `allowOverdraft`, `overdraftLimitEnabled`, and `overdraftLimit`.
Midaz **reserves** the key `"overdraft"` for the system-managed companion balance that records the liability side. A request to create a balance with this key returns an error.
`settings.balanceScope` also distinguishes balances by scope. **Transactional** balances (the default) are user-managed and take part in regular transactions. The system operates **internal** balances exclusively — like the overdraft companion. User transactions cannot target, modify, or delete them through the public API.
For full details on configuration modes, operation splits, automatic repayment, events, and use cases, see [Balance Overdraft](/en/midaz/balance-overdraft).
## Balance history
***
Midaz provides **point-in-time queries** for balances. You can retrieve a balance state at a past timestamp on or after the balance’s creation. If no operation exists before that timestamp, Midaz returns the initial zero state; it returns `404` when the requested timestamp precedes balance creation. This supports auditing, reconciliation, and historical reporting.
### How it works
When you query balance history, Midaz returns historical identity and amount fields. It omits `allowSending`, `allowReceiving`, `deletedAt`, and `metadata`; the current implementation also does not reconstruct historical `direction` or `settings`, and returns `overdraftUsed` as zero. Do not treat it as a full regular-balance response minus permission flags.
**Why does history exclude permission flags?**
`allowSending` and `allowReceiving` are mutable operational settings. You can toggle them at any time without a ledger entry. Balance amounts (`available`, `onHold`) change only from recorded transactions. Permission flags represent the *current* operational state of a balance, not a fact about its past.
Historical audits and reconciliation care about **amounts** at a point in time. Whether sending or receiving worked at a given moment does not matter for audit or reconciliation. Mutable permission state in immutable snapshots would add ambiguity without value.
### Use cases
* **Regulatory auditing**: Prove the exact balance of an account at a specific compliance checkpoint.
* **Reconciliation**: Compare balance snapshots across systems at matching timestamps.
* **Dispute resolution**: Retrieve the precise account state at the time of a contested transaction.
* **End-of-day reporting**: Capture balance positions at market close for treasury operations.
The `date` parameter is required. It must follow the format `yyyy-mm-dd hh:mm:ss` (e.g., `2026-01-15 10:30:00`). Midaz returns `404` when the requested timestamp precedes balance creation.
### Querying balance history
You can query history for a single balance or for all balances of an account:
* [Retrieve Balance History](/en/reference/midaz/retrieve-balance-history) - Get the state of a specific balance at a given point in time.
* [Retrieve Balance History by Account](/en/reference/midaz/retrieve-balance-history-by-account) - Get the state of all balances for an account at a given point in time.
## Managing Balances
***
You can retrieve your balances through the API. The Midaz ledger engine computes balance amounts from transactions — you cannot set `available` or `onHold` directly. You manage the balance records — key, permission flags, and settings — through the endpoints below.
* [Create a Balance](/en/reference/midaz/create-a-balance) - Create a new balance for an account by defining a unique key.
* [List Balances](/en/reference/midaz/list-balances) - Retrieve all balances by organization and ledger.
* [Retrieve a Balance](/en/reference/midaz/retrieve-a-balance) - Get the balance of a specific account by its unique ID.
* [Retrieve Balances by Account](/en/reference/midaz/retrieve-balances-by-account) - Get the balance for a specific account.
* [Retrieve a Balance by Account Alias](/en/reference/midaz/retrieve-a-balance-by-account-alias) - Get the balance with a human-readable account alias (e.g., @user123).
* [Retrieve a Balance of an External Account](/en/reference/midaz/retrieve-a-balance-of-an-external-account) - Retrieve the balance of an external account (e.g., `@external/BRL`).
* [Update a Balance](/en/reference/midaz/update-a-balance) - Update the permission flags and settings of a balance.
* [Delete a Balance](/en/reference/midaz/delete-a-balance) - Delete a balance entry from the system.
Do you want to trace **how** a balance formed? Use the Operations API to inspect the ledger history that affected that account.
## Next steps
***
* Use the [Operations API](/en/midaz/operations) to trace transactions that involve multiple balances.
* Combine multiple balances with [Accounting Routes](/en/midaz/transaction-routing-entities) to create flexible and scalable financial flows.
# Production ready
Source: https://docs.lerian.studio/en/midaz/best-practices
Hub for Midaz production best practices covering common mistakes, scalability strategies, banking entity hierarchy, and hardening for high-volume workloads.
Midaz uses double-entry accounting as a single source of truth for every transaction.
These best practices help you keep financial operations transparent, auditable, and compliant.
## Content
***
This section contains the following pages:
* [**Common mistakes to avoid**](/en/midaz/common-mistakes-to-avoid): The most common mistakes with Midaz and how to avoid them.
* [**Scalability strategies**](/en/midaz/scalability-strategies): How to scale your Midaz deployment for high transaction volumes and multi-currency, multi-entity operations.
* [**Structuring bank operations with Midaz**](/en/midaz/structuring-bank-operation): A strategic approach to entity hierarchy and account structure in a banking environment.
* [**Midaz production best practices**](/en/midaz/midaz-production-best-practices): Recommendations to set up your Midaz production environment from the start. You minimize downtime, protect your data, and handle high-volume workloads.
# Build on Midaz
Source: https://docs.lerian.studio/en/midaz/build-midaz-sdk
Build integrations on Midaz with the official Go and TypeScript SDKs — typed clients, sensible defaults, and shared patterns for API calls and errors.
Midaz SDKs are the public integration layer for applications that call Midaz APIs.
Use an SDK to build an application, automation, backend service, or integration on Midaz. Each SDK works with Midaz resources: organizations, ledgers, accounts, portfolios, balances, transactions, assets, and metadata.
The SDKs give you typed, idiomatic interfaces for the Midaz API. You focus on product behavior, not raw HTTP requests, payloads, pagination, or response parsing.
## Available SDKs
***
We offer two official SDKs:
Use the official Go SDK to build Midaz integrations in Go.
Use the official TypeScript SDK to build Midaz integrations in Node.js or TypeScript applications.
## Why use Midaz SDKs?
***
* Developer-friendly: clean interfaces, strong typing, and sensible defaults.
* Production-ready: shared patterns for API calls, errors, and integration workflows.
* Consistent across stacks: the same integration model runs in Go and TypeScript. Use the language that fits your workflow.
* API-focused: SDKs connect to the public Midaz APIs. Your application keeps control of its data and business logic.
Each SDK is modular, so you bring in only what you need.
## SDKs and internal libraries
***
SDKs are the public path for external applications to integrate with Midaz.
Lerian also maintains internal shared libraries for its own services, plugins, and platform components. Lerian engineering teams use these libraries to standardize how services behave. These libraries are not public integration contracts. Do not treat them as supported APIs for external applications.
To integrate with Midaz, start with the official SDK for your language. If an SDK does not cover a capability, use the Midaz API directly or contact Lerian first.
# Build an account statement
Source: https://docs.lerian.studio/en/midaz/building-account-statements
Turn Midaz account operations into a clear customer statement by filtering by date, direction, and route with the List Operations by Account endpoint.
This guide explains how to transform Midaz account operations into end-user account statements.
You build an account statement from Midaz account operations. Each operation is a ledger movement that links to an account, such as a credit, debit, hold, release, or overdraft event.
To build a statement, retrieve the account operations for the period. Keep the operations that affect the statement view. Then transform each operation into a row that users understand.
Use the [List Operations by Account](/en/reference/midaz/list-operations-by-account) endpoint to list operations for a specific account:
```json theme={null}
GET /v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/{account_id}/operations
```
Use query parameters to define the statement period, control pagination, and filter the operations that the endpoint returns.
### Required for statement queries
| Parameter | Description |
| ----------------- | ---------------------------------------- |
| `start_date` | Start date of the statement period |
| `end_date` | End date of the statement period |
| `limit` | Number of items per page |
| `sort_order=desc` | Returns the most recent operations first |
You need these fields for this statement use case. They define the statement window and make the result predictable for users.
### Required for pagination
| Parameter | Description |
| --------- | ------------------------------------------ |
| `cursor` | Cursor returned from the previous response |
### Optional filters
| Parameter | Description |
| ------------------ | ---------------------------------------- |
| `direction=credit` | Returns only incoming operations |
| `direction=debit` | Returns only outgoing operations |
| `type` | Filters by operation type |
| `route_id` | Filters by the operation route ID (UUID) |
| `route_code` | Filters by the operation route code |
The endpoint can return the following operation types:
| Type | What it means |
| ----------- | ---------------------------------------------- |
| `CREDIT` | Value entering the account |
| `DEBIT` | Value leaving the account |
| `ON_HOLD` | Amount temporarily locked |
| `RELEASE` | Previously locked amount released |
| `OVERDRAFT` | Movement related to overdraft usage |
| `BLOCK` | System-generated account-block companion row |
| `UNBLOCK` | System-generated account-unblock companion row |
Use `type` to classify the accounting movement. Use `direction` to decide whether the statement shows the amount as positive or negative.
Example request:
```json theme={null}
GET /v1/organizations/org_123/ledgers/ledger_001/accounts/account_456/operations?start_date=2026-05-01&end_date=2026-05-31&limit=50&sort_order=desc
```
Each object in the `items` array can become one statement row.
### Required for statement rendering
| Statement field | Operation field |
| -------------------------------- | ------------------------ |
| Date | `createdAt` |
| Description | `description` |
| Movement type | `type` |
| Direction | `direction` |
| Amount | `amount.value` |
| Currency or asset | `assetCode` |
| Balance after operation | `balanceAfter.available` |
| Receipt or transaction reference | `transactionId` |
You need these fields to render a useful statement row. The API does not require all of them. A statement without them loses meaning, traceability, or balance context.
### Recommended for user-friendly statements
| Statement context | Operation field |
| ----------------- | ----------------------- |
| Counterparty | `metadata.counterparty` |
| Document | `metadata.document` |
| Pix key | `metadata.pixKey` |
| End-to-end ID | `metadata.endToEndId` |
| Channel | `metadata.channel` |
| Category | `metadata.category` |
Midaz returns the ledger movement. The integrating system should add business context in the `metadata` of each relevant `source.from[]` and `distribute.to[]` entry when it creates the transaction; metadata does not propagate from source entries to destination entries.
Example transformation:
```json theme={null}
{
"date": "2026-05-18T14:23:11Z",
"description": "Pix transfer received",
"type": "CREDIT",
"direction": "credit",
"amount": "150.00",
"asset": "BRL",
"balanceAfter": "1240.55",
"transactionId": "txn_987654",
"metadata": {
"counterparty": "John Doe",
"pixKey": "john@example.com",
"category": "Transfer"
}
}
```
### Use `direction` to determine the sign
| Direction | Display behavior |
| --------- | ---------------------------- |
| `credit` | Display as a positive amount |
| `debit` | Display as a negative amount |
Do not use `type` to determine whether the value is positive or negative. The `type` field classifies the accounting movement, while `direction` defines whether the value enters or leaves the account.
### Handle hold and release operations separately
Do not display operations with the following types as regular settled movements:
* `ON_HOLD`
* `RELEASE`
Instead:
* `ON_HOLD` should appear as a balance hold or temporary lock
* `RELEASE` should appear as a balance release or unlock
### Define a settled-operation policy
Do not use `balanceAffected` as a settled-operation predicate. A normal `ON_HOLD` operation can set `balanceAffected` to `true` without changing the available balance. Define your statement policy explicitly from operation type and status, and show `ON_HOLD` and `RELEASE` according to the hold/release rules above.
The endpoint splits responses into pages according to the `limit` value.
To retrieve all operations:
1. Read the `next_cursor` field from the response
2. Send it as the `cursor` parameter in the next request
3. Repeat until the response no longer returns `next_cursor`
Example flow:
```text theme={null}
Request 1
-> returns items + next_cursor
Request 2
-> cursor=next_cursor
-> returns more items + next_cursor
Repeat until next_cursor is no longer returned
```
## Example statement output
After you apply filters, transform operations, and apply display rules, the final statement can look like this:
| Date | Description | Amount | Balance after |
| ---------- | -------------------------- | ----------- | ------------- |
| 2026-05-18 | Pix received from John Doe | +150.00 BRL | 1,240.55 BRL |
| 2026-05-18 | Card purchase | -45.90 BRL | 1,194.65 BRL |
The API does not return a ready-made statement page. It returns ledger events that the integrating system turns into a statement experience.
## Add business context
The operations endpoint returns accounting events. A user-facing statement needs more context than the ledger movement alone.
Send business metadata in each operation's metadata when you create the transaction. Midaz stores those fields with that operation. The statement can use them later to show who, what, and why behind the movement.
* `counterparty`
* `document`
* `pixKey`
* `endToEndId`
* `channel`
* `category`
Example metadata:
```json theme={null}
"send": {
"source": {
"from": [{
"accountAlias": "customer_123",
"metadata": {
"counterparty": "John Doe",
"document": "12345678900",
"pixKey": "john@example.com",
"endToEndId": "E1234567890123456789012345678901",
"channel": "PIX",
"category": "Transfer"
}
}]
}
}
```
This lets you display entries such as:
* "Pix received from John Doe"
* "Card purchase at Coffee Shop"
* "Transfer to Savings Account"
instead of generic accounting descriptions.
If the integrating system does not send these fields, the statement still works. It can then only display the accounting data that the operation returns.
Midaz keeps the ledger consistent and auditable. The integrating system adds business context to each operation's metadata.
# Bulk Recorder
Source: https://docs.lerian.studio/en/midaz/bulk-recorder
Use the Bulk Recorder to batch RabbitMQ messages into bulk PostgreSQL inserts, cutting database round trips and raising throughput on high-volume workloads.
## Why this matters
***
Every transaction in Midaz creates balance operations that Midaz must persist. In the default synchronous mode, Midaz writes them directly to the database in the request cycle. This is fine for moderate volumes. At scale — thousands of transactions per second — it becomes the bottleneck.
The Bulk Recorder accumulates messages and writes them in batches. It makes fewer round trips to PostgreSQL, lowers lock contention, and raises throughput. High-volume workloads gain the most: mass payouts, batch settlements, and real-time payment processing.
For broader strategies to scale Midaz, see [Scalability strategies](/en/midaz/scalability-strategies).
## How it works
***
The Bulk Recorder sits between the RabbitMQ consumer and the database layer. It does not insert each message at once. Instead, it collects messages in a buffer and flushes them under two conditions:
1. **Batch size reached** — the buffer fills to the configured size.
2. **Timeout elapsed** — the configured flush timeout expires, even if the buffer is not full.
Whichever condition happens first triggers the flush. This gives large batches under load and low latency during quiet periods.
Here is the full flow, step by step:
1. **RabbitMQ delivers messages** to the BulkCollector one at a time — Message 1, Message 2, up to Message N.
2. **The BulkCollector holds them in memory** instead of one write per message. It collects until the batch fills or the flush timeout expires.
3. **The BulkCollector sends a chunked bulk INSERT to PostgreSQL.** Midaz splits large batches into chunks that respect PostgreSQL's parameter limits. Each chunk uses `ON CONFLICT (id) DO NOTHING`, so retries and duplicate deliveries stay safe.
4. **PostgreSQL confirms the write** and stores the data.
5. **The BulkCollector acknowledges each message** back to RabbitMQ after the write. It acknowledges them one at a time, not in a single bulk ack. This stops a shared channel from acking messages that other workers still process.
## Enabling Bulk Recorder
***
Bulk mode requires async mode. The explicit Bulk Recorder setting is optional because it defaults to enabled:
```bash Environment variables theme={null}
RABBITMQ_TRANSACTION_ASYNC=true
BULK_RECORDER_ENABLED=true
```
Async mode is required. With `RABBITMQ_TRANSACTION_ASYNC=true`, the Bulk Recorder is enabled by default; set `BULK_RECORDER_ENABLED=false` to process queued messages individually. Without async mode, Midaz persists transactions directly instead of consuming queued messages.
`BULK_RECORDER_ENABLED` defaults to `true` when you do not set the environment variable. If you already run with `RABBITMQ_TRANSACTION_ASYNC=true`, bulk mode is probably active. To confirm, check your application logs for `Bulk mode is ACTIVE` at startup.
## Configuration
***
| Variable | Description | Default |
| :------------------------------- | :----------------------------------------------------------------------- | :--------- |
| `BULK_RECORDER_ENABLED` | Enable or disable bulk mode. | `true` |
| `BULK_RECORDER_SIZE` | Number of messages to accumulate before flushing. `0` = auto-calculated. | `0` (auto) |
| `BULK_RECORDER_FLUSH_TIMEOUT_MS` | Maximum time (ms) to wait before flushing an incomplete batch. | `100` |
### Auto-calculated batch size
When `BULK_RECORDER_SIZE` is set to `0` (the default), Midaz derives the batch size:
```
batch size = RABBITMQ_NUMBERS_OF_WORKERS × RABBITMQ_NUMBERS_OF_PREFETCH
```
This aligns the collector's capacity with the real message flow from RabbitMQ. It prevents partial flushes and memory pressure.
If you set `BULK_RECORDER_SIZE` by hand, keep it aligned with your prefetch settings. A size much larger than `workers × prefetch` rarely fills the collector. The collector then flushes mostly on the timeout.
## Tuning for your workload
***
The two main levers are **batch size** and **flush timeout**. The right balance depends on your priority: latency or throughput.
### Low latency (real-time processing)
Keep batches small and timeouts short. Midaz persists messages quickly, even when batches are not full.
```bash Low-latency configuration theme={null}
RABBITMQ_NUMBERS_OF_WORKERS=5
RABBITMQ_NUMBERS_OF_PREFETCH=10
BULK_RECORDER_SIZE=0 # auto: 5 × 10 = 50
BULK_RECORDER_FLUSH_TIMEOUT_MS=50
```
### High throughput (batch operations)
Larger batches and longer timeouts maximize database efficiency. Use this for mass payouts, end-of-day settlements, or migration workloads.
```bash High-throughput configuration theme={null}
RABBITMQ_NUMBERS_OF_WORKERS=10
RABBITMQ_NUMBERS_OF_PREFETCH=20
BULK_RECORDER_SIZE=0 # auto: 10 × 20 = 200
BULK_RECORDER_FLUSH_TIMEOUT_MS=300
```
Start with the defaults and adjust from what you observe. To confirm your settings, watch for the `Bulk mode configured for consumer` log at startup.
## Safety guarantees
***
The Bulk Recorder stays safe under every condition:
### Idempotency
Every bulk insert uses `ON CONFLICT (id) DO NOTHING`. If a message arrives twice — from a retry, a redelivery, or a network fault — PostgreSQL drops the duplicate. You get no data corruption and no constraint violations.
### Deadlock prevention
Before each bulk insert, Midaz sorts the record IDs. All concurrent writers then acquire locks in the same order. This removes the most common source of PostgreSQL deadlocks under high concurrency.
### Internal chunking
Midaz splits large batches into chunks that fit within PostgreSQL's 65,535-parameter limit per query:
| Record type | Columns per row | Rows per chunk | Parameters per chunk |
| :---------- | :-------------- | :------------- | :------------------- |
| Transaction | 18 | 1,000 | 18,000 |
| Operation | 31 | 1,000 | 31,000 |
Midaz handles this chunking for you. Each `INSERT` carries at most 1,000 rows. `BULK_RECORDER_MAX_ROWS_PER_INSERT` is not currently applied to the PostgreSQL chunk size.
## When to use
***
**Use Bulk Recorder when:**
* You process high volumes of transactions (hundreds or thousands per second).
* Your workload includes batch operations like mass payouts, settlements, or data migrations.
* You already use async transaction processing (`RABBITMQ_TRANSACTION_ASYNC=true`).
* You want to reduce database load and connection pressure.
**Keep it disabled when:**
* Your volume is low enough that individual inserts are not a bottleneck.
* You need strict per-message ordering that batch processing would break.
* You debug transaction processing and want a simpler, message-by-message flow.
# Closing a customer account
Source: https://docs.lerian.studio/en/midaz/closing-a-customer-account
Follow the eight-step flow to freeze, settle, and archive a customer account across Ledger and CRM in line with BACEN account closure and audit requirements.
To close a customer account in Midaz, you work across two areas: the **Ledger** (accounts and balances) and **CRM** (holders and alias accounts). BACEN rules make account closure a regulated event, so the order matters. Stop new credits first, then settle pending activity and return any remaining funds. Deactivate the underlying records last.
This guide covers the full closure flow, from the [Holder](/en/midaz/crm/holders) to its [Alias Accounts](/en/midaz/crm/alias-accounts). You can run the flow two ways. **[Via API](#via-api)** gives you the endpoint, an example payload, and the compliance rationale for each step. **[Via Console](#via-console)** gives you the same steps as point-and-click actions in the Midaz Console.
Follow the steps in order. Do not close accounts or archive CRM records before you zero the balances. Early closure can leave orphaned funds or break the audit trail that regulatory reporting needs.
## Overview
***
The closure flow has eight steps, grouped into three phases:
| Phase | Steps | Goal |
| :----------------------- | :---- | :------------------------------------------------------------------------- |
| **1. Freeze** | 1–2 | Mark the Holder inactive and stop new credits at the balance level. |
| **2. Settle and zero** | 3–4 | Clear pending activity and return remaining funds to the customer. |
| **3. Close and archive** | 5–8 | Deactivate Ledger Accounts and archive CRM records under retention policy. |
Throughout this guide, `{organization_id}` and `{ledger_id}` identify the Midaz Organization and Ledger that own the accounts. This guide abbreviates the paths to `/v1/.../accounts/{accountId}` for readability.
## Prerequisites
***
Before you start, make sure you have:
* The `holderId` of the customer to offboard.
* The list of `accountId` values linked to that Holder across the Ledger. Retrieve them with `GET /v1/organizations/{organization_id}/holders/{holder_id}/accounts`; the response is paginated, so retrieve every page.
* The `instrument_id` of each Alias Account, mapped to its corresponding `accountId`. List the Holder's instruments with `GET /v1/organizations/{organization_id}/instruments?holder_id={holder_id}` (also paginated) and use each instrument's `accountId` field to map it to the Ledger Account. Steps 6 and 7 target these `instrument_id` values.
* Confirmation from your compliance team that you can end the customer relationship (no legal holds, open disputes, or pending regulatory requirements).
* Appropriate API credentials with permission to modify holders, instruments, balances, and accounts. Steps 6 and 7 update and delete instruments, so the credentials must carry `patch` and `delete` access on the `instruments` resource.
Account closure is irreversible from the customer's perspective. Before you proceed, confirm there are no active products or open obligations.
## Via API
***
Run the full closure flow programmatically. Each step lists the endpoint, an example payload, and the compliance rationale.
### Step 1 — Freeze the Holder
Mark the Holder as inactive to record the closure across your systems. Update the Holder and set the `status` field on its person profile to an inactive value.
```http theme={null}
PATCH /v1/organizations/{organization_id}/holders/{holder_id}
```
```json theme={null}
{
"naturalPerson": {
"status": "INACTIVE"
}
}
```
Marking the Holder inactive is a record change, not a deletion. The Holder record stays fully readable for audit. This status does not block new credits on its own — Step 2 blocks inflows at the balance level. For a legal person, set `legalPerson.status` instead.
### Step 2 — Block credits on the accounts
For each account linked to the Holder, prevent new funds from entering. First, list the [Balances](/en/midaz/balances) of the account. Then update each balance to disable receiving.
**Retrieve the account balances:**
```http theme={null}
GET /v1/.../accounts/{accountId}/balances
```
**For each `balanceId` returned, block incoming funds:**
```http theme={null}
PATCH /v1/.../balances/{balanceId}
```
```json theme={null}
{
"allowReceiving": false
}
```
Repeat this step for **every** `balanceId` on **every** account belonging to the Holder. A single balance left open can still receive credits and block closure later.
When you set `allowReceiving` to `false`, the balance blocks new inflows but still allows outflows. This is exactly what you need in Step 4 to return the remaining balance to the customer. For details on the permission flags, see [Balances](/en/midaz/balances).
### Step 3 — Settle pending activity
Before you can zero a balance, the account must have no in-flight movements.
* **Check for transactions in processing.** Confirm the account has no pending or uncommitted transactions. Commit or cancel them as appropriate using [Commit a pending transaction](/en/reference/midaz/commit-a-pending-transaction) or [Cancel a pending transaction](/en/reference/midaz/cancel-a-pending-transaction).
If you skip this step, a closed account can receive late entries. Late entries break reconciliation and the BACEN audit trail.
### Step 4 — Zero the balance
Return any remaining funds to the customer (the account holder) and confirm `available = 0` and `onHold = 0` on every balance.
* Record a **return transaction** that moves the remaining `available` amount from each customer account to the holder's designated destination (for example, an external settlement account). Use [Create a transaction](/en/reference/midaz/create-a-transaction-using-json).
* **Confirm `available = 0` and `onHold = 0`** on every balance of every account in the Ledger before you proceed. You can check this with [Retrieve balances by account](/en/reference/midaz/retrieve-balances-by-account).
Midaz **does not allow deleting an account that still holds a balance**. All balances must be zero before Step 5.
### Step 5 — Close the Ledger Accounts in Midaz
After you zero the balances and clear pending activity, delete each Ledger Account.
```http theme={null}
DELETE /v1/.../accounts/{accountId}
```
A successful request returns `204 No Content`. Repeat for every account linked to the Holder. See [Delete an account](/en/reference/midaz/delete-an-account) for the full contract.
Deleting a Ledger Account is a logical removal. The account and its historical operations remain available for audit and reporting, subject to your retention policy.
### Step 6 — Register the closing date on the alias
Record the official closure date on the Holder's Alias Account so the CRM and any regulatory exports reflect when the relationship ended.
```http theme={null}
PATCH /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}
```
```json theme={null}
{
"bankingDetails": {
"closingDate": "2026-06-09"
}
}
```
The `closingDate` field lives in the `bankingDetails` object of the Alias Account and uses `YYYY-MM-DD` format. See [Alias Accounts](/en/midaz/crm/alias-accounts) for the full field reference. BACEN account-lifecycle reporting needs an accurate closing date.
### Step 7 — Archive the alias accounts in CRM
Archive each Alias Account in CRM. Use a **soft delete**. It removes the record from active use but keeps it for the regulatory retention period.
```http theme={null}
DELETE /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}
```
Do **not** pass `hard_delete=true`. A regulated closure must archive (soft-delete) the record and keep it. It must not erase the record permanently. See [Delete an instrument](/en/reference/midaz/delete-an-instrument).
### Step 8 — Archive the Holder in CRM
After you archive all of its alias accounts, archive the Holder itself with a soft delete.
```http theme={null}
DELETE /v1/organizations/{organization_id}/holders/{holder_id}
```
As in Step 7, omit `hard_delete=true`. Keep the Holder record under the applicable retention policy for audit and regulatory inspection. See [Delete a holder](/en/reference/midaz/crm/delete-holder).
## Via Console
***
Run the same eight-step closure flow from the [Midaz Console](/en/midaz/console/midaz-module). The Console covers most of the flow point-and-click, but blocking credits (Step 2) still requires the API. Each step below notes the equivalent API step on this page.
The order is the same as the API flow. Do not delete accounts or archive CRM records before you zero the balances.
### Step 1 — Freeze the Holder
Mark the Holder as inactive to record the closure. This is a record change and does not block new credits on its own.
From the **Holders** page, find the Holder to close.
Click the three dots () in the **Actions** column, and select **Edit**.
In the Holder form, set the **Status** to **Inactive**.
Confirm the update.
Marking the Holder inactive is a record change, not a deletion. The record stays fully readable for audit. It does not block new credits on its own — Step 2 blocks inflows at the balance level. See [Editing a Holder](/en/midaz/console/crm-editing-a-holder).
### Step 2 — Block credits on the accounts
**This step requires the API. The Console does not support editing balance flags after account creation.** The Console lets you set `allowReceiving` only when you first create an Account, not when you edit an existing balance. Use the API to disable receiving on every balance.
Follow [Step 2 — Block credits on the accounts](#step-2--block-credits-on-the-accounts) in the Via API section.
For each account linked to the Holder, use the API to set `allowReceiving` to `false` on every `balanceId`. This blocks new inflows while outflows remain available for the return transaction in Step 4.
### Step 3 — Settle pending activity
Confirm there are no in-flight movements before you zero any balance.
From the **Transactions** page, filter by the accounts linked to the Holder. Confirm the account has no pending or uncommitted transactions, and commit or cancel any that are in flight.
Review any pending transactions and commit or cancel them as appropriate.
### Step 4 — Zero the balance
Return any remaining funds to the customer and confirm `available = 0` and `onHold = 0` on every balance.
From the **Transactions** page, click **New Transaction**. Create a **return transaction** that moves the remaining `available` amount from each customer account to the holder's designated destination, for example an external settlement account. See [Creating a Transaction](/en/midaz/console/creating-a-transaction).
Open each account and confirm **available = 0** and **onHold = 0** before you continue.
Midaz **does not allow deleting an account that still holds a balance**. All balances must be zero before Step 5.
### Step 5 — Close the Ledger Accounts in Midaz
After you zero the balances and clear pending activity, delete each Ledger Account.
From the **Accounts** page, find the Account linked to the Holder, click the three dots () in the **Actions** column, and select **Delete**.
A confirmation dialog will appear. Click **Confirm** to finalize the deletion.
Repeat for every account linked to the Holder.
Deleting a Ledger Account is a logical removal. The account and its historical operations remain available for audit and reporting, subject to your retention policy. See [Deleting an Account](/en/midaz/console/deleting-an-account).
### Step 6 — Register the closing date on the alias
Record the official closure date on the Holder's Alias Account so the CRM and regulatory exports reflect when the relationship ended.
From the **Alias Accounts** page, find the alias account to update, click the three dots () in the **Actions** column, and select **Edit**.
In the Alias Account form, set the **Closing Date** (in `bankingDetails`) to the official closure date using `YYYY-MM-DD` format.
Confirm the update.
BACEN account-lifecycle reporting needs an accurate closing date. See [Editing an Alias Account](/en/midaz/console/crm-editing-alias-account).
### Step 7 — Archive the alias accounts in CRM
Archive each Alias Account with a **soft delete**. It removes the record from active use but keeps it for the regulatory retention period.
From the **Alias Accounts** page, find the alias account to archive, click the three dots () in the **Actions** column, and select **Delete**.
A confirmation dialog will appear. Click **Confirm** to finalize.
Use the standard (soft) delete. It archives and keeps the record instead of erasing it permanently. In regulated deployments, Midaz keeps the underlying record for the retention period. See [Deleting an Alias Account](/en/midaz/console/crm-deleting-alias-account).
### Step 8 — Archive the Holder in CRM
After you archive all of its alias accounts, archive the Holder itself with a **soft delete**.
From the **Holders** page, find the Holder to archive, click the three dots () in the **Actions** column, and select **Delete**.
A confirmation dialog will appear. Click **Confirm** to finalize.
Use **Soft Delete** (the default), not Hard Delete. Keep the Holder record under the applicable retention policy for audit and regulatory inspection. See [Deleting a Holder](/en/midaz/console/crm-deleting-a-holder).
## BACEN compliance notes
***
* **Order is mandatory.** Freeze first (Steps 1–2), then settle and zero (Steps 3–4). This order stops funds from entering an account that is mid-closure.
* **Return funds before closing.** Return any residual balance to the customer and confirm `available = 0` and `onHold = 0` before you delete an account. Midaz blocks closing an account that holds funds, and that closure would also break compliance.
* **Archive, don't erase.** A soft delete (no `hard_delete`) keeps Holders and alias accounts available for the regulatory retention period. Permanent deletion would remove evidence that BACEN audits need.
* **Record the closing date.** The `closingDate` on the alias gives regulators an authoritative timestamp for when the relationship ended.
* **Preserve the audit trail.** Midaz removes Ledger Accounts and operations logically. They remain queryable for reconciliation and reporting.
Treat the eight steps as a single transaction from a compliance standpoint. If any step fails, pause and resolve it before you continue. Do not leave the customer in a partially closed state.
# Common mistakes to avoid
Source: https://docs.lerian.studio/en/midaz/common-mistakes-to-avoid
Avoid the typical pitfalls of Midaz implementations — Ledger structuring, Asset segregation, Accounting Routes, and external Accounts.
A reliable Midaz implementation depends on a few structural choices you make early. Avoid the common mistakes below to keep balances accurate and access under control.
## Poor Ledger structuring
***
* **Mistake:** You design ledgers that are too complex or too simplistic. For example, one ledger per customer creates fragmentation. Grouping unrelated processes creates confusion.
* **Best practice:** Structure ledgers around key business areas or legal entities. Use organized groupings instead of a one-to-one approach. This improves manageability and internal transfers.
## Not using Portfolios for clients
***
* **Mistake:** You treat each account in isolation instead of grouping it under a portfolio. This makes it hard to track all accounts linked to a single customer.
* **Best practice:** Create a portfolio for each customer. Group their accounts for simple queries, relationship management, and reporting.
## Mixing Assets in one Account
***
* **Mistake:** You manage multiple currencies or asset types in a single account, often through metadata overrides. This leads to unclear balances and reporting inconsistencies.
* **Best practice:** Assign one asset per account. If a customer needs multi-currency support, create separate accounts. Use naming conventions or portfolios to structure them.
## Inadequate Accounting Routes
***
* **Mistake:** You submit transactions without properly configured Operation Routes or Accounting Routes (Transaction Routes in the API). This causes inconsistent validations, misrouted funds, and unreliable ledger data.
* **Best practice:** Define Accounting Routes that reflect your business logic and compliance requirements, then set the Ledger setting `accounting.validateRoutes` to `true` to enforce them. Its default is `false`. Use Operation Routes to validate the source and destination of each operation when route validation is enabled. Structured routes keep every transaction auditable.
## Overlooking External Account handling
***
* **Mistake:** You mishandle external transactions. For example, you delete an account when funds leave the bank, which causes imbalances.
* **Best practice:** Use the canonical `@external/` Account (for example, `@external/BRL`) for money that flows into and out of Midaz. This keeps the ledger balanced and matches real bank statements.
## Security lapses in access control
***
* **Mistake:** You grant excessive privileges to administrators or use shared accounts. This compromises security and accountability.
* **Best practice:** Implement role-based access control (RBAC) from the start. Do not use root or admin accounts for daily operations. Enforce strict credential management. Audit permissions regularly to prevent unauthorized privilege escalation.
## Improper error handling in transactions
***
* **Mistake:** You fail to handle failed transactions in the integration layer. This leads to false assumptions about successful operations.
* **Best practice:** Always validate transaction API responses. Implement retry mechanisms with backoff for transient errors. Log permanent failures. Make sure upstream systems, such as core banking applications, handle rejected transactions and notify the user.
## Ensuring a robust Midaz implementation
***
Address these pitfalls early and Midaz stays a reliable financial backbone. Your team spends time on new features, not on avoidable problems.
# Midaz entities
Source: https://docs.lerian.studio/en/midaz/core-entities
See how Midaz core entities — Organizations, Ledgers, Assets, Portfolios, Accounts, Segments, and Balances — connect to power financial operations at scale.
This diagram shows how the main building blocks of Midaz connect to support financial operations at scale. Figure 1 gives an overview of how the system works.
## Core components
***
### Midaz
Midaz is at the center. It coordinates everything, from data persistence to business logic.
### Data Layer
All data flows into the persistent **data layer**. This layer gives the system durability, consistency, and performance.
## Domains and relationships
***
### Organizations
Each organization is a distinct environment in Midaz. It can hold one or many ledgers, based on how you structure your data.
### Ledgers
A ledger holds all financial activity. It tracks balances, stores assets, and powers transactions. Every ledger belongs to an organization.
From here, the system branches into:
* **Assets**: define the kind of value you track.
* **Portfolios**: group related accounts under a shared purpose.
### Assets
Assets define the units of value, like BRL, USD, points, or tokens. Each account uses one specific asset.
### Portfolios
Portfolios group accounts by business logic. You can create as many portfolios as you need to organize your financial structure. Each portfolio contains one or more accounts.
### Accounts & Segments
An account belongs to a portfolio and represents an individual balance. **Segments** group accounts, so you can isolate data by customer, product, region, or any other dimension. Segments help you track and report with precision.
**Account Types** classify accounts by nature and purpose. They enable transaction routing and operational validation across account categories.
### Balance
A **Balance** represents the net amount an **Account** holds of a given **Asset** in a specific **Ledger**.
* An account can carry **multiple balances**. Each has a `balanceKey` (e.g. `default`, `collateral`, `operational`) to partition funds without splitting the account.
* An **external account** has only one balance (`default`).
* If you omit `balanceKey` in a transaction, Midaz uses the default balance.
* Midaz derives balances automatically from ledger operations. You can list, fetch, or (in admin or test scenarios) adjust them manually.
* The Valkey cache key includes `balanceKey` in its format: `balance:{transactions}:::#`.
### Transactions
A transaction moves value between accounts. Each transaction contains **operations** (debits and credits). This keeps your logic consistent, traceable, and aligned with your business model and accounting policies.
### Accounting Routes entities
The [Accounting Routes entities](/en/midaz/transaction-routing-entities) extend Midaz. They create a structured system that guarantees correct annotations and correct financial-flow directions.
It has two parts:
* **Operation Routes:** define atomic transaction behaviors. Each route sets how Midaz annotates and directs one financial movement, based on your business rules and accounting standards.
* **Accounting Routes:** orchestrate multiple Operation Routes into balanced financial events. They enforce double-entry validation and keep transaction integrity across all accounts. (The API calls this `transactionRoute`.)
This pair forms a self-governing system. Midaz automatically validates each transaction for correct annotation and flow direction.
## In short
***
Midaz gives you a modular structure:
* Organizations manage ledgers.
* Ledgers hold assets, portfolios, and accounting structures.
* Portfolios contain accounts, grouped by segment.
* Transactions move value and trigger accounting treatments.
* Your data stays synced, traceable, and ready for financial reporting.
**Need help designing your Midaz setup?**
Our consulting service can help you tailor the structure to your organization's needs. This applies whether you start from scratch or optimize an existing setup.
[Contact us](https://lerian.studio/contact) to learn more.
# Key features
Source: https://docs.lerian.studio/en/midaz/core-features
Explore the core capabilities that make Midaz adaptable and scalable — native events, multi-asset support, segments, and double-entry accounting.
Midaz is a source-available core ledger. Financial institutions use it to build, operate, and scale their transactions on a modular foundation. You keep full control of your stack.
Use Midaz to launch new financial products or to modernize existing processes. The features below show how the ledger records value, scales across organizations, and keeps balances consistent.
## Native event publisher
***
Midaz emits supported domain events through its configured streaming publisher. Configure and operate the delivery path required by your environment before relying on an event stream for downstream processing.
## Client segments
***
Group Accounts into ledger-scoped Segments. These persisted associations support scoped rules and operational grouping.
## Double-entry accounting
***
Midaz records Transactions as Operations, including debit, credit, hold, and release movements. When Accounting Route validation is enabled, configured routes validate applicable participation and posting rules.
## Multi-currency/asset support
***
Midaz supports transactions in multiple currencies and assets. You operate across different financial ecosystems from one ledger.
## Multi-organization/ledger support
***
One Midaz instance can run multiple Organizations and Ledgers. The domain model scopes financial resources by Organization and Ledger.
## N:N transactions
***
A single transaction can move value between many source and destination accounts. This N:N model supports complex business flows.
## Native race condition handling
***
Race conditions happen when multiple operations change the same balance at the same time. Midaz handles this per account with atomic balance updates. Transactions stay reliable and accurate under high concurrency.
## Wallets portfolio management
***
Group accounts into portfolios to manage them together. You get full visibility and control over assets and balances.
## Visual interface
***
A web console gives you a single place to manage transactions, configuration, and system insights.
In newer releases, **[Lerian Console](/en/platform/console/about-lerian-console)** provides the visual interface for Midaz. Midaz runs there as the **[Midaz Module](/en/midaz/console/midaz-module)**. If you run an older version, you may still have the standalone Midaz Console, now in maintenance mode.
# Data model
Source: https://docs.lerian.studio/en/midaz/data-model
Reference the core tables and schemas that power Midaz, designed for developers and database administrators working close to the data.
**This page is for developers and database administrators.** It contains detailed database schemas and table structures. For a business-level overview of Midaz entities and their relationships, see [About Midaz](/en/midaz/about-midaz).
Explore the core tables and schemas that power Midaz. This reference helps developers and database administrators understand how Midaz structures and connects data across the system. As Midaz evolves, it may add new fields and tables.
## Tables
***
Each table supports efficient queries and data integrity.
### Table: `organization`
| Column | Type | Description |
| :----------------------- | :------------ | :--------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Organization. |
| **legalName** | `text` | The legal name of the Organization. |
| **parentOrganizationId** | `uuid` | The unique identifier of the parent Organization. |
| **doingBusinessAs** | `text` | The trade name of the Organization. |
| **legalDocument** | `text` | The document of the Organization. |
| **status** | `jsonb` | Information about the status. |
| **address** | `jsonb` | Information about the address of the Organization. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | The timestamp of soft deletion, if applicable (UTC). |
### Table: `ledger`
| Column | Type | Description |
| :----------------- | :------------ | :----------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Ledger. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **name** | `text` | The name of the Ledger. |
| **status** | `jsonb` | Information about the status. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `asset`
| Column | Type | Description |
| :----------------- | :------------ | :----------------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Asset. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **name** | `text` | The name of the Asset. |
| **type** | `enum` | The type of Asset (`currency`, `fiat`, `crypto`, `commodity`, `others`). |
| **code** | `text` | The code used to refer to the Asset. |
| **status** | `text` | Information about the status. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `account-type`
| Column | Type | Description |
| :-------------- | :------------ | :------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Account Type. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **name** | `text` | Name of the Account Type. |
| **description** | `text` | Description of the Account Type. |
| **keyValue** | `text` | Custom value defined by the user to identify the Account Type. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `account`
| Column | Type | Description |
| :------------------ | :------------ | :--------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Account. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **assetCode** | `text` | The code used to refer to the Asset. |
| **name** | `text` | The name of the Account. |
| **alias** | `text` | A unique, user-friendly identifier for the account. |
| **type** | `text` | The type of account. |
| **parentAccountId** | `uuid` | The unique identifier of the Parent Account. |
| **entityId** | `text` | The unique identifier of the Entity responsible for the Account. |
| **portfolioId** | `uuid` | The unique identifier of the Portfolio. |
| **segmentId** | `uuid` | The unique identifier of the Segment. |
| **status** | `jsonb` | Information about the status. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `portfolio`
| Column | Type | Description |
| :----------------- | :------------ | :--------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Portfolio. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **entityId** | `text` | The unique identifier of the user responsible for the Portfolio. |
| **name** | `text` | The name of the Portfolio. |
| **status** | `jsonb` | Information about the status. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `segment`
| Column | Type | Description |
| :----------------- | :------------ | :----------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Segment. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **name** | `text` | The name of the Segment. |
| **status** | `jsonb` | Information about the status. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **created\_at** | `timestamptz` | Timestamp of creation (UTC). |
| **updated\_at** | `timestamptz` | Timestamp of last update (UTC). |
| **deleted\_at** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `balance`
| Column | Type | Description |
| :----------------- | :------------ | :----------------------------------------------------------- |
| **id** | `text` | The unique identifier of the Balance. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **accountId** | `uuid` | The unique identifier of the Account. |
| **alias** | `text` | The alias for the account used in the operation. |
| **assetCode** | `text` | The code used to refer to the Asset. |
| **available** | `text` | Previous available balance. |
| **onHold** | `text` | Amount on hold/reserved. |
| **version** | `integer` | Balance version, which is updated with each transaction. |
| **accountType** | `text` | The type of account. |
| **allowSending** | `boolean` | If true, indicates that sending transactions is permitted. |
| **allowReceiving** | `boolean` | If true, indicates that receiving transactions is permitted. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `operation-route`
| Column | Type | Description |
| :----------------- | :------------ | :--------------------------------------------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Operation Route. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **title** | `text` | Short text summarizing the purpose of the operation. Used as an entry note for identification. |
| **description** | `text` | Detailed description of the Operation Route purpose and usage. |
| **operationType** | `text` | `operationType` — the entry's applicability: `source`, `destination`, or `bidirectional`. |
| **account** | `jsonb` | Defines the rule for selecting the account that will participate in the operation (debit or credit). |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `operation`
| Column | Type | Description |
| :-------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Operation. |
| **transactionId** | `uuid` | The unique identifier of the Transaction. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **accountId** | `uuid` | The unique identifier of the account of which you want to retrieve the balances. |
| **balanceId** | `uuid` | The unique identifier of the Balance. |
| **accountAlias** | `text` | The alias for the account used in the operation. |
| **description** | `text` | Description of the transaction. |
| **type** | `text` | The type of the operation (debit or credit). |
| **assetCode** | `text` | The name of the asset used in the operation. |
| **chartOfAccounts** | `text` | The name of the chart of accounts that the operation belongs to. |
| **route** | `text` | **\[Deprecated]** Legacy route identifier, kept for backward compatibility. Use `routeId` instead. |
| **routeId** | `uuid` | Identifier of the route used by the operation (the current route field). |
| **amount** | `jsonb` | An object containing information about the amount used in the operation. |
| **balance** | `jsonb` | An object containing information about the balance before the operation. |
| **balanceAfter** | `jsonb` | An object containing information about the balance after the operation. |
| **status** | `jsonb` | The transaction status (pending, completed, reversed). |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
**Deprecated field** — This field remains for backward compatibility. Do not use it.
### Table: `transaction-route`
| Column | Type | Description |
| :------------------ | :------------ | :----------------------------------------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the Transaction Route. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **title** | `text` | Short text summarizing the purpose of the transaction. Used as an entry note for identification. |
| **description** | `text` | A description for the Transaction Route. |
| **operationRoutes** | `array` | A list of Operation Route IDs that define the debit and credit logic for the transaction. |
| **metadata** | `object` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
### Table: `transaction`
| Column | Type | Description |
| :-------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------- |
| **id** | `uuid` | The unique identifier of the transaction. |
| **organizationId** | `uuid` | The unique identifier of the Organization. |
| **ledgerId** | `uuid` | The unique identifier of the Ledger. |
| **description** | `text` | Description of the transaction. |
| **route** | `text` | **\[Deprecated]** Legacy route identifier, kept for backward compatibility. Use `routeId` instead. |
| **routeId** | `uuid` | Identifier of the route used by the transaction (the current route field). |
| **status** | `jsonb` | Information about the status. |
| **amount** | `text` | The sent amount. |
| **assetCode** | `text` | The code used to refer to the Asset. |
| **chartOfAccountsGroupName** | `text` | The name of the chart of accounts group used to categorize the operations of a transaction. |
| **source** | `array` | The list of accounts used as source. |
| **destination** | `array` | The list of accounts used as destination. |
| **operations** | `array` | The list of operations in the transaction. |
| **metadata** | `jsonb` | Key-value pairs to add as metadata. |
| **createdAt** | `timestamptz` | Timestamp of creation (UTC). |
| **updatedAt** | `timestamptz` | Timestamp of last update (UTC). |
| **deletedAt** | `timestamptz` | Timestamp of soft deletion, if applicable (UTC). |
**Deprecated field** — This field remains for backward compatibility. Do not use it.
## Data model
***
*Figure 1* shows the core Midaz tables and their relationships. The diagram helps developers and database administrators see how entities connect within the system. Use it to guide data handling and integration.
# Event publisher
Source: https://docs.lerian.studio/en/midaz/event-publisher
Enable the Midaz event publisher to stream approved, pending, canceled, and reversal transaction events to a RabbitMQ exchange for downstream integrations.
## Why this matters
***
Published events let your systems react to a transaction the moment it happens. You can trigger customer notifications, sync your ERP, feed analytics dashboards, or start compliance workflows. Your systems stay loosely coupled.
The sections below cover the technical setup. For a business overview, see [About Midaz](/en/midaz/about-midaz).
## Enabling transaction events
***
Midaz publishes transaction events **by default**. It treats the flag as enabled unless you set it to `false`. The bundled example configuration ships the flag set to `false`. On a stack that starts from that example, set the flag to `true` in the transaction application:
```bash JSON theme={null}
RABBITMQ_TRANSACTION_EVENTS_ENABLED=true
```
With the flag enabled, Midaz publishes events to this RabbitMQ exchange:
```bash JSON theme={null}
transaction.transaction_events.exchange
```
## Event types
***
Midaz emits one of these event types, based on the transaction lifecycle stage:
| Action | Description |
| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| `APPROVED` | The transaction was successfully completed. This includes single-step transactions and two-phase transactions that have been committed. |
| `PENDING` | A two-phase transaction was created and is waiting for either a commit or cancellation. |
| `CANCELED` | A two-phase transaction was canceled before confirmation. |
| `CREATED` | A reversal transaction was initiated. This is a transient status that progresses to `APPROVED` once processing completes. |
| `NOTED` | An annotation transaction was recorded. The transaction is logged in the ledger without affecting account balances. |
## Example event payload
***
```json JSON expandable theme={null}
{
"source": "midaz",
"eventType": "transaction",
"action": "APPROVED",
"timestamp": "0000-00-00T18:09:03.757330233Z",
"version": "v3.0.0",
"organizationId": "0198575d-f9fd-702b-bb15-fa4c980b32c7",
"ledgerId": "0198575d-fa0b-7ac7-8b7d-9d3ab7dccafc",
"payload": {
"id": "0198575f-a8f9-7924-a6d7-8122f2c77ddd",
"status": {
"code": "APPROVED",
"description": "APPROVED"
},
"amount": "1",
"assetCode": "BRL",
"source": ["account:1"],
"destination": ["account:2"],
"metadata": {
"key": "value"
},
"operations": [
{
"type": "DEBIT",
"amount": { "value": "1" },
"accountAlias": "account:1"
},
{
"type": "CREDIT",
"amount": { "value": "1" },
"accountAlias": "account:2"
}
]
}
}
```
The full payload includes timestamps, balance snapshots, and other identifiers. You use these for audit and traceability.
## Event routing model
***
Midaz uses a topic exchange to publish messages. It does not send messages directly to specific queues. You control which events you receive with your own bindings.
### How routing works
Midaz tags each event with a routingKey. The routingKey uses this format:
```
midaz.transaction.
```
The `` value is the current transaction status: `APPROVED`, `PENDING`, `CANCELED`, `CREATED`, or `NOTED`.
To consume events, your application must:
**Create a queue** in RabbitMQ.
**Bind your queue** to the Midaz exchange with a routingKey pattern for the events you want.
### Visual overview
You can configure multiple queues with different bindings to serve specific teams or services independently.
## Queue and binding example
***
### Creating a new queue
```json JSON theme={null}
{
"queues": [
{
"name": "new_queue_name.queue",
"vhost": "/",
"durable": true
}
]
}
```
### Binding the queue to receive all events
```json JSON theme={null}
{
"bindings": [
{
"source": "transaction.transaction_events.exchange",
"vhost": "/",
"destination": "new_queue_name.queue",
"destination_type": "queue",
"routing_key": "midaz.transaction.*"
}
]
}
```
The wildcard `*` matches all five statuses: `APPROVED`, `PENDING`, `CANCELED`, `CREATED`, and `NOTED`. To subscribe to one event type, replace the wildcard with the exact status. For example, `midaz.transaction.NOTED` receives only annotation events.
Midaz does not manage or create RabbitMQ queues for you. You must provision the queues and set up the correct bindings.
# Foundation
Source: https://docs.lerian.studio/en/midaz/foundation-overview
Meet the foundation layer of Midaz — Organizations, Ledgers, and Assets — the structural building blocks that all Accounts, Balances, and Transactions rely on.
The foundation layer defines the structural building blocks of your financial data in Midaz. Organizations, Ledgers, and Assets form the base on which all accounts, balances, and transactions operate.
## Content
***
Here, you'll find:
* [**Organizations**](/en/midaz/organizations): The top-level entity that represents a financial institution. Organizations govern all financial data. They can also establish parent-child hierarchies for structures like subsidiaries, regional branches, or business units.
* [**Ledgers**](/en/midaz/ledgers): The financial backbone of an Organization. Ledgers track all transactions and operations. This gives you complete traceability and control. Organizations can use multiple Ledgers to segregate financial operations by business line, geography, or regulatory requirement.
* [**Assets**](/en/midaz/assets): Define the units of value your system works with — currencies like BRL and USD, cryptocurrencies, loyalty points, or tokenized securities. Each Account uses a single Asset. Midaz supports multi-asset operations within one system.
# Initiating with Enterprise Model
Source: https://docs.lerian.studio/en/midaz/initiating-with-enterprise-model
Set up Lerian Enterprise in the right order to onboard product, engineering, and ops teams across Midaz, Access Manager, and plugins.
This guide is for **Enterprise Model** users with access to **Midaz** and **Access Manager**. It also shows how to configure the **Fees Engine** and **CRM** capabilities that ship inside Midaz, and how to add **Reporter** — in the most effective order.
## Getting started: Setup order
***
For clean, modular integrations, follow this setup order:
### Step 1: Configure Midaz Core
Start with the [Midaz recommended workflow](/en/midaz/recommended-workflow). It covers these tasks:
* Create your Organization and Ledger(s).
* Define assets and account types.
* Set up portfolios, accounts, and segments.
* Configure Accounting Routes for validation.
* Build double-entry transaction workflows.
### Step 2: Activate Access Manager
**Access Manager** centralizes authentication and role-based access control (RBAC) for Midaz and its plugins.
* Define user roles and scopes.
* Assign permissions to API keys or users.
* Control access to operations by role.
Start with the [Access Manager Setup](/en/platform/access-manager/access-manager) page.
### Step 3: Configure CRM
**CRM** ships inside the Midaz ledger process and manages `Holder` and `Instrument` records. Instruments link Holders to ledger accounts, and accounts can optionally belong to portfolios.
* Register customers as `Holder` objects.
* Create Instruments to link Holders to ledger accounts, or use `POST /v1/organizations/{organization_id}/holders/{id}/accounts` to open a holder-owned account in one call.
* Add metadata for compliance, segmentation, and insights.
This links each customer to the right ledger accounts.
Start with the [CRM Setup](/en/midaz/crm/crm-overview) page.
### Step 4: Configure Fees Engine
Use the **Fees Engine** to automate pricing and monetization rules across your operations.
* Define fee packages with flat or percentage calculations.
* Attach fees to operations like transfers or withdrawals.
* Control when and how the Fees Engine calculates and charges each fee.
Start with the [Fees Engine Setup](/en/midaz/fees/fees-engine-overview) page.
### Step 5: Design with Reporter templates
Use **Reporter** and its template engine to generate dynamic, personalized content such as:
* Transaction summaries
* Regulatory documents
* Customer notifications
Use it to standardize documents and messages with context data from Midaz and CRM.
Start with the [Reporter quick start](/en/reporter/reporter-quick-start) page.
## Final recommendations
***
After you set up all systems:
* Review the integration points between Midaz and each capability.
* Create automated workflows that combine ledger events, customer data, and fee logic.
* Apply permission rules with Access Manager to control access at scale.
Lerian capabilities are modular. Activate and expand them progressively as your architecture matures.
# Ledgers
Source: https://docs.lerian.studio/en/midaz/ledgers
Use Ledgers as the financial backbone of each Organization, tracking Transactions and Operations and enabling orchestrated cross-ledger transfers via bridges.
A **Ledger** is the Organization's financial backbone. It keeps a precise record of all Transactions and Operations. A Ledger tracks every financial event: deposits, withdrawals, transfers, and fees. This gives you complete traceability and control.
Organizations can use **multiple Ledgers** to segregate financial operations. For example, a bank keeps separate Ledgers for different business lines, regions, or regulatory requirements. A single Ledger can also handle all operations for a simpler setup.
## Ledger structure
***
* **Ledger > Organization:** Each Organization can own multiple Ledgers, but each Ledger belongs to only one Organization. This gives you financial separation and accountability.
* **Use multiple Ledgers only when you need to segregate data or operations** — for example, internal treasury operations versus customer accounts.
Transactions **cannot** move directly between Ledgers. To move funds across Ledgers, orchestrate the transfer through a workflow that keeps both sides consistent.
### How to orchestrate cross-Ledger transactions
To move funds between two Ledgers, create an orchestrated workflow. The workflow breaks the transfer into two separate transactions:
1. **Debit from the source Ledger:** Transfer the amount from an account in **Ledger 1** to an **External Account**. This account bridges the two Ledgers and always belongs to one Asset.
2. **Credit to the destination Ledger:** Create a second transaction. It moves the same amount from the **External Account** into the target account in **Ledger 2**.
In this two-step process, each Ledger processes a valid, independent transaction. The External Account keeps both sides consistent.
You can use [Flowker](/en/flowker/what-is-flowker) to automate this orchestration. Flowker can add validations, conditions, and rollback logic if you need them.
## Key characteristics
***
* Ledgers ensure financial integrity and operational transparency.
* All Accounts and Transactions exist within a Ledger and form a complete financial system.
* Each Ledger keeps a balanced set of Accounts.
* You can use multiple Ledgers for segmentation, but cross-ledger transactions need explicit handling.
## Multiple Ledgers
***
Midaz lets you create **multiple Ledgers** for each organization, with more flexibility and control. This helps companies with diverse operational needs. For example, they can separate financial records by country, department, or business function.
### Example use case
Consider a global enterprise with business in multiple regions:
* Ledger 1: US Business
* Ledger 2: EU Business
* Ledger 3: APAC Business
With separate Ledgers, the company keeps region-specific transaction records, tax rules, and currency settings. It still has unified oversight of all operations.
### When to create a new Ledger
A Ledger is a **hard boundary**, not just a label. Accounts in different Ledgers cannot transact directly. You must orchestrate every cross-Ledger movement through an External Account (see [How to orchestrate cross-Ledger transactions](#how-to-orchestrate-cross-ledger-transactions)). This cost drives the decision. Create a new Ledger only when the segments rarely move money between each other.
Use this heuristic to pick the right level of separation:
| Mechanism | What it separates | Reach for it when |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ledger** | A complete, self-contained set of books — its own accounts, balances, and asset configuration | You need true isolation: a separate legal entity, country, or regulatory regime, where balances should never mix and cross-segment movements are rare. |
| **[Portfolio](/en/midaz/portfolios)** | A grouping of accounts *within* one Ledger | You want a consolidated view of a customer or business unit, but the accounts still transact freely with the rest of the Ledger. |
| **[Account](/en/midaz/accounts) / [Account Type](/en/midaz/account-types)** | An individual balance and its rules | A single balance needs its own truth — its own statement, nature, or restrictions. |
In short: if two sets of balances often transact with each other, keep them in the **same** Ledger. Separate them with Portfolios or Accounts. Split into multiple Ledgers only when the isolation is worth the orchestration overhead.
## Customizing Ledgers
***
In Midaz, you can tailor each Ledger's configuration to your requirements. Assets are scoped to a Ledger, so the same code is an independent record in each Ledger with its own name, type, status, and metadata.
**Example** To represent Bitcoin (BTC) across Ledgers, create the BTC asset independently in each Ledger and set the name, status, and metadata each Ledger needs.
Amounts in Midaz are arbitrary-precision decimals. There is no per-asset or per-Ledger decimal-place setting — you do not configure a scale when you create an asset.
This approach gives you granular control over asset management for your operational or regulatory needs.
## Benefits of using Ledgers in Midaz
***
* **Clear organization**: Keep clear, distinct financial records for each part of your business.
* **Custom configurations**: Adjust asset settings, currencies, or other parameters per Ledger for local or departmental needs.
* **Scalability**: Add or change Ledgers as your organization grows or diversifies.
* **Transparency**: Get a complete view across all financial activities. Each Ledger adds to one operational picture.
## Ledger settings
***
Each Ledger has configurable settings that control how it validates transactions. For example, you can require every transaction to follow a predefined route. You can also require account types to match your business rules. The Ledger then rejects invalid movements before it records them.
You can change these settings at any time through the API. You do not need to restart the service or change code.
### Available settings
The following settings are available:
| Setting | Default | Description |
| --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `validateAccountType` | `false` | When enabled, each new account must use an account type registered for the Ledger. The Ledger rejects the account creation if the type is not registered. |
| `validateRoutes` | `false` | When enabled, every transaction must include an associated transaction route. Each operation is also validated against its operation route rules. |
Start with both settings disabled while you configure your operation routes and transaction routes. Once your routing setup is complete, enable validation to enforce your business rules.
### How settings affect transactions
When you enable `validateRoutes` for a Ledger:
1. Every transaction to that Ledger **must** reference a valid transaction route.
2. The Ledger validates each operation against the corresponding operation route rules.
3. The Ledger rejects any non-compliant transaction before it records the movement. This protects your ledger integrity.
When you enable `validateAccountType`:
1. The Ledger checks each new account against its registered account types.
2. If the account type is not registered, the Ledger rejects the account creation.
If you enable validation on a Ledger that already processes transactions, existing records do not change. Validation applies only to **new transactions** that you submit after you enable the setting.
### Managing settings via API
Use the Ledger Settings endpoints to retrieve and update these configurations:
* [Retrieve Ledger Settings](/en/reference/midaz/retrieve-ledger-settings) — View the current settings for a Ledger.
* [Update Ledger Settings](/en/reference/midaz/update-ledger-settings) — Change one or more settings with a partial update (deep merge).
Settings updates take effect immediately. Midaz applies changes with a deep merge and modifies only the fields you include in the request.
## Managing Ledgers
***
You can manage your Ledgers through the API or Lerian Console.
### View API
* [List Ledgers](/en/reference/midaz/list-ledgers) - View the details of all Ledgers.
* [Retrieve a Ledger](/en/reference/midaz/retrieve-a-ledger) - Get details of a specific Ledger.
* [Create a Ledger](/en/reference/midaz/create-a-ledger) - Register a new Ledger.
* [Update a Ledger](/en/reference/midaz/update-a-ledger) - Edit the details of an existing Ledger.
* [Delete a Ledger](/en/reference/midaz/delete-a-ledger) - Mark a Ledger as deleted.
When you delete a Ledger, Midaz does not remove it permanently.
Instead, Midaz sets a `deletedAt` timestamp. The Ledger is then excluded from every read and can no longer be used. Its `status.code` is left unchanged. This preserves historical data for audit trails.
### Via Lerian Console
You can view, create, edit, and delete Ledgers on the Ledgers page in the Midaz Module of Lerian Console.
[**Learn more in the Managing Ledgers guide.**](/en/midaz/console/managing-ledgers-via-console)
If you create your **first Ledger** in Lerian Console, you do it as part of the onboarding flow. To learn more, see the [Midaz Module onboarding](/en/midaz/console/midaz-console-onboarding) guide.
# Metadata indexes
Source: https://docs.lerian.studio/en/midaz/metadata-indexes
Create MongoDB metadata indexes on transactions, operations, and routes to speed up queries and filters on custom metadata keys in high-volume Midaz setups.
## Why this matters
***
Every entity in Midaz supports [metadata](/en/reference/metadata) — custom key-value pairs that extend the standard data model. Queries that filter a large collection by a metadata field can become slow without an index.
A metadata index is a MongoDB index on a specific metadata key. It turns an expensive collection scan into a fast indexed lookup. This matters most in production, where transaction volumes are high and you filter or sort by metadata values.
## How it works
***
When you create a metadata index, Midaz builds a MongoDB index on the `metadata.` field of the entity collection. After that, any query that filters by the metadata key uses the index. MongoDB finds the documents directly, without a full collection scan.
Indexes are:
* **Per-entity**: Each index targets a specific entity type (e.g., `transaction`, `operation`).
* **Per-key**: Each index covers a single metadata key.
* **Optional uniqueness**: You can require that no two documents share the same value for the indexed metadata key.
* **Sparse by default**: The index includes only documents that have the metadata key. This saves storage and speeds up writes.
## Supported entity types
***
Midaz supports metadata indexes for these entities:
| Entity | Collection | Module |
| :------------------ | :----------------- | :---------- |
| `transaction` | Transactions | Transaction |
| `operation` | Operations | Transaction |
| `operation_route` | Operation Routes | Transaction |
| `transaction_route` | Transaction Routes | Transaction |
| `organization` | Organizations | Onboarding |
| `ledger` | Ledgers | Onboarding |
| `account` | Accounts | Onboarding |
| `asset` | Assets | Onboarding |
| `segment` | Segments | Onboarding |
| `portfolio` | Portfolios | Onboarding |
| `account_type` | Account Types | Onboarding |
## Creating a metadata index
***
Use the [Create a Metadata Index](/en/reference/midaz/create-a-metadata-index) endpoint:
```json POST /v1/settings/metadata-indexes/entities/{entity_name} theme={null}
{
"metadataKey": "tier",
"unique": false,
"sparse": true
}
```
**Parameters:**
| Field | Type | Required | Description |
| :------------ | :------ | :------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| `metadataKey` | string | Yes | The metadata key to index. Must start with a letter and contain only alphanumeric characters and underscores. Max 100 characters. |
| `unique` | boolean | No | Whether the index enforces uniqueness across documents. Default: `false`. |
| `sparse` | boolean | No | Whether the index only includes documents that have the metadata key. Default: `true`. |
**Response:**
```json JSON theme={null}
{
"indexName": "metadata.tier_1",
"entityName": "transaction",
"metadataKey": "tier",
"unique": false,
"sparse": true
}
```
## Listing metadata indexes
***
Use the [List Metadata Indexes](/en/reference/midaz/list-metadata-indexes) endpoint. It returns all indexes across all entity types with their usage statistics:
```json GET /v1/settings/metadata-indexes theme={null}
[
{
"indexName": "metadata.tier_1",
"entityName": "transaction",
"metadataKey": "tier",
"unique": false,
"sparse": true,
"stats": {
"accesses": 1523,
"statsSince": "2024-12-01T10:30:00Z"
}
}
]
```
The `stats.accesses` field shows how many queries used the index since statistics collection started. Use it to find unused indexes that you can safely delete.
## Deleting a metadata index
***
Use the [Delete a Metadata Index](/en/reference/midaz/delete-a-metadata-index) endpoint:
```
DELETE /v1/settings/metadata-indexes/entities/{entity_name}/key/{index_key}
```
Deletion takes effect immediately. It affects query performance for any operation that used the index. Before you delete an index, make sure no critical query depends on it.
## Performance considerations
***
**When to create indexes:**
* You frequently filter transactions or operations by a specific metadata key (e.g., `tier`, `channel`, `partner_id`).
* List queries on a large collection are slow when they filter by metadata.
* You need to enforce uniqueness on a metadata field (e.g., external reference IDs).
**When NOT to create indexes:**
* You rarely query by the metadata key — the index only costs storage and slows writes.
* The collection is small enough that full scans are fast.
* You want to add an index speculatively, "just in case."
**Limits:**
Midaz does not enforce its own cap on the number of metadata indexes per entity — MongoDB's per-collection index limit applies instead. Creating an index on a key that already has one returns error `0132` (Metadata Index Already Exists). Keep the index count deliberate: list the indexes and delete any with low or zero `accesses`.
Start with indexes on the metadata keys you query in production. Use the `stats.accesses` field from the list endpoint to make sure queries use each index. Delete the indexes that no query uses.
## Related pages
***
* [Metadata](/en/reference/metadata) — How metadata works across all Midaz entities.
* [Create a Metadata Index](/en/reference/midaz/create-a-metadata-index) — API reference.
* [List Metadata Indexes](/en/reference/midaz/list-metadata-indexes) — API reference.
* [Delete a Metadata Index](/en/reference/midaz/delete-a-metadata-index) — API reference.
# Midaz for academic records
Source: https://docs.lerian.studio/en/midaz/midaz-for-academic-record-management
Use Midaz to record enrollments, grades, and achievements as immutable Ledger transactions — automated transcripts and audit-ready histories.
Universities manage thousands of student records across many departments. The data sits in separate enrollment systems, grading platforms, and administrative databases. When an audit starts or a student requests a transcript, staff rebuild a complete history by hand. This process is slow and error-prone.
Midaz treats each academic activity as a Ledger transaction. It records every enrollment, grade, and achievement immutably, with a full audit trail — the same way it records financial movements.
### Benefits
* **Security and integrity**: Every academic record is an immutable transaction. You cannot change it after you record it, which removes disputes about grades or enrollment history.
* **Transparency**: Students, advisors, and administrators see a single, consistent view of the complete academic record.
* **Efficiency**: Transcript generation, GPA calculations, and audit preparation become automated queries instead of manual processes.
## What changes with Midaz
***
| Without Midaz | With Midaz |
| :------------------------------------------------------------------- | :----------------------------------------------------------------------------------- |
| Student records spread across enrollment, grading, and admin systems | Single ledger tracks all academic activities as immutable transactions |
| Transcript generation requires manual cross-referencing | Query a student's portfolio for a complete, auditable academic history |
| Grade disputes involve digging through logs and email chains | Every grade assignment is a timestamped, immutable transaction with full audit trail |
| GPA calculations done manually or in disconnected spreadsheets | Balance queries on grade accounts calculate GPA automatically |
| Audit preparation takes weeks of data gathering | All records are audit-ready by design — query any time range instantly |
## Using Midaz
***
* **Organization & Ledger:** The university registers as an Organization in Midaz. It creates a Ledger to track all academic activities as transactions.
* **Assets:** Create an Asset for each course, with a unique identifier.
* **Student Portfolios:** For each student, create a Portfolio. The Portfolio acts as the academic record and holds all relevant academic metrics.
* **Accounts for academic metrics:** Create accounts for the academic metrics inside each student's Portfolio. Examples include:
* **Course enrollments Account**: Records the courses the student enrolls in.
* **Grades Account**: Tracks the grades the student receives.
* **Attendance Account**: Monitors class attendance.
* **Achievements Account**: Records extracurricular achievements and recognitions.
* **Transactions**: Use transactions to update accounts based on academic activities:
* **Course enrollment**: When a student enrolls in a course, create a transaction. The transaction moves an entry from a course pool account (e.g., `@external/available-courses`) to the student's "Course Enrollments" account.
* **Grade assignment**: When an instructor assigns a grade, create a transaction. The transaction moves "funds" (the grades) from the class grade account (e.g., `@external/grades`) to the student's grade account.
* **Attendance marking**: To mark attendance, move funds from an attendance account (e.g., `@external/monitors`) to the student's "Attendance" account.
* **Achievements**: Similarly, add achievements to the "Achievements" account via transactions.
* **Reports**: Query the ledger to build a full academic record for each student, across enrollments, grades, attendance, and achievements. These queries support academic counseling and transcript generation.
* For example, retrieve all transactions for the Grades Account. Use the available balance to calculate the student's GPA.
# Midaz for multi-account customers
Source: https://docs.lerian.studio/en/midaz/midaz-for-multi-account-customers
Model multiple Midaz accounts under a single customer — with segregated balances, ledgers, statements, and external identifiers per account.
A single customer rarely has a single balance. The same person can hold a main account, a benefit account, a blocked account, a product sub-account, or a promotional balance. Each balance has its own rules, its own statement, and its own reconciliation needs.
The common shortcut treats these as labels on one account and sorts them out in application code. That works until balances, ledgers, statements, or operational rules must diverge. At that point, a single account can no longer tell the truth about where the money is.
This page shows the reference architecture for **one customer with many Midaz accounts**. It maps each part of the model to a native platform entity. Then it walks through an example: three accounts for the same customer document.
## Why this matters
***
For **product and operations teams**, each balance becomes its own account. The Ledger then enforces every segregation rule — a blocked outflow, a benefit-only spend, a promotional balance with an expiry. The rule lives in the Ledger, not in application logic. Each account carries its own statement and reconciliation trail.
For **engineering teams**, each external address resolves to a specific account before Midaz posts a transaction. Core banking numbers and payment-rail identifiers each point to one balance. You never guess which balance an incoming event belongs to. There is no separate balance store to keep in sync with the Ledger.
| One account with labels | Many accounts per customer |
| ------------------------------------------------------- | ----------------------------------------------------------------- |
| Balance "types" live in metadata or app code | Each balance is its own Account with its own ledger and statement |
| Blocking or restricting funds requires custom logic | Account Type and route rules enforce restrictions at the Ledger |
| Statements must be filtered and reassembled per balance | Each account produces a clean, independent statement |
| External identifiers all point to the same balance | Each external identifier resolves to a specific account |
| Reconciliation mixes unrelated movements | Reconciliation is separated by account by design |
## The reference architecture
***
The model is **one owner, N accounts, N external identifiers**:
* **One owner** — the customer, identified by a document (CPF, CNPJ, tax ID). The owner represents *who* holds the relationship. It does not carry a balance and does not decide transaction routing.
* **N accounts** — each account is a self-contained accounting position, with its own balance, ledger, statement, and rules.
* **N external identifiers** — the addresses other systems (a core banking platform, a payment rail) use to reach a specific account. Each identifier resolves to exactly one account.
A middleware layer keeps the map between external identifiers and accounts. It resolves each identifier to the correct account *before* the call to Midaz. Midaz remains the source of truth for accounts, balances, and entries.
When balance, ledger, statement, or operational rule differs by destination, point each identifier at a distinct account. An external identifier is **not** just a nickname for one shared balance.
## How Midaz maps the model
***
Every part of this architecture maps to a native Midaz entity. You do not build a separate balance store or invent an accounting layer.
| Concept | Midaz entity | What it does |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| The owner (customer) | [**Holder**](/en/midaz/crm/holders) | Identity behind the accounts (`NATURAL_PERSON` or `LEGAL_PERSON`), keyed by `document`. Holds no balance. |
| Each balance position | [**Account**](/en/midaz/accounts) | Source of truth for balance, entries, and statement. |
| The nature of each balance | [**Account Type**](/en/midaz/account-types) | Classifies an account (main, benefit, blocked) and enables route validation. |
| All accounts of one customer | [**Portfolio**](/en/midaz/portfolios) | Groups a customer's accounts to view the total relationship. |
| External address → account | [**Account alias**](/en/midaz/accounts#account-aliases) + [**`entityId`**](/en/midaz/accounts#entity-id-external-system-reference) | The alias is how transactions address an account; `entityId` links it to an external system's identifier. |
| Banking and regulatory context | [**Instrument (CRM)**](/en/midaz/crm/crm-getting-started) | Attaches branch, account number, and regulatory fields. Links a Holder to a specific account. |
Much of what an external "alias registry" would do is already native. The account **alias** is the in-Ledger address. **`entityId`** stores your external system's identifier. The middleware's job is narrow — translate an external rail identifier into the right account alias, then post.
## Prerequisites
***
This example assumes a running Midaz environment with the following in place:
| Requirement | Details |
| --------------------------- | -------------------------------------------------------------------------------------- |
| **Midaz** (v3.x.x+) | Core Ledger with an Organization and Ledger already created |
| **A registered asset** | `BRL` registered as the operating asset in the Ledger |
| **Account Type validation** | Enabled per-Ledger so each account's nature is enforced (see Step 1) |
| **CRM** (optional) | Part of the ledger binary — use it to attach identity, banking, and regulatory context |
Midaz represents values in the smallest unit of the currency. For BRL, `15000` means R\$ 150.00 (centavos).
## Building three accounts for one customer
***
The customer with document `12345678900` needs three accounts. The **main** account is free for ordinary movement. The **benefit** account follows product rules. The **blocked** account accepts inflows but restricts outflows.
Turn on validation so every account must declare a registered type. This is what lets the Ledger enforce each account's nature.
```json theme={null}
PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings
{
"accounting": {
"validateAccountType": true
}
}
```
Settings take effect immediately — no redeployment needed.
Create one Account Type per balance nature. The `keyValue` is what each account's `type` field must match.
```json theme={null}
POST /v1/organizations/{org_id}/ledgers/{ledger_id}/account-types
{
"name": "Main Account",
"description": "Ordinary account, free for regular movement",
"keyValue": "main_account"
}
```
Repeat for `benefit_account` (movement under product rules) and `restricted_account`. The `restricted_account` is the **blocked** account: it accepts inflows but conditions or blocks outflows.
Each account links to the `BRL` asset and declares its `type`. It carries an `alias` (its in-Ledger address) and an `entityId` (its identifier in your external system).
```json theme={null}
POST /v1/organizations/{org_id}/ledgers/{ledger_id}/accounts
{
"name": "Main account — 12345678900",
"assetCode": "BRL",
"alias": "@cust_12345678900_main",
"entityId": "0001/12345-1",
"type": "main_account"
}
```
Create the benefit account with `alias` `@cust_12345678900_benefit`, `entityId` `0001/88888-2`, and `type` `benefit_account`. Create the blocked account with `alias` `@cust_12345678900_blocked`, `entityId` `0002/77777-0`, and `type` `restricted_account`.
The `entityId` is where you store the external address that other systems use to reach this account — your map between Midaz and your source system.
Create one Holder for the customer. The same Holder owns all three accounts and keeps identity in one place.
In Midaz v4, CRM is part of the ledger binary, so it needs no separate service or port. The organization ID travels in the URL path — see [Getting started with CRM](/en/midaz/crm/crm-getting-started) for the full schema.
```bash theme={null}
curl -X POST http://localhost:3002/v1/organizations/{org_id}/holders \
-H "Content-Type: application/json" \
-d '{
"type": "NATURAL_PERSON",
"name": "Jane Smith",
"document": "12345678900",
"contact": {
"primaryEmail": "jane.smith@example.com"
}
}'
```
Save the returned `holderId` — you'll use it in the next step.
Create one Instrument per ledger account to attach banking and regulatory context. The Instrument powers CRM-driven features and keeps customer-facing details out of the Ledger.
```bash theme={null}
curl -X POST http://localhost:3002/v1/organizations/{org_id}/holders/{holder_id}/instruments \
-H "Content-Type: application/json" \
-d '{
"ledgerId": "",
"accountId": "",
"bankingDetails": {
"branch": "0001",
"account": "12345",
"type": "CACC",
"countryCode": "BR"
},
"metadata": {
"purpose": "main"
}
}'
```
Notice how the main account's `entityId` (`0001/12345-1`) decomposes into the `branch` (`0001`) and `account` (`12345`) you record here. That external address now resolves to one specific account. Repeat for the benefit and blocked accounts, and point `accountId` at each one.
The result: one customer, three accounts, three distinct external addresses. Each account keeps its own balance, statement, and rules under a single Holder.
| Owner | External identifier | Midaz account | Use | Treatment |
| ------------- | ------------------- | ------------------- | ----------------------- | ---------------------------------------------- |
| `12345678900` | `0001/12345-1` | `@cust_..._main` | Main account | Free for ordinary movement |
| `12345678900` | `0001/88888-2` | `@cust_..._benefit` | Benefit account | Movement governed by product rules |
| `12345678900` | `0002/77777-0` | `@cust_..._blocked` | Court-ordered / blocked | Inflow allowed, outflow conditioned or blocked |
## The transaction boundary
***
When an external event arrives, the resolution happens *before* the call to Midaz. Each layer stays in its lane, and that preserves accounting clarity.
A transaction, query, or settlement arrives with an external identifier.
The middleware looks up the external identifier and resolves it to the correct Midaz account alias. It also applies status validation (active, blocked, closed).
Midaz records the entry against the resolved account and preserves balance and ledger. Statement and reconciliation stay separate by account.
| Layer | Responsibility | Should not do |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| **CRM / registration** | Hold the commercial and identity view of the customer, including the link between document and relationship. | Transaction routing, destination decisions, or settlement rules. |
| **Middleware** | Resolve external identifiers to a Midaz account before the transaction, and apply status validation. | Invent balances, duplicate accounting, or depend on the CRM in real time for routing. |
| **Midaz** | Record accounts, balances, entries, ledgers, and statements as the financial source of truth. | Know external-rail details beyond the identifiers needed for integration. |
An identifier for a blocked or closed account must fail validation **before** the call to Midaz. Routing decisions belong in the middleware. The Ledger stays the source of truth for balances and entries.
## What this unlocks
***
* **Real segregation** — each balance has its own ledger and statement. You cannot spend a blocked balance through the main account by accident.
* **Unambiguous routing** — every external event has a single, well-defined destination account.
* **Centralized identity** — one Holder owns many accounts. Identity and contact data live in one place while balances stay separate.
* **Native, not bolted-on** — accounts, types, aliases, and `entityId` are platform primitives, so there's no parallel balance store to reconcile against the Ledger.
## What you need to get started
***
| Requirement | Details |
| --------------------------- | ----------------------------------------------------------------------------------- |
| **Midaz** (v3.x.x+) | Organization, Ledger, and a registered asset |
| **Account Type validation** | Enabled per-Ledger via the [Ledger Settings API](/en/midaz/ledgers#ledger-settings) |
| **Account Types** | One per balance nature (main, benefit, blocked, …) |
| **Accounts** | One per balance, each with an `alias` and an `entityId` |
| **CRM** (optional) | A Holder per customer plus an Instrument per ledger account |
## Next steps
***
The core financial unit — aliases, `entityId`, and external accounts.
Classify accounts and enforce their nature with route validation.
Group a customer's accounts to view the total relationship.
Centralize identity and attach banking and regulatory context.
# Midaz for supply chains
Source: https://docs.lerian.studio/en/midaz/midaz-for-supply-chain-management
Use Midaz as a ledger for supply chain management, recording every raw material, production, and shipping movement as an immutable, auditable transaction.
Manufacturing companies manage complex material flows. Raw materials arrive from many suppliers, move through production stages, become finished goods, and ship to customers. A quality issue or a regulatory audit raises one question. Where did this material come from, and where did it go? Today that answer takes days of cross-referencing spreadsheets and ERP exports.
Midaz records every material movement as an immutable ledger transaction. From receiving to production to shipping, you get a complete and auditable history of your supply chain.
### Benefits
* **Transparency and traceability**: An immutable audit trail documents every step. Recall investigations and compliance audits become straightforward.
* **Efficiency**: Real-time balance queries show inventory levels and production stages. You avoid manual stock checks and planning delays.
* **Security**: Tamper-proof records block unauthorized changes to supply chain data. Your regulatory reporting stays accurate.
## What changes with Midaz
***
| Without Midaz | With Midaz |
| :-------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- |
| Material tracking spread across ERP modules, spreadsheets, and emails | Every material movement is a ledger transaction with full audit trail |
| Recall investigations take days of cross-referencing multiple systems | Query the ledger for the complete journey of any material — instant traceability |
| Inventory counts done manually or through periodic reconciliation | Real-time balance queries show current stock at any stage (raw, in-production, finished) |
| Supplier disputes lack authoritative records | Immutable transactions provide tamper-proof evidence of every delivery and movement |
| Regulatory audits require weeks of data preparation | All records are audit-ready by design — export any time range on demand |
## Using Midaz
***
* **Assets**: Set up each supply chain item as an Asset with a unique code. Assets cover everything from raw materials to final products.
* **Portfolios**: Create a portfolio for each supplier and warehouse. A portfolio groups the accounts that track material and goods flow.
* **Accounts**: Within each portfolio, create an account for each supply chain stage. For example:
* **Raw materials account**: Tracks raw materials held in the warehouse.
* **In-production account**: Monitors materials in the production process.
* **Finished goods account**: Records completed products ready for distribution.
* **Shipment account**: Tracks items in transit to customers or other warehouses.
* **Transactions**: Use a transaction to update accounts as materials move through each stage. For example:
* **Receiving materials**: When materials arrive, a transaction moves the amount from the supplier's Raw Materials account to the warehouse's Raw Materials account.
* **Production**: During production, a transaction moves the amount from the Raw Materials account to the In-Production account.
* **Production completion**: When production finishes, a transaction moves the amount from the In-Production account to the Finished Goods account.
* **Shipping**: When goods ship, a transaction moves the amount from the Finished Goods account to the Shipment account.
* **Reports**: Query Midaz for current inventory levels, production output, and shipment status. Use the data to plan stock, production schedules, and logistics.
* **Compliance and audits**: The immutable Midaz ledger keeps supply chain records secure and audit-ready. It meets regulatory requirements and standards.
# Getting started with Midaz
Source: https://docs.lerian.studio/en/midaz/midaz-getting-started
Run Midaz locally in about ten minutes: clone the repo, start infrastructure, create your first Organization, Ledger, Accounts, and process a first transaction.
In this guide, you set up a working Midaz environment. You then run the core workflow behind any financial application on the platform. You create an organization, a ledger, and accounts, and then process your first transaction.
By the end, you have a running ledger ready for your use case. This can be payments, lending, marketplace settlement, or internal treasury.
## Prerequisites
***
Before you begin, install these tools:
| Tool | Minimum version | Check command |
| ---------------------------------------------------------- | --------------- | ------------------------ |
| [Go](https://go.dev/dl/) | 1.26.4+ | `go version` |
| [Docker](https://docs.docker.com/get-docker/) | 24+ | `docker --version` |
| [Docker Compose](https://docs.docker.com/compose/install/) | 2.20+ | `docker compose version` |
| [Make](https://www.gnu.org/software/make/) | 3.81+ | `make --version` |
| [Git](https://git-scm.com/) | 2.30+ | `git --version` |
Midaz runs on **macOS** (Apple Silicon and Intel) and **Linux** (amd64). On Windows, run it through **WSL2**.
## Step 1 — Clone the repository
***
Clone the Midaz repository and move into the project directory.
```bash Terminal theme={null}
git clone https://github.com/LerianStudio/midaz.git
cd midaz
```
## Step 2 — Set up environment files
***
Midaz uses `.env` files to configure each component. Generate them from the provided examples:
```bash Terminal theme={null}
make set-env
```
This command copies `.env.example` to `.env` in each component directory. The default values work for local development. You do not need to change them.
Docker must already be running for this step: `make set-env` also generates the `LCRYPTO_*` CRM keys through a Docker one-shot container, and fails if Docker is unavailable.
## Step 3 — Start the infrastructure
***
Start the supporting services that Midaz needs: PostgreSQL, MongoDB, Valkey, RabbitMQ, Redpanda, and OpenTelemetry.
```bash Terminal theme={null}
make infra COMMAND=up
```
Wait until all containers report a healthy status. Check their status with:
```bash Terminal theme={null}
docker compose -f components/infra/docker-compose.yml ps
```
Infrastructure services use the following default ports:
| Service | Port |
| -------------------- | ----- |
| PostgreSQL Primary | 5701 |
| PostgreSQL Replica | 5702 |
| MongoDB | 5703 |
| Valkey (Redis) | 5704 |
| Grafana (OTEL) | 3100 |
| RabbitMQ AMQP | 3003 |
| RabbitMQ Management | 3004 |
| Redpanda (Kafka API) | 19092 |
## Step 4 — Start Midaz
***
Midaz runs as a single **Ledger** service that includes the onboarding and transaction domains. Start it with:
```bash Terminal theme={null}
make up
```
This command starts the infrastructure, if needed, runs the ledger migration container, and then starts the Midaz services — the ledger and Tracer. All ledger APIs are available on **port 3002**.
Check that Midaz responds:
```bash Terminal theme={null}
curl http://localhost:3002/health
```
You should receive a `200 OK` response.
## Step 5 — Create an organization
***
An organization represents the business entity behind the financial operation: your company, a client, or a regulated institution. In production, it maps to the legal entity that holds your ledgers, accounts, and transactions.
For the complete endpoint specification, see [Create an Organization](/en/reference/midaz/create-an-organization).
```bash Terminal theme={null}
curl -X POST http://localhost:3002/v1/organizations \
-H "Content-Type: application/json" \
-d '{
"legalName": "Acme Corp",
"legalDocument": "12345678000100",
"status": {
"code": "ACTIVE"
},
"address": {
"country": "BR"
}
}'
```
Save the `id` returned in the response. You use it in the next steps as `{organization_id}`.
## Step 6 — Create a ledger
***
A ledger is an isolated book of records within an organization. You can create separate ledgers for different financial domains, such as payments, fee collection, or settlement. Each ledger has its own accounts and transaction history.
For the complete endpoint specification, see [Create a Ledger](/en/reference/midaz/create-a-ledger).
```bash Terminal theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers \
-H "Content-Type: application/json" \
-d '{
"name": "Primary Ledger",
"status": {
"code": "ACTIVE"
}
}'
```
Save the returned `id` as `{ledger_id}`.
## Step 7 — Create an asset
***
An asset defines the unit of value tracked in the ledger. This can be a fiat currency like BRL or USD. It can also be loyalty points, crypto tokens, securities, or any custom unit your business tracks.
You must create at least one asset before you create accounts.
For the complete endpoint specification, see [Create an Asset](/en/reference/midaz/create-an-asset).
```bash Terminal theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/assets \
-H "Content-Type: application/json" \
-d '{
"name": "Brazilian Real",
"type": "currency",
"code": "BRL",
"status": {
"code": "ACTIVE"
}
}'
```
## Step 8 — Create accounts
***
Accounts represent the participants or buckets in your financial flow: a customer wallet, a revenue pool, a merchant account, or an internal reserve. Each account links to a single asset and follows double-entry accounting rules.
You need at least two accounts to process a transaction: one to debit (source) and one to credit (destination).
For the complete endpoint specification, see [Create an Account](/en/reference/midaz/create-an-account).
Create a source account:
```bash Terminal theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts \
-H "Content-Type: application/json" \
-d '{
"name": "Revenue Account",
"assetCode": "BRL",
"type": "deposit",
"status": {
"code": "ACTIVE"
},
"alias": "@revenue"
}'
```
Create a destination account:
```bash Terminal theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts \
-H "Content-Type: application/json" \
-d '{
"name": "Customer Account",
"assetCode": "BRL",
"type": "deposit",
"status": {
"code": "ACTIVE"
},
"alias": "@customer-001"
}'
```
## Step 9 — Process your first transaction
***
This is the core action: it moves value between accounts with full traceability. Midaz records every transaction as a balanced operation. It debits the source and credits the destination, so your books stay consistent by design.
For the complete endpoint specification, see [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json).
```bash Terminal theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/transactions/json \
-H "Content-Type: application/json" \
-d '{
"description": "First transaction",
"send": {
"asset": "BRL",
"value": "1000",
"source": {
"from": [
{
"accountAlias": "@revenue",
"amount": {
"asset": "BRL",
"value": "1000"
}
}
]
},
"distribute": {
"to": [
{
"accountAlias": "@customer-001",
"amount": {
"asset": "BRL",
"value": "1000"
}
}
]
}
}
}'
```
This transaction sends **R\$ 10.00** from `@revenue` to `@customer-001`. The value `"1000"` represents 10.00 in BRL's smallest unit, cents.
Midaz uses integer values to avoid floating-point errors. This is standard practice in financial systems.
## Step 10 — Verify the balance
***
Check the destination account balance to confirm the transaction.
For the complete endpoint specification, see [Retrieve a Balance by Account Alias](/en/reference/midaz/retrieve-a-balance-by-account-alias).
```bash Terminal theme={null}
curl http://localhost:3002/v1/organizations/{organization_id}/ledgers/{ledger_id}/accounts/alias/@customer-001/balances
```
The returned balance should reflect the credited amount. At this point, you have a working ledger that processes real transactions.
## Explore the API
***
Midaz can serve its OpenAPI 3.1 spec and interactive API documentation. The docs surface is off by default. To enable it, set `OPENAPI_DOCS_ENABLED=true` in `components/ledger/.env` and restart Midaz. Then access:
* **API docs**: `http://localhost:3002/v1/docs`
* **OpenAPI spec**: `http://localhost:3002/v1/openapi.json` (or `/v1/openapi.yaml`)
## Observability
***
Midaz ships with a preconfigured Grafana instance integrated with OpenTelemetry.
* **Grafana dashboard**: `http://localhost:3100`
* **Default credentials**: `midaz` / `lerian`
From Grafana, you can explore logs, traces, and metrics across all Midaz services.
## Stopping Midaz
***
To stop all services:
```bash Terminal theme={null}
make down
```
To remove containers and volumes and start from a clean environment:
```bash Terminal theme={null}
make clean-docker
```
## Next steps
***
New to Midaz? Start with [Midaz entities](/en/midaz/core-entities) to understand organizations, ledgers, accounts, and transactions.
Learn the different ways to create transactions, including JSON, inflow, and outflow, and when to use each.
Deploy Midaz to Kubernetes using the official Helm chart.
Manage holders and alias accounts to connect real-world identities to your Midaz accounts.
Add Fees Engine, Pix, and other capabilities to your Midaz deployment.
# Midaz in a global portfolio
Source: https://docs.lerian.studio/en/midaz/midaz-in-a-global-portfolio
Model multi-currency customer relationships in Midaz — group BRL, USD, and EUR Accounts under a single Portfolio for a unified global banking view.
With Midaz, you can build a global account structure like the ones major banks offer. Clients hold accounts in multiple currencies, such as BRL, USD, and EUR. You manage every currency account under a single client profile.
## What changes with Midaz
***
| Without Midaz | With Midaz |
| :---------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| Multi-currency accounts require separate systems or complex workarounds | Each currency is an Asset; accounts in different currencies grouped under one Portfolio |
| No unified customer view across currencies | A single portfolio shows all of a client's accounts — BRL, USD, EUR — in one place |
| Currency-specific reporting built manually per system | Query all accounts under a portfolio via API for consolidated financial views |
| Adding a new currency means provisioning new infrastructure | Create a new Asset and an account — the ledger handles the rest |
## Using Midaz
***
* **Organization and Ledger:** ABC Bank registers as an Organization in Midaz. It then creates a Ledger to track all customer transactions.
* **Assets:** The bank defines an Asset for each currency it supports.
* **Customer Portfolios:** Each customer has a Portfolio. It groups their checking, savings, and credit accounts in one place.
* **Customer Accounts:** Create an Account for each currency the customer uses. Group these accounts under the customer's Portfolio.
### Example: Maria’s international business needs
Maria is an entrepreneur in Spain who runs an e-commerce business that sells handmade crafts worldwide. Her customers are in the U.S., Europe, and Japan, so she manages several currencies.
She opened an account with **ABC Bank**, which uses Midaz to manage its transactions.
# Midaz in banking
Source: https://docs.lerian.studio/en/midaz/midaz-in-banking
Discover how Midaz powers retail, corporate, and personalized banking through Portfolios, Segments, and dedicated settlement Accounts.
Midaz supports three banking models: **retail banking**, **corporate banking**, and **personalized banking services**. It uses portfolios and segments to model each one.
## What changes with Midaz
***
| Without Midaz | With Midaz |
| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------ |
| Customer accounts scattered across legacy systems with no unified view | Single ledger with portfolios grouping all accounts per customer |
| Adding new products (savings, credit) requires core system changes | New account types and segments configured via API — no code changes to the ledger |
| Fee and interest tracking mixed with operational accounts | Dedicated internal accounts for fee income, interest expense, and settlement |
| Segmentation (VIP, Student) enforced manually per product | Segments classify accounts, and the Fees Engine waives fees for a whole segment |
| Corporate hierarchies require custom workarounds | Native child accounts and multi-portfolio support for subsidiaries |
## Driving innovation in banking
***
Banks add products without changing core ledger structures. You configure new account types and segments through the API.
## Retail banking for individual customers
***
A retail bank runs on a single organization and a primary ledger. Each customer maps to a portfolio.
### Using Midaz
* **Organization & Ledger:** ABC Bank registers as an organization. It creates a "Retail Banking Ledger" for all customer transactions.
* **Assets:** The bank defines an asset for every currency it operates. This gives multi-currency support for domestic and international accounts.
* **Customer portfolios:** Each customer has a portfolio. It holds their checking, savings, and credit accounts.
* **Transactions:** Midaz records deposits, transfers, and payments with full traceability.
* **Internal accounts:** Fee and interest accounts track the bank's revenue and expenses.
* **Segments for personalization:** You classify accounts into service tiers, such as Standard or Gold. The Fees Engine can waive fees for a tier.
* **Child accounts for budgeting:** Customers create sub-accounts inside their savings portfolio for better money management.
#### Example: ATM withdrawal
John Doe withdraws \$100 from an ATM:
\$100 from John’s checking account.
\$100 to an "ATM Cash Dispensed" internal account.
Midaz debits \$3 from John’s account and credits the Fee Income account.
Midaz handles millions of retail customers.
## Corporate banking for business and enterprise customers
***
Corporate banking needs a more detailed structure. It often uses separate ledgers or segments to keep it apart from retail banking.
### Using Midaz
* **Dedicated Ledger:** ABC Bank creates a "Corporate Banking Ledger" for business accounts.
* **Portfolios for enterprises:** Each corporate client, such as ACME Corp, gets a portfolio with accounts for operations, payroll, and taxes.
* **Hierarchical accounts:** Child accounts let a business split funds across departments and keep one consolidated balance.
* **Bulk transactions:** Midaz supports large operations, such as payroll with many simultaneous debits and credits.
* **Segments & Products:** You classify companies as SME or Large Enterprise to offer tailored services.
* **Multi-entity clients:** You give each subsidiary its own portfolio under the same organization.
#### Example: loan disbursement & supplier payments
ACME Corp receives a \$1,000,000 loan:
\$1,000,000 to ACME’s loan account.
\$1,000,000 from the bank’s Loans Receivable account.
* \$100,000 payment to a supplier at ABC Bank (internal transfer).
* \$50,000 payment to an external supplier (external transfer recorded in the clearing account).
Midaz gives complete visibility into large operations.
## Portfolios and Segments for personalized services
***
Banks offer custom financial products. Midaz supports this through portfolios and segments.
### Customization strategies
* **Youth & student accounts:** You assign these accounts to a "Student" segment. The Fees Engine waives their fees.
* **Premium banking:** You group "Diamond" accounts in a premium segment. You apply fee rules to the whole segment.
* **Co-branded & special products:** You define loyalty points as an asset and track them in a customer's account.
* **Unified account views:** An application lists all accounts under a portfolio in one call.
* **Cross-portfolio operations:** A customer with a personal and a business portfolio transfers funds between them. The accounts stay separate in each portfolio.
#### Example: student account perks
A new customer signs up as a student.
You assign their account to the "Student" segment.
The Fees Engine waives their monthly fees.
When the customer reaches a set age, you move the account to a different segment.
# Midaz models
Source: https://docs.lerian.studio/en/midaz/midaz-models
Compare Midaz Community and Enterprise models — choose the licensing tier that fits your needs and pair it with SaaS or BYOC deployment.
Midaz offers two licensing models: **Community** and **Enterprise**. Community fits developers and teams that want the core ledger at no cost. Enterprise fits organizations that need scale, compliance, and support.
## Deployment flexibility
***
Both models are available under Lerian's [BYOC (Bring Your Own Cloud)](/en/deployment-models) model. You deploy and operate the platform in your own infrastructure. Enterprise customers can also choose the **SaaS** model, where Lerian manages the full infrastructure.
Your licensing model (Community or Enterprise) defines **what features you get**. Your deployment model (SaaS or BYOC) defines **how the platform runs**. These are independent choices.
For a full comparison, see [Deployment models](/en/deployment-models).
## Community model
***
The **Community** model gives you full access to Midaz's core features at no cost, with community support.
### Key benefits
* Free access to [Midaz's core features](/en/midaz/core-features).
* Community support through Discord and GitHub.
* Helm Charts for simpler deployments.
This model fits developers, startups, and teams that try Midaz without enterprise support or SLAs.
## Enterprise model
***
The **Enterprise** model fits organizations that require **scalability, compliance, and dedicated support**. It includes **everything in the Community model**, plus:
### Key benefits
* **Lifecycle management** that keeps your infrastructure optimized.
* **Software support** for mission-critical operations.
* **Advanced access management** and **role-based control**.
* **Onboarding assistance** to speed up adoption.
* **Software availability SLA** for uptime commitments.
* **Incident reports and security compliance**.
* **Plugin integration** in a preconfigured environment.
Plugins are available only to Enterprise clients.
If you want access on another plan, [get in touch](https://lerian.studio/contact). Our team can help you find the right option.
## Choosing the right model
***
The tables below help you compare both models and choose the right one.
### Services
| | Community | Enterprise |
| :------------------------------------ | :--------------------------- | :--------------------------- |
| **Community Forum (Github/Discord)** | | |
| **Helm Charts available** | | |
| **Lifecycle management** | | |
| **Observability (through Telemetry)** | | |
| **Software support** | | |
| **Software availability SLA** | | |
| **Incident reports** | | |
| **Onboarding assistance** | | |
### Enterprise features
| | Community | Enterprise |
| :---------------------------------- | :--------------------------- | :--------------------------- |
| **Single sign-on (SSO)** | | |
| **Workflows** | | |
| **Event streaming (webhooks)** | | |
| **Observability - Telemetry** | | |
| **Cross-organization transactions** | | |
| **Access Manager RBAC** | | |
| **Multi-tenant support** | | |
### Core features
| | Community | Enterprise |
| :-------------------------------------- | :--------------------------- | :--------------------------- |
| **Native event publisher** | | |
| **User credentials** | | |
| **Parent account ID** | | |
| **Client segments** | | |
| **Double-entry** | | |
| **Multi-currency/asset Support** | | |
| **Multi-organization/ledger Support** | | |
| **N:N transactions** | | |
| **Native race condition (per-account)** | | |
| **Self-reconciliation** | | |
| **Wallets portfolio** | | |
| **Terraform deploy** | | |
| **Visual interface** | | |
# Midaz production best practices
Source: https://docs.lerian.studio/en/midaz/midaz-production-best-practices
Set up Midaz for production with multi-AZ deployment, autoscaling, managed services, and the Kubernetes resilience patterns Lerian recommends.
Midaz runs in production at high volume. This guide gives you the deployment, high-availability, and observability patterns Lerian recommends. Follow them to keep downtime low and protect your data.
## Best-fit setup
***
Start a production deployment with these choices:
* Deploy across multiple availability zones.
* Run at least 3 worker nodes with autoscaling.
* Separate application and database workloads.
* Use managed services such as RDS, ElastiCache, and MongoDB Atlas.
* Apply Kubernetes patterns for resilience, security, and observability.
* Automate backups and alerting from day one.
## Infrastructure planning
***
### Cluster architecture
Plan the cluster for resilience and performance:
* Deploy across multiple availability zones.
* Run at least 3 worker nodes for high availability.
* Enable node autoscaling to absorb workload spikes.
* Separate application and database workloads when possible.
### Resource sizing
* Match node sizes to the expected workload.
* Give critical services enough resources first.
* Apply resource quotas to prevent contention.
* Monitor usage and tune the sizing over time.
### Storage
* Use SSD-backed storage for all database components.
* Define a storage class for each cloud provider.
* Provision volumes with headroom for growth.
* Use replicated or durable storage for critical data.
## Database architecture and high availability
***
Midaz uses CQRS (Command Query Responsibility Segregation) to separate reads from writes. This design lets you scale each path on its own.
### PostgreSQL
* Use a dedicated primary for writes and replicas for reads.
* Enable synchronous replication for critical data.
* Configure automatic failover with Patroni or AWS RDS.
* Monitor replication lag and consistency.
* Prefer managed services such as AWS RDS or GCP Cloud SQL.
### Redis / Valkey
* Deploy in cluster mode across multiple zones.
* Enable automatic failover with native clustering, or a Sentinel topology via `REDIS_MASTER_NAME`. Verify your client configuration — a managed service (ElastiCache, Memorystore) is the safer default.
* Use managed services such as AWS ElastiCache or GCP Memorystore.
### MongoDB
* Use replica sets with members across zones.
* Monitor role transitions and lag.
* Schedule regular backups.
* Do not write to secondaries unless you intend to.
* Use managed services such as MongoDB Atlas or AWS DocumentDB.
## Messaging infrastructure
***
Midaz runs two messaging surfaces, both off by default:
* **RabbitMQ** carries the internal async transaction balance-operation pipeline (`RABBITMQ_TRANSACTION_BALANCE_OPERATION_*`, enabled with `RABBITMQ_TRANSACTION_ASYNC=true`) plus the legacy outbound transaction, overdraft, and audit event exchanges. Use a managed RabbitMQ service such as AWS MQ or CloudAMQP in production.
* **RedPanda** (via lib-streaming) is the forward-looking event backbone. Set `STREAMING_ENABLED=true` and `STREAMING_BROKERS`; events publish to `lerian.streaming..` topics. Transaction-lifecycle and overdraft events currently publish to both transports during the migration window.
CQRS read/write separation is served by PostgreSQL replicas (`DB_*_REPLICA_*` DSNs), not by broker consumers rebuilding read models.
## High availability strategies
***
### Service redundancy
* Deploy multiple replicas for every service.
* Use anti-affinity rules to spread services across zones.
* Apply Pod Disruption Budgets to limit downtime during updates.
### Load balancing
* Use ingress controllers with health checks.
* Avoid session affinity unless a service requires it.
* Enable connection draining for smooth rollouts.
## Security considerations
***
### Network security
* Apply Kubernetes network policies to control traffic.
* Give each service account minimal permissions.
* Secure external access with TLS.
* Restrict admin interfaces with IP allowlists.
### Secret management
* Use Kubernetes Secrets for credentials and tokens.
* Rotate secrets on a regular schedule.
* Never hardcode secrets in containers or config files.
* Use an external secret manager for a stronger posture.
## Monitoring and observability
***
### Metrics
* Monitor the key application and infrastructure KPIs.
* Set alert thresholds that lead to action.
* Use dashboards for real-time visibility.
### Logging
* Centralize logs across all services.
* Use a structured format for easier filtering.
* Apply log retention and rotation policies.
* Define log-based alerts for critical events.
### Tracing
* Enable distributed tracing across services.
* Sample traces to balance performance and cost.
* Correlate traces with logs and metrics for full visibility.
### Alerting
* Create clear, reliable alerts.
* Tune thresholds to reduce noise.
* Route each alert through the right channel.
* Keep runbooks for recurring issues.
## Backup strategy
***
* Automate regular backups for critical systems.
* Store backups in more than one location or region.
* Test the restore procedure on a regular schedule.
* Keep the backup documentation current and accessible.
## Idempotency
***
Protect critical operations against duplicate processing in production:
* Send an idempotency key on every transaction create request with the `X-Idempotency` header.
* Use explicit, deterministic keys tied to your business IDs (order IDs, payment references), not auto-generated keys.
* Read the `X-Idempotency-Replayed` response header to tell a new transaction from a cached replay.
* Set the `X-TTL` header in seconds to match your retry window. The default is 300. Use a shorter value for synchronous flows and a longer one for asynchronous flows.
All Lerian products support idempotency through their own header conventions. For implementation details and a comparison across products, see [Retries and idempotency](/en/reference/retries-idempotency).
## Final notes
***
Align your infrastructure to the Midaz architecture and you gain:
* Clean read/write separation with CQRS.
* Compatibility with managed cloud services.
* A clear path to observability, failover, and secure operations.
Review your setup on a regular schedule to keep this foundation solid as you grow.
## What’s next?
***
Ready to scale, migrate, or harden your production environment?
* Read the [Midaz deployment guide](/en/midaz/deployment).
* [Contact our team](https://lerian.studio/contact) for tailored support.
# Midaz SDK for Go
Source: https://docs.lerian.studio/en/midaz/midaz-sdk-go
Build Go applications on top of Midaz with the official v4 SDK — typed pagination, structured errors, and OpenTelemetry observability out of the box.
The **Midaz SDK for Go** is the idiomatic v4 client for the Midaz financial-ledger APIs. It gives you typed access to every service — Organizations, Ledgers, Accounts, Transactions, and more — with a single surface for authentication, pagination, errors, logging, and observability.
It handles the boilerplate so your code stays focused on business logic.
**Coming from v2?** v4 is a clean major version with breaking changes across authentication, pagination, errors, and service access. There is no deprecation window — you swap your import from `/v2` to `/v4` and migrate at the same time.
See [Migrating from v2](#migrating-from-v2) before you upgrade.
## Getting started
***
### Step 1 – Install Go
Before using the SDK, you **must** install Go on your machine. v4 declares **Go 1.26** in `go.mod`. The public API also uses `iter.Seq2` and `log/slog`.
Go to the [official Go website](https://golang.org/dl/).
Download the installer for your OS (Windows, macOS, or Linux).
[**Follow the installation instructions.**](https://go.dev/doc/install)
### Step 2 – Create or use an existing Go project
**Create a Go project:**
To create a Go project, use the following command:
```bash Bash theme={null}
mkdir my-midaz-app
cd my-midaz-app
go mod init my-midaz-app
```
**Use an existing Go project:**
If you're working in an existing project, make sure there's a `go.mod` file in the root. If not, run the following command to create one:
```bash Bash theme={null}
go mod init your-module-name
```
### Step 3 – Add the Midaz SDK
Inside your project directory, run the following command to pull the v4 SDK and add it to your `go.mod` and `go.sum` files:
**The module path requires the `/v4` suffix.** If you omit it, Go will resolve a stale pre-v4 release that is missing every change shipped in this version. Always import `github.com/LerianStudio/midaz-sdk-golang/v4`.
```bash Bash theme={null}
go get github.com/LerianStudio/midaz-sdk-golang/v4
```
Using VS Code or GoLand? Your IDE may automatically run `go get` when you import a new package.
### Step 4 – Import the SDK
Create or open a `main.go` file and add the following content. The example below builds a client against your local Midaz stack with anonymous authentication, lists organizations, then creates a new one.
The example below targets a local Midaz stack with auth disabled. If you don't have one running yet, see [Getting started with Midaz](/en/getting-started) to spin one up before running the snippet.
```go Go expandable theme={null}
package main
import (
"context"
"fmt"
"log"
"github.com/LerianStudio/midaz-sdk-golang/v4"
"github.com/LerianStudio/midaz-sdk-golang/v4/models"
)
func main() {
// Build a client. v4 requires exactly one auth source — use
// midaz.WithAnonymous() for a local stack, or midaz.WithAccessManager(...)
// for a development or production environment.
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentLocal),
midaz.WithAnonymous(),
)
if err != nil {
log.Fatalf("midaz.New: %v", err)
}
defer c.Shutdown(context.Background())
ctx := context.Background()
// List the first 5 organizations using a typed list-opts struct.
page, err := c.Organizations.ListOrganizations(ctx, models.OrganizationsListOpts{
PageListOpts: models.PageListOpts{Limit: 5},
})
if err != nil {
log.Fatalf("ListOrganizations: %v", err)
}
for _, org := range page.Items {
fmt.Printf("- %s (%s)\n", org.LegalName, org.ID)
}
// Create a new organization. Notice that midaz.CreateOrganizationInput is
// the same type as models.CreateOrganizationInput — re-exported on the
// midaz package so most user code only needs one import.
dba := "Example Inc."
org, err := c.Organizations.CreateOrganization(ctx, &midaz.CreateOrganizationInput{
LegalName: "Example Corporation",
LegalDocument: "123456789",
DoingBusinessAs: &dba, // optional fields are *string — use &local for short literals
Address: midaz.Address{
Line1: "123 Main St",
City: "New York",
State: "NY",
ZipCode: "10001",
Country: "US",
},
})
if err != nil {
log.Fatalf("CreateOrganization: %v", err)
}
fmt.Printf("Organization created: %s\n", org.ID)
}
```
This gives you access to:
* The Midaz client for calling every API service.
* Built-in data models (like `CreateOrganizationInput`).
* Auth via **Access Manager** (production) or **Anonymous** (local development).
* A typed configuration system that fails fast at construction time.
Want to learn more about authentication? Jump to the [Authentication](#authentication) section for the full setup.
### Step 5 – Run the project
Run the following command:
```bash Bash theme={null}
go run main.go
```
## SDK architecture
***
The **Midaz SDK for Go** is built around clarity and predictability. Every service is reachable directly on the client. Every list method follows the same trio shape. Every error is structured. Every option fails fast at construction time.
#### Layered design
| Layer | What it handles |
| :------------------- | :--------------------------------------------------------------------------------------------- |
| **Client** | The main entry point — `midaz.New(...)` wires authentication, retries, and observability. |
| **Services** | High-level access to each Midaz domain, exposed as promoted fields on the client. |
| **Models** | The core data structures that mirror Midaz's domain logic, re-exported on the `midaz` package. |
| **Utility packages** | Modular helpers for config, errors, observability, retries, idempotency, and more. |
Services are reached directly on the client — `c.Accounts`, `c.Transactions`, `c.Organizations`. You may still see an embedded `Entity` field in autocomplete, but new code should use the direct service fields shown in this guide.
Want to dive deeper? Check the [architecture guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/architecture.md) for the full story behind the rewrite.
### Services
The `Services` layer is your access point to every Midaz domain. Each service handles one resource family and ships every method as part of its single interface — no `UseAllAPIs()` toggle, no service registration step. Every service is ready to use the moment `midaz.New()` returns.
#### Available services
| Service | What it does |
| :-------------------- | :----------------------------------------------------- |
| `c.Organizations` | Manage organizations. |
| `c.Ledgers` | Create and retrieve ledgers. |
| `c.Assets` | Define and manage assets. |
| `c.AssetRates` | Set up and fetch asset exchange rates. |
| `c.Accounts` | Manage accounts and check balances. |
| `c.AccountTypes` | Manage account type definitions. |
| `c.Portfolios` | Group accounts under portfolios. |
| `c.Segments` | Categorize accounts using segments. |
| `c.Transactions` | Create and search financial transactions. |
| `c.TransactionRoutes` | Define and manage transaction routing rules. |
| `c.Operations` | Drill into the atomic operations inside a transaction. |
| `c.OperationRoutes` | Define and manage operation routing rules. |
| `c.Balances` | Get real-time account balances. |
| `c.Holders` | Manage CRM account holders. |
| `c.Aliases` | Manage CRM aliases for accounts and entities. |
| `c.MetadataIndexes` | Manage searchable metadata indexes. |
### Models
Models reflect how Midaz thinks about finance, with each type tied closely to a real-world business concept. You'll use them across every service call — from onboarding accounts to recording multi-leg transactions.
In v4, the most common model types are re-exported on the `midaz` package itself. That means `midaz.Account` and `models.Account` are the same type, and most code only needs one import.
#### Common model types
| Model | What it represents |
| :------------------- | :------------------------------------------------------------------- |
| `midaz.Organization` | A business entity that owns ledgers and accounts. |
| `midaz.Ledger` | A collection of accounts and transactions. |
| `midaz.Asset` | A unit of value (currency, token, etc.) that can be stored or moved. |
| `midaz.Account` | An account for tracking assets and balances. |
| `midaz.Portfolio` | A collection of accounts for grouping and management. |
| `midaz.Segment` | A categorization unit for granular organization. |
| `midaz.Transaction` | A financial event composed of multiple operations. |
| `midaz.Operation` | An individual debit or credit entry within a transaction. |
| `midaz.Balance` | The current state of an account's holdings. |
Need a builder, an internal request shape, or a deprecated type? Import `github.com/LerianStudio/midaz-sdk-golang/v4/models` directly — every type lives there, and the `midaz` aliases preserve type identity, so the two import paths interoperate cleanly.
### Utility packages
Inside the `pkg` folder of the SDK, you'll find utility packages for common dev challenges — config handling, retry policies, and security primitives. They split into two groups. **Core** packages power cross-cutting SDK concerns. **Helper** packages provide domain-specific or low-level utilities.
#### Core packages
| Package | What it solves |
| :-------------- | :---------------------------------------------------------------------------------- |
| `auth` | Access Manager OAuth and token lifecycle. Replaces the v2 `access-manager` package. |
| `config` | Centralized config handling, env overrides, and custom service URLs. |
| `concurrent` | Tools for batching, rate-limiting, and worker pools. |
| `errors` | Structured error types, classifiers, and the canonical `Retryable()` predicate. |
| `observability` | Tracing, metrics, and logs through one OpenTelemetry provider. |
| `retry` | Retry policy options with exponential backoff and jitter. |
| `sdkctx` | Per-request context flags — idempotency keys, soft vs hard delete, include-deleted. |
| `validation` | Input validation with clear, structured error messages. |
#### Helper packages
| Package | What it solves |
| :------------ | :---------------------------------------------------------------- |
| `accounts` | Account-specific helpers and convenience functions. |
| `conversion` | Type conversion helpers between models and external formats. |
| `data` | Data utilities and faker helpers for testing and demos. |
| `format` | Utilities for formatting data the Midaz way (dates, times, etc.). |
| `generator` | Demo and mass-data generation for end-to-end scenarios. |
| `integrity` | Checksum and integrity verification utilities. |
| `performance` | Helpers for tuning bulk operations and high-throughput tasks. |
| `security` | Security utilities, including SSRF protection and TLS validation. |
| `stats` | Processing statistics and metrics aggregation. |
| `transaction` | Transaction-building helpers and fluent builders. |
| `utils` | General-purpose helpers used across the SDK. |
| `version` | SDK version metadata and identification. |
The v2 `pkg/access-manager` package has moved to `pkg/auth` (so the directory matches the package name). The v2 `pkg/pagination` package was removed — its surface lives in `models` and on each service today.
## Authentication
***
In v4, the SDK requires exactly one authentication source at construction time. Calling `midaz.New(...)` with neither returns a typed configuration error — no more silent 401 cascades on the first API call.
You have two choices:
* **`midaz.WithAccessManager(...)`** — production-shape OAuth via the Lerian Access Manager. Recommended for any non-local stack.
* **`midaz.WithAnonymous()`** — opt out of authentication entirely. Suitable only for a local Midaz stack with auth disabled.
The two options are mutually exclusive.
### Production: Access Manager
Plug your Access Manager credentials into `midaz.WithAccessManager`. The SDK eagerly fetches an initial token at construction time, so misconfigurations surface as configuration errors instead of cascading 401s.
Replace the values in the `// Configure Access Manager` block with your own credentials before running.
```go Go expandable theme={null}
package main
import (
"context"
"log"
"os"
"github.com/LerianStudio/midaz-sdk-golang/v4"
)
func main() {
// Configure Access Manager. ClientID and ClientSecret are typically
// loaded from environment variables — never hardcoded.
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentProduction),
midaz.WithAccessManager(midaz.AccessManager{
Address: "https://auth.midaz.io",
ClientID: os.Getenv("MIDAZ_CLIENT_ID"),
ClientSecret: os.Getenv("MIDAZ_CLIENT_SECRET"),
}),
)
if err != nil {
log.Fatalf("midaz.New: %v", err)
}
defer c.Shutdown(context.Background())
// Use c.Organizations, c.Ledgers, c.Accounts, ... as usual.
}
```
The SDK requests a token from your Access Manager, attaches it to every API call, and refreshes it automatically when it expires.
### Local development: Anonymous
For a local Midaz stack with auth disabled, opt out explicitly:
```go Go theme={null}
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentLocal),
midaz.WithAnonymous(),
)
```
### Configure via environment variables
You can also point the SDK at your Access Manager via environment variables. Export them in your shell or your process manager:
```bash Bash theme={null}
export PLUGIN_AUTH_ENABLED=true
export PLUGIN_AUTH_ADDRESS=https://your-auth-service.com
export MIDAZ_CLIENT_ID=your-client-id
export MIDAZ_CLIENT_SECRET=your-client-secret
```
`config.FromEnvironment()` reads the **process environment**, not a `.env` file. If you keep variables in a `.env` file during development, load them with a library like `godotenv` before calling `config.NewConfig(config.FromEnvironment())`.
Then opt in to environment loading at config time:
```go Go theme={null}
import "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/config"
cfg, err := config.NewConfig(config.FromEnvironment())
if err != nil {
log.Fatalf("config: %v", err)
}
c, err := midaz.New(midaz.WithConfig(cfg))
```
Environment loading is **explicit** in v4 — `config.FromEnvironment()` must be in the option chain. The SDK no longer reads env vars implicitly during construction.
Want the full auth walkthrough? Check the [Authentication guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/auth.md) in the SDK repo.
## Multi-tenancy
***
**Tenant scope comes from the Access Manager / JWT claims** used to obtain the token. The SDK applies tenant identity automatically from those claims — no extra configuration on the client side.
To run calls under a different tenant scope, use a separate set of Access Manager credentials — or build a second client with its own token context.
## Listing and iteration
***
Every list endpoint in v4 ships in three flavors. Pick the one that matches your use case — they're consistent across every service.
| Method | Returns | Use when |
| :------------- | :----------------------------------- | :---------------------------------------------------------- |
| `ListXxx` | `*models.ListResponse[T]` (one page) | You want exactly one page and decide when to advance. |
| `ListXxxAll` | `iter.Seq2[T, error]` | You want every item; the SDK handles paging internally. |
| `ListXxxPages` | `iter.Seq2[*ListResponse[T], error]` | You need page-level metadata for checkpointing or batching. |
### Typed list options
Each list method takes a typed opts struct that embeds one of two base structs depending on how the endpoint paginates:
| Pagination shape | Endpoints | Base struct |
| :--------------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ |
| **Page-based** | Organizations, Ledgers, Assets, Portfolios, Segments, Accounts, AccountTypes, Balances, Holders, Aliases | `models.PageListOpts{Limit, Page, SortDirection, StartDate, EndDate}` |
| **Cursor-based** | Transactions, Operations, OperationRoutes, TransactionRoutes, AssetRates | `models.CursorListOpts{Limit, Cursor, SortDirection, StartDate, EndDate}` |
Each endpoint also exposes a typed `Filters` sub-struct with only the fields that endpoint actually honors. Setting a field on the wrong shape — for example, `Page` on a cursor endpoint — fails at compile time, not silently at runtime.
### Iterate every item with `ListAll`
The most idiomatic shape: a `range` loop over every item across every page. The SDK advances cursors and fetches pages internally.
```go Go theme={null}
import (
"github.com/LerianStudio/midaz-sdk-golang/v4"
"github.com/LerianStudio/midaz-sdk-golang/v4/models"
)
opts := models.AccountsListOpts{
PageListOpts: models.PageListOpts{Limit: 100},
Filters: models.AccountsFilters{
Status: "ACTIVE",
AssetCode: "USD",
},
}
for account, err := range c.Accounts.ListAccountsAll(ctx, orgID, ledgerID, opts) {
if err != nil {
return fmt.Errorf("list accounts: %w", err)
}
process(account)
}
```
### Iterate page envelopes with `ListPages`
When you need page-level metadata — for checkpointing, batching, or stopping mid-page — iterate over `ListXxxPages` instead. Every entry is a `*ListResponse[T]` with the full `Pagination` block attached.
```go Go theme={null}
opts := models.TransactionsListOpts{
CursorListOpts: models.CursorListOpts{Limit: 50},
Filters: models.TransactionsFilters{Status: "APPROVED"},
}
for page, err := range c.Transactions.ListTransactionsPages(ctx, orgID, ledgerID, opts) {
if err != nil {
return fmt.Errorf("page iter: %w", err)
}
log.Printf("page=%d items=%d next_cursor=%q",
page.Pagination.Page, len(page.Items), page.Pagination.NextCursor)
for _, tx := range page.Items {
process(tx)
}
if shouldStop(page) {
break // The SDK aborts in-flight paging cleanly.
}
}
```
See the [Pagination guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/pagination.md) in the SDK repo for `HasMore()` semantics, `NextCursor` handling, and the page-vs-cursor decision table.
## Error handling
***
Most errors returned by the SDK service layer are `*pkg/errors.Error` with structured fields:
| Field | What it carries |
| :---------- | :------------------------------------------------------------------------------- |
| `Category` | The error class (validation, authentication, network, configuration, and so on). |
| `Code` | A stable string code suitable for branching or telemetry. |
| `Operation` | The SDK operation that produced the error (e.g. `Accounts.GetAccount`). |
| `Resource` | The resource family the error refers to, when relevant. |
You can branch with typed predicates, walk fields with `errors.As`, or use the canonical `Retryable()` method to drive retry policy.
### Branch with typed predicates
The SDK ships with a full set of `Is*` predicates so you can match errors without poking at internals:
```go Go expandable theme={null}
import (
"errors"
"fmt"
sdkerrors "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/errors"
)
acc, err := c.Accounts.GetAccount(ctx, orgID, ledgerID, accountID)
if err != nil {
switch {
case sdkerrors.IsNotFoundError(err):
return fmt.Errorf("account not found: %w", err)
case sdkerrors.IsAuthError(err):
// Matches both 401 and 403 — re-authenticate or fix permissions.
return fmt.Errorf("auth failure: %w", err)
case sdkerrors.IsValidationError(err):
return fmt.Errorf("invalid input: %w", err)
case sdkerrors.IsConflictError(err):
return fmt.Errorf("already exists: %w", err)
case sdkerrors.IsRateLimitError(err):
return fmt.Errorf("rate limited: %w", err)
case sdkerrors.IsNetworkError(err):
return fmt.Errorf("transient transport: %w", err)
case sdkerrors.IsConfigurationError(err):
return fmt.Errorf("setup mistake: %w", err)
}
// Walk the structured fields when you need them.
var sdkErr *sdkerrors.Error
if errors.As(err, &sdkErr) {
log.Printf("op=%s resource=%s code=%s retryable=%v",
sdkErr.Operation, sdkErr.Resource, sdkErr.Code, sdkErr.Retryable())
}
}
```
### Drive retry decisions with `Retryable()`
The `Error.Retryable()` method is the canonical retry-policy source. Use it instead of building your own classification.
```go Go theme={null}
var sdkErr *sdkerrors.Error
if errors.As(err, &sdkErr) && sdkErr.Retryable() {
// Apply your retry logic — backoff, jitter, max attempts, etc.
}
```
### Wrap raw transport errors
If you're calling lower-level HTTP code outside the SDK and want the same structured error shape, use `ClassifyTransportError`:
```go Go theme={null}
import sdkerrors "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/errors"
resp, err := httpClient.Do(req)
if err != nil {
return sdkerrors.ClassifyTransportError("PaymentService.Charge", err)
}
```
### Local validation: FieldErrors
Local input validation surfaces `*pkg/validation.FieldErrors` — a structured collection of per-field complaints from the SDK before any HTTP call. Use `errors.As` to inspect them when validating user input or builders.
```go Go theme={null}
import (
"errors"
"fmt"
"github.com/LerianStudio/midaz-sdk-golang/v4/pkg/validation"
)
var fieldErrs *validation.FieldErrors
if errors.As(err, &fieldErrs) {
for _, fe := range fieldErrs.Errs() {
fmt.Printf("- %s: %s\n", fe.Field, fe.Message)
}
}
```
Want to dive deeper? Check the [Error handling guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/errors.md) in the SDK repo for the full category map, every code, and retry boundary semantics.
## Logging
***
In v4, **`*slog.Logger` is the canonical logger surface**. Wire it via `midaz.WithLogger(...)`. The SDK is **silent by default** — it uses `slog.DiscardHandler` until you opt in.
That means no surprise log lines in your stdout, and no fighting with the SDK over log format. You decide the handler, the level, and the destination.
```go Go expandable theme={null}
package main
import (
"context"
"log"
"log/slog"
"os"
"github.com/LerianStudio/midaz-sdk-golang/v4"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentLocal),
midaz.WithAnonymous(),
midaz.WithLogger(logger),
)
if err != nil {
log.Fatalf("midaz.New: %v", err)
}
defer c.Shutdown(context.Background())
// SDK retry diagnostics, slow-call warnings, and other internal
// log lines now flow through your handler.
}
```
Need zap, zerolog, or charmbracelet/log? They all integrate as `slog.Handler` adapters — the SDK doesn't care which backend produces the records, only that it speaks slog.
Wiring zap, zerolog, or another backend? The [Logging guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/logging.md) has adapter recipes for every common logging library.
## Observability
***
OpenTelemetry is first-class in v4. One observability provider gives you spans, metrics, and OTel-correlated logs through a single wiring point.
Configure observability either by passing a fully-built provider, or by passing options that the SDK assembles for you.
### Wire a provider
```go Go expandable theme={null}
package main
import (
"context"
"log"
"github.com/LerianStudio/midaz-sdk-golang/v4"
"github.com/LerianStudio/midaz-sdk-golang/v4/pkg/observability"
)
func main() {
ctx := context.Background()
provider, err := observability.New(ctx,
observability.WithServiceName("payments-api"),
observability.WithEnvironment("production"),
observability.WithComponentEnabled(true, true, true), // tracing, metrics, logs
)
if err != nil {
log.Fatalf("observability.New: %v", err)
}
defer provider.Shutdown(ctx)
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentProduction),
midaz.WithAccessManager(midaz.AccessManager{ /* ... */ }),
midaz.WithObservabilityProvider(provider),
)
if err != nil {
log.Fatalf("midaz.New: %v", err)
}
defer c.Shutdown(ctx)
}
```
### Or pass options inline
```go Go theme={null}
import "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/observability"
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentProduction),
midaz.WithAccessManager(am),
midaz.WithObservabilityOptions(
observability.WithServiceName("payments-api"),
observability.WithComponentEnabled(true, true, true),
),
)
```
The SDK emits one HTTP span per outbound request with proper W3C `traceparent` propagation. Business log records carry safe IDs only — never payloads, names, addresses, or auth headers.
You can also wrap a block of business logic in a span via `Client.Trace`:
```go Go theme={null}
err = c.Trace("create-organization", func(ctx context.Context) error {
_, err := c.Organizations.CreateOrganization(ctx, input)
return err
})
```
`WithObservabilityOptions` and `WithObservabilityProvider` use **replacement semantics**, not merge. Each call replaces any previously installed provider. To start from a baseline, include `observability.WithDevelopmentDefaults` or `observability.WithProductionDefaults` as the first option in the chain.
Want to dive deeper? See the [`10-observability-otel` example](https://github.com/LerianStudio/midaz-sdk-golang/tree/main/examples/10-observability-otel) in the SDK repo for span attributes, metric names, and exporter setup.
## Idempotency
***
Auto-idempotency is **on by default** in v4. The SDK emits an `X-Idempotency: ` header on every unsafe HTTP request (POST, PUT, PATCH, DELETE). Retries on transient failures then do not double-create resources.
You can override the auto-generated key per-call when you need a stable, caller-supplied key — typical for saga steps, outbox rows, or UI-driven submissions.
### Set a stable key per request
```go Go theme={null}
import "github.com/LerianStudio/midaz-sdk-golang/v4/pkg/sdkctx"
ctx := sdkctx.WithIdempotencyKey(context.Background(), "tx-2026-05-06-001")
tx, err := c.Transactions.CreateTransaction(ctx, orgID, ledgerID, input)
```
### Suppress idempotency for one call
For rare fire-and-forget administrative endpoints, suppress the header per-request:
```go Go theme={null}
ctx := sdkctx.WithoutAutoIdempotency(context.Background())
err := c.SomeAdminService.DoOneShotThing(ctx, input)
```
### Disable globally
If you don't want auto-idempotency anywhere, turn it off at the client level:
```go Go theme={null}
c, err := midaz.New(
midaz.WithEnvironment(midaz.EnvironmentLocal),
midaz.WithAnonymous(),
midaz.WithIdempotency(false),
)
```
You can also disable it via the `MIDAZ_IDEMPOTENCY=false` environment variable when using `config.FromEnvironment()`.
## Environment variables
***
You can configure the SDK with environment variables instead of hardcoded values. Environment loading is **explicit** in v4 — pass `config.FromEnvironment()` in your config option chain to opt in.
| Variable | Description |
| :-------------------- | :-------------------------------------------------------------------------------- |
| `MIDAZ_ENVIRONMENT` | Target environment (`local`, `development`, `production`). |
| `MIDAZ_BASE_URL` | Base URL for all Midaz services, used when the service-specific URLs are not set. |
| `MIDAZ_LEDGER_URL` | Ledger API URL. It serves the onboarding and transaction endpoints. |
| `MIDAZ_CRM_URL` | CRM API URL. Holders and Aliases use it. |
| `PLUGIN_AUTH_ENABLED` | Enable Access Manager authentication (`true` or `false`). |
| `PLUGIN_AUTH_ADDRESS` | Address of the Access Manager service. |
| `MIDAZ_CLIENT_ID` | Client ID for Access Manager authentication. |
| `MIDAZ_CLIENT_SECRET` | Client secret for Access Manager authentication. |
| `MIDAZ_TIMEOUT` | HTTP request timeout in seconds. |
| `MIDAZ_DEBUG` | Enable debug logs (`true` or `false`). |
| `MIDAZ_MAX_RETRIES` | Maximum retry attempts for failed requests. Set `0` to disable retries. |
| `MIDAZ_IDEMPOTENCY` | Enable automatic idempotency keys for unsafe requests (`true` or `false`). |
For the full option matrix across `midaz`, `pkg/config`, and `pkg/sdkctx`, see the [Configuration guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/configuration.md) in the SDK repo.
## Example projects
***
The SDK ships with a numbered tour of its core capabilities (examples 01–10) plus a set of advanced and reference examples. The numbered set is **focused tutorials**: each one teaches exactly one concept with the smallest possible body. Browse them in the [examples directory](https://github.com/LerianStudio/midaz-sdk-golang/tree/main/examples) on GitHub.
| Example | What it demonstrates |
| :---------------------- | :------------------------------------------------------------- |
| `01-hello-world` | Minimal init plus the first API call (\~17 body lines). |
| `02-auth` | Access Manager authentication (production setup). |
| `03-end-to-end` | Org → ledger → asset → account → transaction. |
| `04-listing-cursor` | Cursor-based pagination with `iter.Seq2`. |
| `05-listing-pages` | Page-based pagination — `List` / `ListAll` / `ListPages`. |
| `06-idempotency` | Auto / explicit / suppressed idempotency modes. |
| `07-retries` | Default policy, custom policy, disabled retries. |
| `08-logging-slog` | `*slog.Logger` integration (v4 logging). |
| `09-testing-with-mocks` | `go.uber.org/mock` for unit testing your code against the SDK. |
| `10-observability-otel` | Full OpenTelemetry surface (tracing + metrics + logs). |
The repository also includes specialized and reference examples — `concurrency`, `configuration`, `context`, `tracing`, `tracing-server`, `pkg-validation-demo`, `mass-demo-generator`, and `workflow-with-entities`. They cover advanced patterns (bounded parallelism, OTel context propagation across processes, mass data generation) for when you've outgrown the focused set.
## Migrating from v2
***
v4 is a **clean-cut major version with no deprecation window**. There is no transitional release, no `// Deprecated:` shim, no backward-compatible alias of the old surface. Swap your import from `/v2` to `/v4` and walk the breaking changes below.
The biggest moves to plan for:
* **Module path and package name**: `github.com/LerianStudio/midaz-sdk-golang/v4` (was `/v2`), package `midaz` (was `client`).
* **Authentication**: `WithAuthToken` is gone. Use `WithAccessManager` for production or `WithAnonymous` for local stacks. One auth source is required at construction time.
* **Service access**: `c.Accounts.X` (was `c.Entity.Accounts.X`). The `c.Entity` field still exists for compatibility, but every example uses the short form.
* **Pagination**: `models.ListOptions` and its 30 fluent setters are gone. Use the typed per-endpoint opts (`models.AccountsListOpts`, `models.TransactionsListOpts`, ...) and the new `ListAll` / `ListPages` iterators.
* **Errors**: `*MidazError` is gone. Service-layer errors now use `*pkg/errors.Error` with `Retryable()` as the official retry source. Local validation may surface `*pkg/validation.FieldErrors`.
* **Tenant identity**: `WithTenantID`, the `MIDAZ_TENANT_ID` env var, and the `X-Tenant-ID` header are gone. Tenant scope flows through Access Manager / JWT claims.
* **Logging**: `*slog.Logger` replaces the bespoke `observability.Logger` interface as the canonical surface. The SDK is silent by default.
For the authentication changes in detail, see the [Authentication guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/auth.md) in the SDK repo. The [examples directory](https://github.com/LerianStudio/midaz-sdk-golang/tree/main/examples) shows the v4 API in practice.
## Explore the APIs
***
For more information about the APIs, refer to the following links:
# Midaz SDK for TypeScript
Source: https://docs.lerian.studio/en/midaz/midaz-sdk-typescript
Build typed financial integrations with the Midaz SDK for TypeScript — builder pattern, automatic retries, observability, and strict validation.
The Midaz SDK for TypeScript helps you build financial integrations. It gives you a typed, developer-friendly interface over the Midaz financial services platform. You focus on your business logic, not on the transport code.
The SDK works with Organizations, Ledgers, Accounts, Transactions, and more. Use it for a simple workflow or for complex operations.
A layered, modular architecture supports performance, extensibility, and the developer experience.
### Why use the Midaz SDK for TypeScript?
* **Type-safe by design**: Full TypeScript support with precise type definitions.
* **Builder pattern**: Fluent, readable interfaces to construct complex objects.
* **Robust error handling**: Recovery strategies and clear error signals.
* **Observability included**: Tracing, metrics, and logs, ready to use.
* **Layered architecture**: Clean separation between client, entities, API, and models.
* **Automatic retries**: Configurable retry policies for transient failures.
* **Concurrency controls**: Built-in tools to run tasks in parallel with controlled throughput.
* **Fast with caching**: In-memory caching for better performance.
* **Strict validation**: Catch invalid input early with clear error messages.
## Getting started
***
### Prerequisite
* The Midaz SDK for TypeScript **requires** TypeScript **v5.8 or later**.
### Installing the SDK
Install the **Midaz SDK for TypeScript** with one of the following commands:
```bash npm theme={null}
npm install @lerianstudio/midaz-sdk
```
```bash yarn theme={null}
yarn add @lerianstudio/midaz-sdk
```
After you install it, follow the [*Quick Start Guide*](#quick-start-guide) to learn how to use the SDK.
## Authentication
***
The **Midaz SDK for TypeScript** authenticates through the Lerian **Access Manager** (OAuth). For a local stack with authentication disabled, you can build a client without it.
You never call a `createClient` factory — build a configuration with `createClientConfigWithAccessManager()` (or `createClientConfigBuilder()` for a no-auth local stack) and pass it to `new MidazClient(config)`.
#### Access Manager authentication
To integrate with external identity providers over OAuth:
```typescript TypeScript theme={null}
import { createClientConfigWithAccessManager, MidazClient } from '@lerianstudio/midaz-sdk';
const client = new MidazClient(
createClientConfigWithAccessManager({
address: 'https://auth.example.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
}).withEnvironment('sandbox')
);
```
The Access Manager handles tokens for you: acquisition, caching, and renewal. You do not manage tokens manually.
#### Local development (no authentication)
For a local Midaz stack with authentication disabled, build a client without the Access Manager:
```typescript TypeScript theme={null}
import { createClientConfigBuilder, MidazClient } from '@lerianstudio/midaz-sdk';
const client = new MidazClient(
createClientConfigBuilder().withEnvironment('development')
);
```
We offer an [Access Manager plugin](/en/platform/access-manager/access-manager) that you can use. If you'd like to know more about it, [contact us](https://lerian.studio/contact).
## Quick start guide
***
The following sections give practical code examples for the **Midaz SDK for TypeScript**.
### Create a client
This is the first step. The client is your main entry point to the SDK. It handles authentication and gives you access to all entity services.
**Example:**
```typescript TypeScript theme={null}
import { createClientConfigBuilder, MidazClient } from '@lerianstudio/midaz-sdk';
const client = new MidazClient(
createClientConfigBuilder().withEnvironment('sandbox') // Options: 'development', 'sandbox', 'production'
);
```
### Create an Asset
Create assets with the builder pattern and `createAssetBuilder`.
**Example:**
```typescript TypeScript theme={null}
import { createAssetBuilder } from '@lerianstudio/midaz-sdk';
const assetInput = createAssetBuilder('US Dollar', 'USD')
.withType('currency')
.withMetadata({ precision: 2, symbol: '$' })
.build();
const asset = await client.entities.assets.createAsset('org_123', 'ledger_456', assetInput);
```
In this code, you add the required `name` and `assetCode` fields to the builder `const assetInput = createAssetBuilder('US Dollar', 'USD')`. Then you add any other properties with `with*` methods.
### Create an Account
Create accounts with the builder pattern and `createAccountBuilder`.
**Example:**
```typescript TypeScript theme={null}
import { createAccountBuilder } from '@lerianstudio/midaz-sdk';
const accountInput = createAccountBuilder('Savings Account', 'USD')
.withType('savings')
.withAlias('personal-savings')
.build();
const account = await client.entities.accounts.createAccount('org_123', 'ledger_456', accountInput);
```
In this code, you add the required `name` and `assetCode` fields to the builder `const accountInput = createAccountBuilder('Savings Account', 'USD')`. Then you add any other properties with `with*` methods.
### Create a Transaction
Create transactions with the builder pattern and `createTransactionBuilder`.
**Example:**
```typescript TypeScript expandable theme={null}
import { createTransactionBuilder } from '@lerianstudio/midaz-sdk';
const transactionInput = createTransactionBuilder()
.withCode('payment_001')
.withOperations([
{
accountId: 'source_account_id',
assetCode: 'USD',
amount: 100 * 100, // $100.00
type: 'debit',
},
{
accountId: 'destination_account_id',
assetCode: 'USD',
amount: 100 * 100, // $100.00
type: 'credit',
},
])
.withMetadata({ purpose: 'Monthly payment' })
.build();
```
In this code, you add all properties with `with*` methods.
### Error recovery
Use enhanced error recovery for critical operations.
```typescript TypeScript theme={null}
import { withEnhancedRecovery } from '@lerianstudio/midaz-sdk/util/error';
const result = await withEnhancedRecovery(
() => client.entities.transactions.createTransaction('org_123', 'ledger_456', transactionInput),
{
maxRetries: 3,
enableSmartRecovery: true,
}
);
```
### Clean up resources
```typescript TypeScript theme={null}
client.close();
```
### Using Access Manager for authentication
```typescript TypeScript expandable theme={null}
import { createClientConfigWithAccessManager, MidazClient } from '@lerianstudio/midaz-sdk';
// Initialize the client with Access Manager authentication
const client = new MidazClient(
createClientConfigWithAccessManager({
address: 'https://auth.example.com', // Identity provider address
clientId: 'your-client-id', // OAuth client ID
clientSecret: 'your-client-secret', // OAuth client secret
tokenEndpoint: '/oauth/token', // Optional, defaults to '/oauth/token'
refreshThresholdSeconds: 300, // Optional, defaults to 300 (5 minutes)
})
.withEnvironment('sandbox')
.withApiVersion('v1')
);
// The SDK will automatically handle token acquisition and renewal
// You can now use the client as normal
const organizations = await client.entities.organizations.listOrganizations();
// For environment-specific configurations with Access Manager
const sandboxClient = new MidazClient(
createSandboxConfigWithAccessManager({
address: 'https://auth.example.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
})
);
// Clean up resources when done
client.close();
```
## SDK architecture
***
The Midaz SDK uses a multi-layered service architecture for a clean, modular, and scalable developer experience. It has three layers, shown in *Figure 1*. Each layer serves a distinct purpose.
* **Client interface**: This is the main entry point for SDK users. It manages configuration such as API keys and environments. It initializes services lazily and exposes all SDK functionality.
* **Entity services layer**: This layer holds domain-specific services, such as Accounts, Assets, and Transactions. Each service offers consistent methods: create, get, update, delete, and list. Each service also adds specialized operations for its entity.
* **Core services layer**: All entity services use these foundational utilities. They handle HTTP requests, input validation, error processing, observability, configuration, and caching.
The SDK architecture emphasizes:
* **Consistency** through shared patterns across services.
* **Scalability** via dependency injection and service factories.
* **Reliability** through enhanced error handling and typed responses.
* **Testability** with support for mocking, integration, and contract testing.
Want to dive Deeper? Check the following pages for more information about the Architecture:
* [Midaz SDK architecture overview](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/overview.md).
* [Client interface architecture](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/client-interface.md).
* [Service layer architecture](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/service-layer.md).
## Builder pattern
***
The **Midaz SDK for TypeScript** uses a builder pattern to help you assemble complex objects in a safe, adaptable way. Instead of a fixed set of inputs, it gives you a step-by-step, fluent, chainable interface.
**Builder functions in the SDK:**
* Tell you the parameters in advance.
* Let you set optional fields with `.with*()` methods and chain them.
* Prevent invalid states through a guided structure.
* Hide internal complexity for better readability.
### Example
Here’s a quick example:
```typescript TypeScript theme={null}
const assetInput = createAssetBuilder('USD Currency', 'USD')
.withType('currency')
.withMetadata({ precision: 2 })
.build();
```
You can then pass this `assetInput` to the corresponding create method in the SDK.
Want to dive deeper? Check the [Builder Pattern in Midaz SDK](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/core-concepts/builder-pattern.md) page for more information.
## Working with entities
***
Each entity service covers a distinct part of the financial domain, such as accounts, assets, or transactions.
These services create, retrieve, update, and delete data for each type of entity.
They also offer specialized features for each use case, so you handle financial data with confidence.
| Entity | Description |
| :----------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
| [**Organizations**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/organizations.md) | Manage business units and organizational data. |
| [**Ledgers**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/ledgers.md) | Structure and manage financial records. |
| [**Assets**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/assets.md) | Work with assets such as currencies, commodities, and other value units. |
| [**Accounts**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/accounts.md) | Create, retrieve, update, and delete accounts within a Ledger. |
| [**Segments**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/segments.md) | Organize portfolios for analytics and reporting. |
| [**Portfolios**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/portfolios.md) | Group accounts and assets into meaningful financial collections. |
| [**Balances**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/balances.md) | Retrieve and calculate asset balances for accounts. |
| [**Asset Rates**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/asset-rates.md) | Handle exchange rates between different asset types. |
| [**Transactions**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/transactions.md) | Create and manage transactions that move assets between accounts. |
| [**Operations**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/operations.md) | Manage atomic debits and credits that make up a transaction. |
You access each service through the SDK client. They follow a consistent structure, so you build and maintain financial features more easily.
Want to dive deeper? Check the [Entities pages](https://github.com/LerianStudio/midaz-sdk-typescript/tree/main/docs/entities) for more information.
## Using utilities
***
The SDK provides utility modules for common operations: performance, error handling, configuration, and observability.
These tools work with the rest of the SDK and help you build financial applications with less effort.
| Utility | Description |
| :---------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- |
| [**Account Helpers**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/account-helpers.md) | Simplifies common account-related logic and transformations. |
| [**Cache**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/cache.md) | Enables lightweight caching for better runtime performance. |
| [**Concurrency**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/concurrency.md) | Helps coordinate and limit concurrent tasks safely and efficiently. |
| [**Config**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/config.md) | Centralized configuration setup and access. |
| [**Data**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/data.md) | Assists with data formatting and pagination tasks. |
| [**Error Handling**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/error-handling.md) | Offers recovery strategies and error processing mechanisms. |
| [**HTTP Client**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/http-client.md) | Provides a low-level HTTP interface for direct API calls. |
| [**Network**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/network.md) | Adds high-level networking features like retries and backoff. |
| [**Observability**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/observability.md) | Captures traces, metrics, and logs to support monitoring and debugging. |
| [**Pagination**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/pagination.md) | Handles paginated responses with predictable, consistent helpers. |
| [**Validation**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/validation.md) | Validates input and output data to help maintain data integrity. |
Want to dive deeper? Check the [Utilities pages](https://github.com/LerianStudio/midaz-sdk-typescript/tree/main/docs/utilities) for more information.
## Error handling
***
The **Midaz SDK for TypeScript** helps you handle errors clearly and consistently. When an error occurs during an SDK operation, the SDK throws a structured error. The error includes key fields:
* `code`: A short, consistent identifier for the error type.
* `message`: A human-readable description.
* `statusCode`: The HTTP status code, when available.
Handle an error like this:
```typescript TypeScript theme={null}
try {
await client.transactions.create(transaction);
} catch (err) {
console.error(`Error (${err.code}): ${err.message}`);
// Optionally: inspect err.statusCode
}
```
### Common error codes
| Code | Description | HTTP Status |
| :-------------------- | :----------------------------------------------------------- | :---------- |
| `invalid_input` | Your request is missing required data or has invalid values. | `400` |
| `unauthorized` | Authentication failed or credentials are missing. | `401` |
| `forbidden` | You're not allowed to perform this action. | `403` |
| `not_found` | The resource you're trying to access doesn't exist. | `404` |
| `conflict` | The operation conflicts with an existing resource. | `409` |
| `internal_error` | Something went wrong on our side. | `500` |
| `service_unavailable` | Temporary outage—try again later. | `503` |
### Best practices
* **Validate input** before you call SDK methods, to avoid `invalid_input`.
* **Check your auth** when you get `unauthorized` or `forbidden`.
* **Retry** on transient issues like `internal_error` or `service_unavailable`.
* **Use `statusCode` and `message`** to show debug info in development logs.
The SDK keeps errors predictable and actionable.
**Tip**
Want to dive deeper? Check the following pages for more information:
* [Error handling in Midaz SDK](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/core-concepts/error-handling.md).
* [Error handling architecture](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/architecture/error-handling.md).
* [Error handling (Utilities)](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/utilities/error-handling.md).
## CI/CD pipeline
***
We use GitHub Actions for automated, production-ready builds:
* Runs tests across multiple Node.js versions.
* Enforces code quality with ESLint and Prettier.
* Keeps dependencies up to date with Dependabot.
* Handles releases automatically with semantic versioning.
* Generates changelogs.
## Want to contribute?
***
To contribute to the Midaz SDK for TypeScript, start with our [contributing guide on GitHub](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/CONTRIBUTING.md). It covers what you need to get started.
## License
***
This project is licensed under the Apache License 2.0. For details, see the [License](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/LICENSE) page.
# Operations
Source: https://docs.lerian.studio/en/midaz/operations
Understand Operations as the smallest units of ledger activity in Midaz — single debit or credit entries on an Account, grouped into balanced Transactions.
An operation is the smallest unit of a transaction. Every transaction contains two or more operations (*Figure 1*). Each operation records a change in the balance of one account:
* A **Debit operation** records value that moves **out of** an account — the source.
* A **Credit operation** records value that moves **into** an account — the destination.
The effect on the stored balance depends on the balance `direction`. A normal account (the common case) shrinks on a debit and grows on a credit.
A transaction is a set of operations that must net to zero from the ledger's view. For example, a P2P payment with a fee includes:
* a **\$100 debit** from the sender's account.
* a **\$98 credit** to the recipient's account.
* a **\$2 credit** to the platform's fee account.
Midaz makes sure a transaction balances before it commits.
Operations also carry metadata such as timestamps and references to the transaction they belong to. You create a transaction through the Midaz JSON API. For each transaction, you specify a set of operations. Each operation has an amount, an asset, and a source or destination account. Midaz then checks that the operations balance before it commits them.
## Key characteristics
***
* Each operation belongs to a transaction and references a specific account and asset.
* Operations include metadata like amount, direction (debit/credit), type, timestamps, references, and more.
* Midaz creates operations when you start a transaction and fills them with the data you provide.
* Midaz validates all operations against the configured rules before it commits them to the ledger.
Every operation has a **`direction`** (`debit` or `credit`) and a **`type`**. The `direction` is the double-entry backbone described above. The `type` classifies the movement. Beyond the standard `CREDIT` and `DEBIT`, the `type` marks special movements:
* `ON_HOLD` and `RELEASE` — two-phase fund reservations.
* `OVERDRAFT` — system-generated companion operations on the internal overdraft balance.
* `BLOCK` and `UNBLOCK` — the label applied to every operation of a transaction created through the dedicated [block and unblock endpoints](/en/midaz/transactions#blocking-and-unblocking-funds). The label overrides only the operation `type`, never its direction or amount.
The `direction` always keeps its debit or credit meaning, regardless of `type`.
## Operation Routes
***
With Accounting Routes, each operation connects to structured validation logic. That logic lives in [Operation Routes](/en/midaz/transaction-routing-entities).
### What is an Operation Route?
An **Operation Route** is a validation rule. It defines which account types or specific accounts a transaction component can use. It also sets the accounting direction (source or destination) and a descriptive annotation.
It works as a validation layer between transaction input and ledger structure. Each operation you submit must meet the account requirements and business rules.
In short: transactions contain multiple operations. Operation Routes validate each operation against the configured validation rules.
### Operation > Operation Route relationship
When you submit a transaction to Midaz, it contains multiple **Operations**. With **Account Types and Accounting Routes enabled**, Midaz validates each operation against the rules in the Operation Routes. This makes sure that:
* The specified account matches the expected account type or specific account requirement.
* The operation direction (debit/credit) aligns with the route definition.
This adds a layer of validation, consistency, and integrity to transaction processing. It matters most for structured financial requirements.
If the operations you submit do not match the configured Operation Route requirements, Midaz fails the validation and rejects the transaction.
## Managing Operations
***
Midaz generates operations automatically as part of a transaction flow. You cannot create an operation directly. You can query an operation as needed.
* [List Operations by Account](/en/reference/midaz/list-operations-by-account) — View all Operations linked to a specific Account.
* [Retrieve an Operation by Account](/en/reference/midaz/retrieve-an-operation-by-account) — Get details of a specific Operation for that Account.
# Organizations
Source: https://docs.lerian.studio/en/midaz/organizations
Define the top-level Organization that owns your Midaz Ledgers — represent banks, fintechs, and parent-child hierarchies for subsidiaries.
An **Organization** is the top-level entity in Midaz. It represents a financial institution, such as a bank or fintech. An Organization owns and governs the financial data of that institution. It is the primary container for all financial operations.
A bank or financial group registers as an Organization in Midaz. The Organization gives its operations a structured environment.
**SaaS and multi-tenant environments** — Midaz scopes every Organization to your tenant automatically. When you list Organizations, you see only the ones in your account. The platform enforces [tenant isolation](/en/multi-tenancy) for you. You do not filter by tenant or add a tenant identifier to your requests.
### Sub-Organizations
Organizations support **parent-child hierarchies**. A bank uses them to represent subsidiaries, regional branches, or separate business units in one framework.
This structure fits financial groups that run several legal entities. Each entity owns its own [Ledgers](/en/midaz/ledgers) and stays operationally independent. The parent-child link records how the entities relate.
### Organization structure
* **Organization > Ledgers:** Every Organization owns one or more Ledgers. Each Ledger holds its financial records, such as accounts and transactions, and belongs to one Organization.
### Key characteristics
* Organizations are the highest level in the hierarchy.
* Each Organization governs its own Ledgers and sets their operational boundaries.
## Flexible hierarchies
***
Midaz supports organizational hierarchies for complex structures.
If your company has several locations or divisions, you can create **child organizations** for specific branches, offices, or subdivisions. Each child organization connects to a **parent organization**.
Each Organization keeps its own data. Records in one Organization do not cross into another.
If a single group manages several Organizations, use this hierarchy to set clear boundaries between them.
### Example
**Company XPTO** is a Brazilian company with branches in São Paulo and New York. Its structure is as follows:
* **Parent Organization**: XPTO Ltda
* **Child Organizations**:
* XPTO New York
* XPTO São Paulo
With this structure, the company can:
* Set a distinct role for each branch.
* Track each branch separately.
* Adapt workflows to each branch.
## Benefits of organizational structuring
***
* **Scalability**: Add or change branches as the business grows.
* **Efficiency**: Manage all levels from one hierarchy.
* **Personalization**: Configure each branch on its own.
Midaz adapts to your company's structure, whether it is simple or complex.
Need help with your Midaz setup? Our consulting service can help you design the structure for your organization, from scratch or from an existing setup.
[Contact us](https://lerian.studio/contact) to learn more.
## Managing Organizations
***
You manage your Organization through the API or Lerian Console.
### Via API
* [Create an Organization](/en/reference/midaz/create-an-organization) — Register a new Organization.
* [List Organizations](/en/reference/midaz/list-organizations) — View all Organizations in your workspace.
* [Retrieve an Organization](/en/reference/midaz/retrieve-an-organization) — Get details of a specific Organization.
* [Update an Organization](/en/reference/midaz/update-an-organization) — Edit an existing Organization.
* [Delete an Organization](/en/reference/midaz/delete-an-organization) — Mark an Organization as deleted.
When you delete an Organization, Midaz does not remove it from the system.
Midaz sets a `deletedAt` timestamp instead. The Organization is then excluded from every read and can no longer be used. Its `status.code` is left unchanged, which preserves historical data for audit trails.
### Via Lerian Console
You view, create, edit, and delete Organizations from the **Organizations** page in the Midaz Module of Lerian Console.
[**Learn more in the Managing Organizations guide.**](/en/midaz/console/managing-organizations)
When you create your **first Organization** in the Console, you do it as part of the onboarding flow. To learn more, see the [Midaz Module onboarding](/en/midaz/console/midaz-console-onboarding) guide.
# Portfolios
Source: https://docs.lerian.studio/en/midaz/portfolios
Group Midaz Accounts under a single customer or business unit as a Portfolio to view the total client relationship and apply policies at the wallet level.
A **Portfolio** is a logical group of Accounts that belong to a single entity, such as a customer or business unit. It groups the related Accounts under one structure and makes them simpler to manage.
Think of a Portfolio as a **wallet** that holds multiple asset-specific Accounts for a customer.
For example, a retail banking customer might have a Portfolio that includes:
* A BRL checking account.
* A USD savings account.
* A credit card account.
With a Portfolio, an institution can view a customer’s total relationship across all accounts. The institution can also apply policies at the Portfolio level.
## Portfolio structure
***
* **Portfolio > Accounts**: A Portfolio groups one or more Accounts. These Accounts usually link to one customer or entity.
* **Portfolio > Ledger > Organization**: A Portfolio belongs to an Organization through its Ledger.
## Key characteristics
***
* Portfolios commonly represent **customers** in a banking context.
* They simplify financial aggregation and give customer-level insights.
* You can also create internal Portfolios for operational use cases.
## Managing Portfolios
***
You can manage your Portfolios either through the API or through Lerian Console.
### Via API
* [List Portfolios](/en/reference/midaz/list-portfolios) — View all Portfolios in your workspace.
* [Retrieve a Portfolio](/en/reference/midaz/retrieve-a-portfolio) — Get details of a specific Portfolio.
* [Create a Portfolio](/en/reference/midaz/create-a-portfolio) — Register a new Portfolio.
* [Update a Portfolio](/en/reference/midaz/update-a-portfolio) — Edit an existing Portfolio.
* [Delete a Portfolio](/en/reference/midaz/delete-a-portfolio) — Remove a Portfolio.
### Via Lerian Console
You can view, create, edit, and delete Portfolios from the Portfolios page in the Midaz Module of Lerian Console.
[**Learn more in the Managing Portfolios guide.**](/en/midaz/console/managing-portfolios)
# Receiving events from Midaz
Source: https://docs.lerian.studio/en/midaz/receiving-midaz-events
Compare RabbitMQ direct bindings and the managed Streaming Hub for receiving Midaz ledger events, and choose the right transport for your integration setup.
Midaz emits a stream of domain events. It emits one each time something important happens in the ledger. The ledger posts a transaction, a balance draws overdraft, or you create an account. Your system can react to those changes for reconciliation, notifications, downstream projections, or analytics. To react, consume the event stream instead of polling the REST API.
This guide covers the two supported ways to receive Midaz events and how to choose between them:
1. **RabbitMQ direct** — bind your own queue to Midaz's AMQP exchanges.
2. **Streaming Hub** — subscribe through Lerian's managed fan-out service.
## The two approaches at a glance
***
Midaz publishes domain events over two transports:
* A **RabbitMQ (AMQP)** publish to topic exchanges that the Midaz deployment owns.
* An **internal event bus** publish that the Streaming Hub consumes and fans out to per-tenant subscribers.
Coverage differs by event. Transaction-lifecycle and overdraft events publish to **both** transports during the migration window. Audit append-log entries go to RabbitMQ only. The remaining catalog events — entity, route, holder, instrument, fee, limit, and rule events — go to the internal event bus only.
That gives you two integration surfaces for the *same* underlying events:
| Event | RabbitMQ direct | Streaming Hub |
| -------------- | ------------------------------------ | ---------------------------------------------------- |
| Transport | AMQP 0-9-1 | Webhook / Pull (HTTP) / SQS / RabbitMQ / EventBridge |
| You connect to | Midaz's broker | A control-plane REST API |
| Coupling | Tight (Midaz infra) | Loose (managed edge) |
| Best for | Self-hosted / co-located deployments | External / SaaS integrators |
If you do not operate your own Midaz broker, use the Streaming Hub. Direct RabbitMQ is for integrators who run inside or adjacent to the Midaz deployment and own the broker.
## Option A — RabbitMQ direct
***
### How it works
The Midaz ledger publishes events through an internal producer to a set of **topic exchanges**. A consumer binds its own queue to the relevant exchange with a routing-key pattern. The consumer then reads messages over AMQP.
Publisher facts (from the ledger service):
* **Content type:** `application/json`
* **Delivery mode:** persistent
* **Headers:** the producer injects OpenTelemetry trace-context into every message
* **Tenancy:** the producer is multi-tenant and publishes messages to a **tenant-specific vhost**
Event exchanges and their routing keys:
| Exchange | Routing key pattern | Emits |
| ----------------------------------------- | ------------------------------------------------------------------- | ------------------------------- |
| `transaction.transaction_events.exchange` | `midaz.transaction.` (e.g. `midaz.transaction.APPROVED`) | Transaction lifecycle envelopes |
| `transaction.overdraft_events.exchange` | `midaz.balance.overdraft.` (`drawn` / `repaid` / `cleared`) | Overdraft events |
| `audit.append_log.exchange` | `audit.append_log.key` | Audit append-log entries |
**Midaz enables these exchanges by default.** Midaz treats each flag as enabled unless you explicitly set it to `false`. The bundled example environment configuration ships all three flags set to `false`. A stack that starts from that example emits nothing until you override the flags.
### For self-hosted operators
A dedicated environment flag controls each exchange. Midaz treats a flag as enabled unless you explicitly set it to `false`. The bundled example configuration ships all three flags set to `false`:
| Exchange | Env flag |
| ----------------------------------------- | ------------------------------------- |
| `transaction.transaction_events.exchange` | `RABBITMQ_TRANSACTION_EVENTS_ENABLED` |
| `transaction.overdraft_events.exchange` | `RABBITMQ_OVERDRAFT_EVENTS_ENABLED` |
| `audit.append_log.exchange` | `AUDIT_LOG_ENABLED` |
On a stack that starts from the example configuration, set the flag to `true` in your Midaz deployment configuration. You can also remove the `false` value instead. This keeps the matching exchange active.
### Message shape
Transaction events wrap the domain object in an envelope:
```json theme={null}
{
"source": "midaz",
"eventType": "transaction",
"action": "APPROVED",
"timestamp": "2026-07-06T12:00:00Z",
"version": "",
"organizationId": "...",
"ledgerId": "...",
"payload": { "...full transaction JSON..." }
}
```
The routing key encodes the action, for example `midaz.transaction.APPROVED` or `midaz.balance.overdraft.drawn`. You can bind narrowly and avoid a filter in code.
### Setup example
Connection parameters come from the Midaz deployment's RabbitMQ configuration. These include `RABBITMQ_HOST`, `RABBITMQ_PORT_HOST` (the AMQP port used to dial the broker — `3003` in the bundled infrastructure; `RABBITMQ_PORT_AMQP` is the management port despite its name), a consumer user such as `RABBITMQ_CONSUMER_USER`, and `RABBITMQ_VHOST`. In multi-tenant deployments the producer resolves a per-tenant vhost; a single-tenant deployment uses the one static `RABBITMQ_VHOST`. For TLS, set `RABBITMQ_TLS=true` in multi-tenant mode, or `RABBITMQ_URI=amqps` in single-tenant mode.
```python theme={null}
import pika
params = pika.URLParameters("amqps://consumer:***@midaz-rabbitmq:5671/")
conn = pika.BlockingConnection(params)
ch = conn.channel()
# The transaction and overdraft exchanges ship in Midaz's bundled RabbitMQ
# definitions; declare passively or match their topology. The balance-operation
# exchange also ships there but is internal — it feeds Midaz's own async balance
# pipeline (RABBITMQ_TRANSACTION_BALANCE_OPERATION_*), so do not consume from it.
# The audit append-log exchange is NOT in those definitions — declare it yourself.
exchange = "transaction.transaction_events.exchange"
# Bind a durable queue you own to the routing keys you care about.
ch.queue_declare(queue="my-consumer.transactions", durable=True)
ch.queue_bind(
queue="my-consumer.transactions",
exchange=exchange,
routing_key="midaz.transaction.*", # all statuses
)
def on_message(chan, method, props, body):
handle(body) # application/json
chan.basic_ack(method.delivery_tag) # native, per-message ack
ch.basic_qos(prefetch_count=10)
ch.basic_consume(queue="my-consumer.transactions", on_message_callback=on_message)
ch.start_consuming()
```
### When to use it
* You **operate or co-locate** the Midaz deployment and already own the broker.
* You want **native AMQP semantics** — per-message `ack`/`nack`, prefetch/QoS, dead-letter exchanges you control, competing consumers on one queue.
* Your stack is already RabbitMQ-native and you want the lowest-latency, in-cluster hop.
### Trade-offs
* Couples you to Midaz's internal broker topology and credentials.
* The bundled example configuration ships the exchanges **disabled**. A stack that starts from it needs the operator to re-enable them.
* No managed retry/dead-letter/auto-disable — you own delivery reliability on the consumer side.
* Not viable for a third party that has only network access to a hosted Midaz.
## Option B — Streaming Hub
***
### How it works
The Streaming Hub is Lerian's fan-out delivery edge. It consumes events from Midaz's internal event bus and delivers each matched event to a **per-tenant subscriber sink** that you register. You never touch Kafka or the broker. You register a subscription through a REST control plane and choose a delivery method.
Supported sink kinds:
* **`webhook`** — the hub POSTs each event to your HTTPS endpoint, **HMAC-signed** with a per-subscription signing secret.
* **`pull`** — you poll `GET /v1/events`. The read is the acknowledgment (cursor-as-ack), with no inbound endpoint.
* **`sqs`**, **`rabbitmq`**, **`eventbridge`** — the hub delivers into *your* AWS queue, *your* RabbitMQ broker, or *your* EventBridge bus.
The hub adds delivery reliability on top of the raw stream. It provides a durable inbox with consume-once dedup, per-sink retry with backoff, dead-lettering on exhaustion, and auto-disable of failing endpoints.
### Events available
Query the catalog to see the event types you can subscribe to:
```http theme={null}
GET /v1/catalog
Authorization: Bearer
```
```json theme={null}
{
"events": [
{
"eventType": "transaction.posted",
"topic": "lerian.streaming.ledger_transaction.posted",
"schemaVersion": "1.0.0",
"schemaMajor": 1,
"description": "A transaction was posted."
}
]
}
```
Midaz publishes a broad catalog keyed as `.` (all at schema `1.0.0`), including:
* `organization.*`, `ledger.*`, `account.*`, `asset.*`, `portfolio.*`, `segment.*` (`created` / `updated` / `deleted`)
* `operation-route.*`, `transaction-route.*` (`created` / `updated` / `deleted`)
* `balance.created`, `balance.config-changed`, `balance.deleted`
* `balance.overdraft-drawn`, `balance.overdraft-repaid`, `balance.overdraft-cleared`
* `transaction.posted`, `transaction.committed`, `transaction.canceled`, `transaction.reverted`
### Authentication
The control plane is **100% `lib-auth` JWT (Bearer)** — the hub mints no credential of its own. Present a plugin-auth-issued JWT on every `/v1` call:
```http theme={null}
Authorization: Bearer
```
The hub derives the tenant only from the validated JWT claims (`tenantId`, then `owner` as a fallback), never from the request body. A machine client obtains its token from plugin-auth's **client-credentials** flow. It exchanges an application id and secret for a short-lived access token.
### Setup example — webhook subscription
```http theme={null}
POST /v1/subscriptions
Authorization: Bearer
X-Idempotency:
Content-Type: application/json
{
"name": "my-webhook",
"sink_kind": "webhook",
"endpoint": "https://hooks.example.com/lerian",
"event_types": ["transaction.posted", "transaction.committed"],
"schema_major": 1,
"plan_tier": "standard"
}
```
`201 Created` returns the subscription **and the signing secret exactly once**. Store it immediately. You can rotate the secret later, but you can never read it again:
```json theme={null}
{
"subscription": { "id": "0190b8e2-...", "verification_state": "active", "...": "..." },
"signingSecret": "whsec_...ONCE..."
}
```
Verify the HMAC signature on every inbound webhook with that secret. Do this before you trust the payload. Use `POST /v1/subscriptions/:id/ping` to send a signed synthetic event. This confirms that your endpoint works.
### Setup example — pull subscription (serverless-friendly)
Create it with `"sink_kind": "pull"` (omit `endpoint` — the server synthesizes one), then poll:
```http theme={null}
GET /v1/events?subscription_id=&limit=100
Authorization: Bearer
```
```json theme={null}
{
"events": [
{ "seq": 42, "ceId": "...", "eventType": "transaction.posted",
"receivedAt": "2026-07-06T12:00:00Z", "payload": { "...": "..." } }
],
"next_cursor": 42
}
```
The read is the ack: the highest `seq` returned advances a durable, monotonic cursor. Replay the server-issued `next_cursor` as `?after=` on the next call. Each event carries `ceId` for your own dedup.
### Queue sinks (SQS / RabbitMQ / EventBridge)
You create a queue-kind subscription without a credential, and it starts in `pending_verification`. It emits nothing until you supply an outbound credential via `PUT /v1/subscriptions/:id/credential`. The hub probes that credential (connect and auth) and, on success, flips the subscription to `active`. For a RabbitMQ sink, set `endpoint` to `"/"`. The broker host lives in the encrypted credential. For AWS sinks, fetch the IAM trust policy and `ExternalId` from `GET /v1/subscriptions/:id/setup-artifacts`, then wire the cross-account grant before the credential PUT.
### When to use it
* You are an **external integrator** with only network access to a hosted Midaz.
* You want **serverless** delivery — a webhook endpoint or an HTTP pull loop, no broker to run.
* You want **managed reliability** — dedup, retry/backoff, dead-lettering, auto-disable, signed webhooks — without building it yourself.
* You want to fan the same events into **AWS-native** infrastructure (SQS/EventBridge).
### Limitations
* No SSE or WebSocket transport — delivery is webhook push or HTTP pull (plus the queue sinks).
* The catalog is **global** (identical regardless of which tenant authenticates).
* Pull consumers own their cursor position — a forward seek past the cursor permanently skips the gap. Use `?after=` to replay from any prior `seq`.
***
## Decision table
| Criterion | RabbitMQ direct | Streaming Hub |
| ----------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| **Your stack** | Already RabbitMQ / AMQP-native, in-cluster | Anything that speaks HTTPS (or AWS SQS/EventBridge) |
| **Deployment model** | You run / co-locate Midaz and own the broker | Hosted / SaaS Midaz, external integrator |
| **Granular ack** | ✅ Native per-message `ack`/`nack`, prefetch, DLX | Partial — webhook 2xx or pull cursor-as-ack; no per-message nack |
| **Serverless** | ❌ Needs a long-lived AMQP consumer | ✅ Webhook endpoint or stateless pull loop |
| **Simplicity to start** | ❌ Broker creds, vhost, exchange must be enabled | ✅ One authenticated `POST /v1/subscriptions` |
| **Managed reliability** | ❌ You own retry / DLQ / backoff | ✅ Dedup, retry/backoff, dead-letter, auto-disable |
| **Latency** | Lowest (in-cluster hop) | Slightly higher (fan-out edge) |
| **Auth model** | Broker username/password (+ optional TLS) | plugin-auth JWT (Bearer); signed webhooks |
| **Event scope** | Transaction / overdraft / audit exchanges (must be enabled) | Full Midaz catalog via `/v1/catalog` |
| **Fan into AWS** | ❌ Build it yourself | ✅ SQS / EventBridge sinks |
**Rule of thumb:** if you own the broker and want raw AMQP control, go **RabbitMQ direct**. In every other case — external integration, serverless, managed reliability, AWS delivery — use the Streaming Hub.
***
## Security considerations
* **Least-privilege credentials.** For RabbitMQ direct, connect with a **consumer-scoped** user (not the publisher/default user), restricted to the tenant vhost, and enable TLS (`amqps://`). For the Streaming Hub, scope the plugin-auth application to the tenant it represents and rotate the client secret.
* **Verify webhook signatures.** Always validate the HMAC-v1 signature with your per-subscription signing secret before you act on a webhook. Treat an unsigned or mismatched request as hostile.
* **Guard the signing secret.** The hub shows it once at create or rotate, and never again. Store it in a secrets manager, then rotate it via `POST /v1/subscriptions/:id/secret/rotate` (24h dual-sign overlap) if the secret leaks.
* **HTTPS-only endpoints.** Webhook sinks must be `https://`. The hub SSRF-validates endpoints at create and at credential PUT, and it rejects plaintext or private targets.
* **Tenant isolation is claim-derived.** The hub reads tenant identity only from validated JWT claims (or `ce-tenantid` on ingest), never from the request body, so never send `tenant_id` in a payload.
* **Idempotency & dedup.** Send `X-Idempotency` on mutating control-plane calls. On the data plane, dedup on `ceId` (Streaming Hub), the message id, or your own key (RabbitMQ), because both transports are at-least-once.
* **Don't expose Midaz internals.** Never share the internal balance-operation exchange or broker credentials with external consumers, and front third parties with the Streaming Hub instead.
# Recommended workflow
Source: https://docs.lerian.studio/en/midaz/recommended-workflow
Follow the recommended Midaz onboarding workflow to create your Organization, Ledgers, Assets, Account Types, and Accounts in the correct order, step by step.
You have explored how Midaz works, from its core entities to its architecture and security. This page turns that knowledge into a working setup.
A structured workflow keeps every transaction clear, traceable, and auditable from the start. Follow the steps below in order. They set up your Organization, Ledgers, Assets, Account Types, and Accounts.
## Setting up and managing Ledgers
***
When you onboard to Midaz, follow these steps to build a solid base for your financial infrastructure:
### Step 1: [Set up the Organization](/en/reference/midaz/create-an-organization)
Your organization in Midaz represents your bank or business entity. If you have not created it yet, set it up in the Console or API. Make sure the core details are accurate, such as the legal name and identifiers. This context underpins all ledger operations.
### Step 2: [Create the Ledgers](/en/reference/midaz/create-a-ledger)
Create a Ledger under your organization to hold financial records. Most setups start with a primary ledger for all customer-facing operations (for example, "Main Banking Ledger"). At first it contains no accounts. It serves as the base layer for all financial activity.
### Step 3: [Define Asset types](/en/reference/midaz/create-an-asset)
Register the assets your ledger supports. These can include currencies (for example, BRL, USD), points, or crypto tokens. You use these assets across your account and transaction configuration. You can add more assets as your product portfolio grows.
### Step 4: [Create Account Types](/en/reference/midaz/create-an-account-type)
To enforce account classification rules, define your **Account Types**. These are structured categories, for example `user_wallet`, `treasury`, or `revenue`. You reference them later during account creation or transaction validation.
When you **enable** Account Type validation, each new account must use a registered Account Type. By default, Account Types are optional. This keeps classification consistent across your ledger.
Each Account Type has a `keyValue`. The `keyValue` must be unique and alphanumeric, with dashes and underscores allowed. You use these identifiers in validation flows and transaction routes.
### Step 5: Configure Accounting Routes
Use **Operation Routes** and the **Transaction Route** (the `transactionRoute` resource in the API) to define and enforce validation for your transactions.
* First, [**create the Operation Routes**](/en/reference/midaz/create-an-operation-route). Each Operation Route validates one leg of the transaction. For example, the debit must come from an account of type `user_wallet`, and the credit must go to the alias `@external/BRL`.
* Then, [**create the Transaction Route**](/en/reference/midaz/create-transaction-route). It combines the individual operation routes into a complete flow. An example is a cashout transaction with specific debit and credit validations.
After you configure routing and enable validation on the ledger, all transactions must match the defined routing structure. Midaz rejects any transaction that does not match.
The `route` field in the transaction payload must include the ID of the **Transaction Route** you created.
### Step 6: Manage your Ledgers
Keep your ledgers clean and scalable with the following best practices:
* Use Midaz's RBAC system to assign permissions and control access per ledger.
* Plan separate ledgers when needed, for example "Testing", "Internal", or "Multi-entity". Document their ownership.
* Review and reconcile ledger data regularly, especially when you integrate with external settlement systems.
* Midaz enforces double-entry logic, so every movement is fully traceable. Do not change balances manually.
## Configuring Accounts, Portfolios, and Segments
***
This section shows how to structure customer data and product entitlements in Midaz.
### Step 1: [Create a customer Portfolio](/en/reference/midaz/create-a-portfolio)
Create a **Portfolio** for each new customer. Portfolios act as the logical containers for accounts. Link each portfolio to your CRM or internal system through metadata (for example, an Entity ID).
### Step 2: [Set up customer Accounts](/en/reference/midaz/create-an-account)
Each financial product or asset the customer holds becomes a separate **Account** under their portfolio.
* A checking account in BRL and a savings account in USD are separate accounts.
* If you enable [Account Types](/en/reference/midaz/create-an-account-type), the `type` field must match one of the pre-registered values.
* Use account aliases to simplify downstream operations.
Account Types standardize account categorization across your ecosystem. They improve routing, reporting, and integrations.
### Step 3: [Apply segmentation](/en/reference/midaz/create-a-segment)
Use Segments to categorize portfolios or accounts (for example, `standard`, `vip`, `enterprise`). These tags drive dynamic logic based on customer segmentation, such as fee exemptions or tiered interest.
### Step 4: Establish sub-accounts (optional)
For organizations or special cases, you can configure sub-accounts. Use clear naming conventions to keep traceability (for example, "Acme USD - Payroll").
### Step 5: [Set the initial balance](/en/reference/midaz/create-a-transaction-using-json)
Use the **Transactions API** to add the opening balance. This keeps traceability and double-entry compliance:
* Debit the funding source.
* Credit the new account.
* Do not update balances manually. Midaz tracks every value through transactions.
### Step 6: Verify and review
Before you run operations, check the following:
* Confirm each account has the correct assets and types.
* Validate account balances.
* Check that you applied segmentation.
* Confirm the setup in the API or the Console.
### Step 7: Ongoing Account management
Manage lifecycle operations through the API or Console to keep system integrity:
* Update account details as needed.
* Close accounts only once the balance reaches zero.
* Reclassify portfolios/accounts if segmentation changes.
## Implementing double-entry transaction management
***
Midaz guarantees ledger integrity through strict enforcement of double-entry logic. Every transaction must contain at least one debit and one credit operation.
### Step 1: Structure the transaction
Structure your **Transaction** object:
* **After you configure your Transaction Route**, insert its ID into the `route` field of your transaction payload.
* Define the source and destination accounts, with their asset and value details.
* Add descriptive metadata to keep traceability and clarity.
### Step 2: Choose your integration method
Use the **Transactions API** for programmatic transaction creation.
### Step 3: Enforce double-entry validation
Midaz checks that the sum of credits matches the sum of debits. Midaz rejects a transaction that has a mismatch or a structural violation.
### Step 4: Implement idempotency
Use idempotency keys to prevent duplicates from network retries. Midaz recognizes a repeated key and replays the original response instead of processing the request twice.
### Step 5: Track and reconcile transactions
Use built-in tools to:
* Track submitted and posted transactions.
* Audit metadata and timestamps.
* Reconcile transaction batches for compliance.
### Step 6: Submit corrections via reversals
Do not edit or delete transactions. Submit a reversal instead. Midaz links each reversal to its original transaction for full traceability.
### Step 7: Optimize complex workflows
Use multi-operation transactions to:
* Process interest accruals.
* Execute batched payouts.
* Handle conditional flows with routing logic.
Midaz keeps atomicity across all legs. Either the full transaction posts, or nothing does.
# Rest API
Source: https://docs.lerian.studio/en/midaz/rest-api
Integrate with Midaz through a versioned REST API — JSON payloads, explicit versioning, and a predictable, resource-based design.
The Midaz REST API gives you programmatic access to the ledger. It uses standard HTTP methods and status codes.
## Key features
***
* **RESTful design**: The API follows REST principles for a predictable structure.
* **JSON format**: The documented API uses JSON for requests and responses.
* **Versioning**: Explicit version prefixes keep changes backward-compatible.
The API exposes the core Midaz resources: organizations, ledgers, accounts, assets, portfolios, transactions, and operations. Each resource has its own endpoints.
For technical details on the available endpoints, see the [API Reference](/en/reference/introduction).
# Scalability strategies
Source: https://docs.lerian.studio/en/midaz/scalability-strategies
Scale Midaz to millions of Accounts and Transactions per day — partitioning by Ledgers, horizontal service scaling, and read replicas.
Midaz handles millions of accounts and transactions per day. You start small and scale horizontally as demand grows, with no re-architecture. For a business overview, see [About Midaz](/en/midaz/about-midaz).
How you deploy and use Midaz affects its performance. To handle large banking operations, use ledgers to segment workloads and scale each service on its own.
## Structuring entities for High Transaction Volumes
***
### Partitioning by Organizations, Ledgers, and Accounts
Does your bank process millions of transactions each day? Distribute the workload across many accounts, ledgers, or organizations. For example, segment customers by last-name initial, or assign a separate ledger to each region.
Each Midaz ledger is an independent book. Midaz processes the transactions in a ledger together. This split stops a single ledger from becoming a bottleneck. Midaz scales out: different ledgers can run on separate instances or database partitions.
### Lock-oriented Operations
Midaz applies each balance batch atomically in a single Redis Lua script, so a batch either lands in full or rolls back in full. Concurrency is handled optimistically: each balance carries a version, and on overdraft repayment paths a version mismatch aborts the whole batch with error `0174` so the caller re-reads state and retries.
The `@external` account skips the balance-floor and overdraft checks. It can go negative without limit, so transactions against it never fail on available funds. Each transaction still commits atomically — all operations apply, or none do.
### Horizontal scaling of services
Midaz uses a microservices architecture. You scale each component on its own — for example, transaction processing and queries. Start small. As demand grows, scale the services that need it.
For high loads, deploy several instances of the transaction service behind a load balancer. Use Kubernetes (K8s) pod scaling.
### CQRS and read replicas
Midaz uses CQRS (Command Query Responsibility Segregation) to separate write and read operations. You optimize each path on its own.
Route heavy reporting and query loads to read replicas. This keeps transaction commits fast. Reads do not slow down writes, and writes do not slow down reads.
### Batching and N:N transactions
Where possible, batch related operations into a single transaction. This reduces overhead. Use N:N transactions for bulk work such as mass payouts.
For example, one batch of 100 debits and 100 credits is often faster than 100 separate transactions. This works when the operations belong to the same logical set. Midaz processes these N:N transactions in one atomic commit.
### Monitoring and iterative scaling
Monitor the key metrics: transaction latency, throughput, resource use (CPU and memory), and database performance. Use the Midaz observability tools (OpenTelemetry and Grafana), or send the metrics to your own monitoring stack.
Scale out or up before you reach a performance threshold. Midaz is modular and source-available, so you deploy the parts you need.
## Handling multi-currency and multi-entity Operations
***
### Multi-currency strategies
Midaz supports multi-currency transactions with a separate asset account for each currency. To scale well:
* Configure every currency asset you need.
* Handle currency-specific logic — such as rounding and conversion rates — in your integration layer.
* For frequent currency conversion, such as forex trading, run the conversion in a separate service. Midaz records the debits and credits, but an external FX-rate service supplies the rates.
* You rarely need a separate ledger per currency, unless policy requires it. Instead, use account types and the transaction-routing API to group accounts by currency for clear reporting.
* If one currency covers most transactions, keep all currencies in the same ledger. If multi-currency operations grow too complex, split them into a separate ledger per currency group.
### Multiple Entities: Consolidation and separation
For a bank that operates across countries or legal entities:
* Use the organization hierarchy to segment entities. Give each entity its own base currency, local assets, and ledgers.
* Model inter-entity transactions as external movements. One organization credits its external account, and the other debits its own.
* You can run several organizations on one Midaz instance. Allocate enough resources: the microservices process all requests together, so total volume matters.
* You can deploy a separate Midaz instance per isolated entity, but you lose unified visibility. One cluster with internal organization separation is usually more practical.
## Performance optimization best practices
***
### Indexing and queries
Use the Midaz APIs to read data, not direct database queries. These APIs carry predefined indexes for the core identifiers: accounts, portfolios, and transactions.
### Indexing custom metadata fields
Midaz creates no default index on the metadata collections — metadata documents are keyed by `entity_id`. Create the indexes you need explicitly through the Metadata Index API.
By default, Midaz does **not** index **custom metadata fields**. Clients fill these fields freely as key-value pairs.
Frequent queries or filters on non-indexed fields can trigger full collection scans. These scans slow down as the data volume grows.
Create an index when you regularly use a custom metadata field in **search filters or sort order**. With an index, MongoDB finds the documents fast and cuts query response time.
**Recommendations:**
* Index only the metadata fields you query or sort on often.
* Do not index rarely-accessed fields, because each extra index adds write overhead.
Manage metadata indexes through dedicated APIs:
* [Create a Metadata Index](/en/reference/midaz/create-a-metadata-index)
* [List Metadata Indexes](/en/reference/midaz/list-metadata-indexes)
* [Delete a Metadata Index](/en/reference/midaz/delete-a-metadata-index)
### Testing at scale
Before full deployment, simulate high traffic with load-testing tools. Create millions of accounts, run heavy transaction volumes, and monitor performance.
The Midaz architecture (microservices and CQRS) targets high-scale operations, but real-world tests help you tune your setup. Look for linear scalability: more application instances should raise throughput in proportion.
# Segments
Source: https://docs.lerian.studio/en/midaz/segments
Group Accounts into Segments to deliver tailored benefits — VIP rates, student perks, fee waivers, and personalized rules across Portfolios.
A **Segment** is a group of Accounts that share a characteristic, such as a product tier or a customer type. Financial institutions use Segments to apply tailored rules, services, and conditions to each group.
Each Segment has a **Segment ID**, a unique identifier. Financial institutions use this ID to track and manage each Segment.
#### Examples
* **Black or diamond (Cards)**: a premium group of card accounts for customers who want exclusive benefits.
* Included benefits: VIP lounge access, insurance coverage, a dedicated concierge, and loyalty rewards.
* **Student account (Banking Accounts)**: a group of banking accounts for students.
* Included benefits: no account fees, cashback on education purchases, and discounts on cinemas and events.
## Segment structure
***
* **Segment > Accounts:** You tag each Account with a `segmentId` (a Segment UUID) to add it to a Segment.
* A Segment belongs to one Ledger. Accounts in different Portfolios can share the same Segment.
## Key characteristics
***
* Segments give you a scalable way to apply tailored policies to Accounts.
* A Segment can include many Accounts.
## Managing Segments
***
You manage your Segments through the API or Lerian Console.
### Via API
* [List Segments](/en/reference/midaz/list-segments) — View all Segments in your workspace.
* [Retrieve a Segment](/en/reference/midaz/retrieve-a-segment) — Get the details of a specific Segment.
* [Create a Segment](/en/reference/midaz/create-a-segment) — Register a new Segment.
* [Update a Segment](/en/reference/midaz/update-a-segment) — Edit an existing Segment.
* [Delete a Segment](/en/reference/midaz/delete-a-segment) — Remove a Segment you no longer use.
### Via Lerian Console
You view, create, edit, and delete Segments on the Segments page in the Midaz Module of Lerian Console.
[**Learn more in the Managing Segments guide.**](/en/midaz/console/managing-segments)
# Structuring bank operations with Midaz
Source: https://docs.lerian.studio/en/midaz/structuring-bank-operation
Design Organizations, Ledgers, Portfolios, and Segments for retail and corporate banking — best practices for scalable hierarchies.
Midaz supports retail and corporate banking through a clear entity hierarchy. This page gives best practices to structure organizations, ledgers, accounts, portfolios, and segments. A good structure keeps operations simple as you scale.
## Optimal hierarchy of Organizations and Ledgers
***
### Single vs. multiple Organizations
A bank that operates as a single legal entity needs one Organization in Midaz. For a banking group with subsidiaries, use a parent organization over several child organizations. This structure matches your corporate governance. It also isolates data per entity.
The organization structure should reflect your corporate structure.
### Using multiple Ledgers
Within an organization, choose the number of ledgers from your operational needs. Keep a **primary ledger** for customer transactions. Add ledgers for special purposes, such as treasury operations or regulatory segmentation.
Use multiple ledgers only when necessary to keep complexity low. A transfer between ledgers needs external orchestration through the API.
## Efficient account structuring for retail and corporate banking
***
### Retail customers – Portfolio per customer
For retail banking, create a **Portfolio for each customer**. Each portfolio holds one account per asset type, such as checking, savings, or credit card. This structure makes balances easy to read per customer.
Use [CRM](/en/midaz/crm/crm-data-security) to manage personal data such as tax IDs, addresses, and banking aliases — all in one place.
### Corporate clients – hierarchical accounts
For corporate banking, use **child accounts** within portfolios to reflect internal structure. A company can hold a parent account for its main funds. It can add child accounts for subdivisions, such as payroll or expense tracking.
### Internal accounts
Create internal accounts for revenue, expenses, and settlement flows. Named accounts like "Fee Income – USD" or "Interest Expense – USD" keep financial reporting clear.
## Worked example: a typical digital bank
***
This example shows how a digital bank can structure its Midaz deployment. The bank offers checking accounts, savings, and Pix transfers.
```
Organization: "Neobank S.A."
│
├── Ledger: "Main Ledger (BRL)"
│ │
│ ├── Segments
│ │ ├── "Standard" (default tier)
│ │ ├── "VIP" (premium customers)
│ │ └── "Business" (corporate accounts)
│ │
│ ├── Account Types
│ │ ├── "checking" → everyday transactions
│ │ ├── "savings" → interest-bearing deposits
│ │ ├── "settlement" → internal clearing
│ │ └── "fee-revenue" → bank fee collection
│ │
│ ├── Internal Accounts
│ │ ├── Settlement Account (clearing for Pix, TED)
│ │ ├── Fee Revenue Account
│ │ └── Interest Expense Account
│ │
│ └── Customer Portfolios
│ ├── Portfolio: "Maria Silva"
│ │ ├── Checking Account (BRL) — Segment: VIP
│ │ └── Savings Account (BRL) — Segment: VIP
│ │
│ └── Portfolio: "Tech Corp Ltda."
│ ├── Main Checking (BRL) — Segment: Business
│ ├── Payroll Account (BRL) — Segment: Business
│ └── Expense Account (BRL) — Segment: Business
│
└── Ledger: "Treasury"
└── FX and interbank positions
```
### How a Pix transfer flows through this structure
Maria sends R\$ 500 via Pix from her checking account.
The "Pix Out" Transaction Route applies. It defines a debit from Maria's checking account and a credit to the Settlement Account. It can also add a fee operation.
Midaz creates four operations atomically. It debits R$ 500 from Maria and credits R$ 500 to Settlement. It also debits a R$ 0.50 fee from Maria and credits R$ 0.50 to Fee Revenue.
Midaz emits a transaction event. The bank's notification service reads the event and sends Maria a push notification.
An external integration reconciles the Settlement Account with the central bank's Pix infrastructure.
### Decision guide: when to split ledgers
| Scenario | Recommendation |
| :---------------------------------------- | :------------------------------------------------------------- |
| Single currency, single business unit | One ledger is enough |
| Multiple currencies with occasional FX | One ledger, separate asset accounts per currency |
| Separate legal entities or subsidiaries | One ledger per entity, under separate organizations |
| Regulatory requirement for data isolation | Separate ledgers, or separate organizations for full isolation |
| Treasury and trading operations | Dedicated treasury ledger alongside operational ledger |
When in doubt, start with a single ledger. You can always split later as operational complexity grows. If you split too early, you add orchestration overhead without clear benefit.
# Accounting Routes
Source: https://docs.lerian.studio/en/midaz/transaction-routing-entities
Validate every Transaction with Accounting Routes and Operation Routes — enforce structure and business rules before recording any movement.
Accounting Routes are Midaz's two-layer validation system for financial transactions. **Accounting Routes** define the full transaction pattern. **Operation Routes** validate each operation inside that pattern. Together they keep every transaction structurally correct and compliant with your business rules.
**Naming:** The Lerian Console and product documentation call this concept **Accounting Routes**. In the API and SDKs, the `transactionRoute` resource represents the transaction-level route, with `transaction-route` endpoints. The two terms refer to the same thing.
* **Accounting Routes** define the complete structure of a transaction: the required sequence of operations that forms a valid financial event.
* **Operation Routes** define the rules for each operation (or "leg") of that transaction. Each rule sets the expected account type or specific account, the accounting annotation, and the debit or credit side.
When you submit a transaction, Midaz validates it in two layers. The Accounting Routes layer checks that the overall structure matches the predefined pattern. The Operation Routes layer checks that each component meets account requirements and business rules.
If any part of the transaction fails these checks, Midaz rejects it before it records the transaction. This protects the integrity of your ledger and does not limit its flexibility.
You define the validation patterns through Operation Routes and Accounting Routes. Midaz checks that your transactions comply with these rules before it processes them.
## What are Accounting Routes for?
***
Accounting Routes provide structured control over your financial operations by separating transaction logic from business code. Instead of hardcoding validation rules in your application, you configure reusable patterns. These patterns make every financial movement follow your organization's requirements.
These entities link Transactions and Operations from the Midaz ledger to higher-level abstractions. These abstractions help you integrate specialized plugins and external systems, especially for **accounting and treasury**. The structured annotations and classifications create a standardized vocabulary that other components can understand and use.
This approach delivers:
* **Consistency**: All transactions follow predefined structures regardless of where they originate.
* **Flexibility**: Adapt your ledger design to match your business needs without code changes.
* **Integrity**: Automatic validation prevents malformed transactions from affecting your ledger.
* **Maintainability**: Centralized configuration makes it easier to update financial rules as your business evolves.
* **Interoperability**: Business-semantic fields let you integrate accounting plugins and external financial systems.
Accounting Routes keep your financial data structured and validated for simple transfers and complex multi-party transactions. They also provide the semantic foundation for advanced integrations.
## Working with Accounting Routes
***
To use Accounting Routes, you complete a one-time setup and then execute transactions. The steps below show the full process.
### Initial Setup
#### 1. Configure Ledger for transaction route validation
To activate transaction route validation for a specific Ledger, enable the validation settings through the [Ledger Settings API](/en/midaz/ledgers#ledger-settings). This controls whether transactions in that Ledger must comply with your configured routes.
```json PATCH /v1/organizations/{org_id}/ledgers/{ledger_id}/settings theme={null}
{
"accounting": {
"validateRoutes": true,
"validateAccountType": true
}
}
```
* **`validateRoutes`**: When enabled, every transaction must reference a valid transaction route.
* **`validateAccountType`**: When enabled, Midaz rejects an account whose `type` is not a registered Account Type. This gates **account creation**, not transactions — the `account_type` rule on an Operation Route is enforced by `validateRoutes`, independent of this flag.
Settings changes need no redeployment: you update them at any time through the API. The write invalidates the settings cache, but reads are cached for **5 minutes**, so allow up to that long for a change to be observed by every replica.
#### 2. Create Operation Routes
Create Operation Routes that define validation rules and behavior for individual transaction components.
**Key fields:**
* **title**: Brief label that identifies the operation route.
* **code** (deprecated): a legacy external reference kept for backward compatibility. The engine does **not** write it to operations. Instead, it records the resolved rubric's `code` (from `accountingEntries`) as `routeCode` on each operation.
* **description**: Optional detailed explanation.
* **metadata**: Key-value pairs for business context and custom categorization.
* **operationType**: The accounting direction for this route — `source`, `destination`, or `bidirectional`.
* `source` — Identifies accounts where funds originate (debit side).
* `destination` — Identifies accounts that receive funds (credit side).
* `bidirectional` — Applies to both sides of the transaction, as both source and destination.
* **account**: Optional validation rules that set a required account type or a specific account.
* **ruleType**: Type of account validation rule (`account_type`, `alias`).
* **validIf**: The expected value that must match for validation to pass.
* **accountingEntries**: Optional accounting entries for each action type. See [Accounting Entries](#4-configure-accounting-entries-actions) below.
Configure account rules based on your needs:
**Option A: No Account Rule**
If you don't need account validation for the operation route, omit the account object:
```json JSON theme={null}
{
"title": "Fee Collection",
"description": "Operation route for collecting service fees from user transactions",
"metadata": {
"businessUnit": "payments",
"category": "revenue"
},
"operationType": "source"
}
```
**Option B: Account Validation Rule**
If you need account validation for the operation, configure account rules based on your ledger setup:
* **Target Specific Account**
Validate against a specific account using its alias.
```json JSON theme={null}
{
"title": "Fee Revenue Collection",
"description": "Operation route for crediting collected fees to revenue account",
"metadata": {
"businessUnit": "payments",
"category": "revenue"
},
"operationType": "destination",
"account": {
"ruleType": "alias",
"validIf": "@external/BRL"
}
}
```
* **Target Account Type**
Validate against specific account types.
```json JSON theme={null}
{
"title": "User Cashout Fee",
"description": "Operation route for collecting fees from user cashout transactions",
"metadata": {
"businessUnit": "payments",
"category": "fee"
},
"operationType": "source",
"account": {
"ruleType": "account_type",
"validIf": ["user_wallet", "asset"]
}
}
```
**Option C: With Accounting Entries**
Attach accounting entries directly to the operation route through the `accountingEntries` field. This field maps each transaction-lifecycle stage to the correct double-entry accounting codes. See [Configure Accounting Entries (Actions)](#4-configure-accounting-entries-actions) below for the full action-type model, the debit/credit requirements, and the validation matrix.
A route with accounting entries configured:
```json JSON theme={null}
{
"title": "Pix Cash-in - Current Account",
"description": "Operation route for receiving Pix payments into current account",
"operationType": "source",
"accountingEntries": {
"direct": {
"debit": {
"code": "1.1.001",
"description": "Cash - Available funds"
},
"credit": {
"code": "3.1.001",
"description": "Service Revenue"
}
},
"hold": {
"debit": {
"code": "1.1.002",
"description": "Clearing Values"
},
"credit": {
"code": "2.1.001",
"description": "Pending Obligations"
}
},
"commit": {
"debit": {
"code": "2.1.001",
"description": "Pending Obligations"
},
"credit": {
"code": "3.1.001",
"description": "Service Revenue"
}
},
"cancel": {
"debit": {
"code": "2.1.001",
"description": "Pending Obligations"
},
"credit": {
"code": "1.1.002",
"description": "Clearing Values"
}
}
},
"account": {
"ruleType": "alias",
"validIf": "@current_account"
},
"metadata": {
"channel": "pix"
}
}
```
The `operationType` field also supports `bidirectional`. A bidirectional route operates in both directions. Use it for routes that both send and receive, or for operations that you may need to reverse.
#### 3. Build Accounting Routes
Complete your setup by combining Operation Routes into Accounting Routes (the `transactionRoute` resource in the API). These define your complete transaction patterns. Each pattern maps how operations work together to form balanced financial events that match your business processes.
The `operationRoutes` field uses an array of objects with `operationRouteId` rather than a plain array of UUID strings.
```json JSON theme={null}
{
"title": "Fee Transaction",
"description": "Complete transaction for collecting fees from user cashout operations",
"metadata": {
"transactionType": "cashout_fee",
"businessFlow": "withdrawal_processing"
},
"operationRoutes": [
{
"operationRouteId": "0197e6aa-1695-734a-a8c3-8c79e0ad32c2"
},
{
"operationRouteId": "0197e675-37cc-71d7-96c2-f58000f33aa0"
}
]
}
```
#### 4. Configure Accounting Entries (Actions)
Each Operation Route can include **Accounting Entries**. These structured rubrics define how Midaz records debit and credit entries for each transactional event: `direct`, `hold`, `commit`, `cancel`, and `revert`, plus three supplementary keys — `overdraft`, `block`, and `unblock` — that describe accounting impact but are **not** valid transaction-route actions. The engine uses them to resolve which accounts it debits and credits for each action. They also determine the `routeCode` and `routeDescription` annotations on each operation.
The `accounting.validateRoutes` setting in the [Ledger Settings](/en/midaz/ledgers#ledger-settings) controls this behavior. When you enable it, Midaz rejects an operation route that is missing or does not match, and returns `0117 ErrAccountingRouteNotFound`. When you disable it, route resolution is best-effort. A missing rubric leaves `routeCode` empty and does not stop the transaction.
The **[Accounting Entries](/en/midaz/accounting-entries)** page documents the full model in detail. This includes the accounting-entry actions, the debit/credit requirements per operation type, the graceful and strict validation modes, and configuration examples. This section covers only how rubrics attach to Operation Routes.
At the route level, you supply accounting entries through the `accountingEntries` block. See **Option C** under [Create Operation Routes](#2-create-operation-routes) above. Each action takes one entry with a `debit` rubric, a `credit` rubric, or both, depending on the route's `operationType`:
* **Source** routes require the **debit** rubric.
* **Destination** routes require the **credit** rubric.
* **Bidirectional** routes require **both** debit and credit rubrics.
##### Accounting entries validation matrix
Not every combination of `operationType` and action is valid. Midaz enforces a strict validation matrix when you create or update an Operation Route. If the rules do not match, Midaz rejects the request before it persists the route.
This matrix is critical for integrators. An invalid combination returns error `0166` (field required) or `0162`/`0165` (scenario not allowed for the direction).
**source**
| Action | Debit | Credit | Notes |
| :------- | :------- | :------- | :------------------------------------------------------ |
| `direct` | Required | Optional | Standard one-step transaction at origin |
| `hold` | Required | Required | Reserves funds — moves available → on\_hold |
| `commit` | Required | Optional | Finalizes a two-phase transaction |
| `cancel` | Required | Required | Releases reserved funds — moves on\_hold → available |
| `revert` | — | — | Not allowed (error `0165`). Use `bidirectional` instead |
**destination**
| Action | Debit | Credit | Notes |
| :------- | :------- | :------- | :------------------------------------------------------ |
| `direct` | Optional | Required | Standard one-step transaction at destination |
| `hold` | — | — | Not allowed (error `0162`) |
| `commit` | Optional | Required | Finalizes a two-phase transaction |
| `cancel` | — | — | Not allowed (error `0162`) |
| `revert` | — | — | Not allowed (error `0165`). Use `bidirectional` instead |
**bidirectional**
| Action | Debit | Credit | Notes |
| :------- | :------- | :------- | :---------------------------------- |
| `direct` | Required | Required | Both sides of the double entry |
| `hold` | Required | Required | Both sides of the double entry |
| `commit` | Required | Required | Both sides of the double entry |
| `cancel` | Required | Required | Both sides of the double entry |
| `revert` | Required | Required | Only direction that supports revert |
If an entry has neither `debit` nor `credit`, Midaz rejects it, regardless of operation type or action.
**Additional rules:**
* **Reserve group atomicity**: On `source` and `bidirectional` routes, if you define `hold`, you must also define `commit` and `cancel` (and vice versa). These three actions form an atomic group there; you can't configure one without the others. On `destination` routes, `hold` and `cancel` are not allowed (error `0162`), so `commit` can be configured without them; it still requires `direct` per the rule below.
* **Direct is mandatory**: If you define any other action (`hold`, `commit`, `cancel`, `revert`, `overdraft`, `block`, `unblock`), you must also define `direct`. It serves as the baseline entry for the operation route.
* **`overdraft` requires both rubrics** on every `operationType`, including `source` and `destination`.
* **`block` and `unblock` mirror `direct`**: debit on a `source` route, credit on a `destination` route, both on `bidirectional`.
* Any key outside these eight is rejected with error `0053` (Unexpected Fields).
When you design your operation routes, start with the `direct` action. Add `hold`/`commit`/`cancel` only if you need two-phase transaction support. Add `revert` only on `bidirectional` routes.
### Ongoing Operations
#### 5. Execute Validated Transactions
With your routing configuration in place, you can now submit transactions. In the transaction request, **include the ID of the Accounting Route you created**. Midaz then validates the transaction against your routing patterns. This keeps all financial operations consistent and correct.
For the Accounting Route and Operation Routes configured above, Midaz composes the following validation structure:
```bash text theme={null}
Transaction Route: "Fee Transaction" (ID: 5656daa5-5b2a-4637-955f-e43bafceaf5d)
├── Operation Route 1: "User Cashout Fee" (ID: 0197e6aa-1695-734a-a8c3-8c79e0ad32c2)
│ ├── Type: source
│ ├── Account Rule: account_type ["user\_wallet", "asset"]
│ └── Validates: source operations in transactions
└── Operation Route 2: "Fee Revenue Collection" (ID: 0197e675-37cc-71d7-96c2-f58000f33aa0)
├── Type: destination
├── Account Rule: alias "@external/BRL"
└── Validates: destination operations in transactions
```
For route properties on Midaz transactions, an appropriate payload request:
```json JSON expandable theme={null}
{
"routeId": "5656daa5-5b2a-4637-955f-e43bafceaf5d",
"description": "Cashout fee collection transaction",
"send": {
"asset": "BRL",
"value": "10",
"source": {
"from": [
{
"accountAlias": "@user/wallet_123",
"amount": {
"asset": "BRL",
"value": "10"
},
"description": "Fee debit from user wallet",
"routeId": "0197e6aa-1695-734a-a8c3-8c79e0ad32c2"
}
]
},
"distribute": {
"to": [
{
"accountAlias": "@external/BRL",
"amount": {
"asset": "BRL",
"value": "10"
},
"description": "Fee credit to revenue account",
"routeId": "0197e675-37cc-71d7-96c2-f58000f33aa0"
}
]
}
}
}
```
When you submit this transaction, Midaz validates two things. The `@user/wallet_123` account must match the `user_wallet` account type rule. The `@external/BRL` account must match the exact alias. Both checks confirm that the transaction follows your routing patterns.
##### Route fields on operations
When you enable route validation and configure accounting entries, every processed operation includes two extra fields. Midaz populates these fields from the matched rubric:
* **routeCode** — The `code` of the resolved `AccountingRubric` for that operation's action and direction.
* **routeDescription** — The description of the resolved accounting rubric. Midaz populates it alongside `routeCode`.
These fields link each operation to its accounting classification. Downstream systems such as [Reporter](/en/reporter/what-is-reporter) can then produce accurate financial reports without extra lookups.
## Managing Operation and Accounting Routes
***
To **configure your Operation Routes**, use the following endpoints:
* [Create an Operation Route](/en/reference/midaz/create-an-operation-route) — Define a new accounting rule for your operations.
* [List Operation Routes](/en/reference/midaz/list-operation-routes) — View all configured Operation Routes.
* [Retrieve an Operation Route](/en/reference/midaz/retrieve-an-operation-route) — Get detailed information on a specific Operation Route.
* [Update an Operation Route](/en/reference/midaz/update-an-operation-route) — Modify existing accounting rules.
* [Delete an Operation Route](/en/reference/midaz/delete-an-operation-route) — Remove an outdated or unused Operation Route.
To **configure your Accounting Routes** (the `transactionRoute` resource in the API), use the following endpoints:
* [Create a Transaction Route](/en/reference/midaz/create-transaction-route) — Define new routing logic to connect transactions to accounting operations.
* [List Transaction Routes](/en/reference/midaz/list-transaction-routes) — View all configured Transaction Routes.
* [Retrieve a Transaction Route](/en/reference/midaz/retrieve-a-transaction-route) — Get details of a specific Transaction Route.
* [Update a Transaction Route](/en/reference/midaz/update-a-transaction-route) — Modify existing routing criteria.
* [Delete a Transaction Route](/en/reference/midaz/delete-a-transaction-route) — Remove routes that are no longer applicable.
# Transactions
Source: https://docs.lerian.studio/en/midaz/transactions
Record financial events with Midaz double-entry Transactions — multiple Balances, debits, credits, and full traceability across Accounts.
A **Transaction** in Midaz records a complete financial event. A transaction often uses multiple accounts and balances. Midaz runs on a double-entry accounting system that keeps every financial movement balanced.
With the **multiple balances** feature, each operation specifies the account and the **balance key** to use. You can then debit or credit different logical balances of the same account (for example, `credit`, `operational`, or `collateral`).
If you do not provide a `balanceKey`, the transaction uses the **default balance**.
## Double-entry accounting
***
The double-entry system follows one principle. Every transaction has two entries: a debit and a credit. This structure records all financial activity and keeps your accounts balanced.
Each transaction affects two accounts and keeps them in balance:
* **Debits** show the value received or the resources consumed.
* **Credits** show the value given or the resources provided.
Midaz tracks and balances every debit and credit automatically.
### Example
In this example, you transfer R\$1000 from one account to another. The transaction has two operations:
* One operation to debit R\$1,000.00 from the source account.
* One operation to credit R\$1,000.00 to the destination account.
Midaz captures both entries automatically. You can view and analyze these movements through the API or Lerian Console.
## N:N Transactions (Many-to-Many)
***
Traditional financial systems limit transactions to one-to-one or one-to-many relationships. Midaz supports N:N transactions. A single transaction can use multiple source and destination accounts.
### Examples
* **Marketplace payout**: a single escrow account pays multiple sellers, and each seller pays a platform fee.
* **Peer-to-peer with fees**: one transaction debits the payer and credits both the payee and a fee account.
Midaz processes each case as a single atomic transaction. It debits and credits all parties together.
## Atomicity and integrity
***
Transactions are atomic. **Either all operations succeed, or none do.** Partial financial events do not occur.
If any part of a transaction fails validation — for example, one account has insufficient funds — Midaz does not apply the transaction. The ledger stays consistent.
## Transaction source
***
A transaction in Midaz can start from a single source or from multiple sources.
The sum of the values in `source` must equal the value after `send`. It must also equal the sum of the values in `distribute`.
### Single source
In a single-source transaction, Midaz takes the amount from one source account. You can also name a specific balance.
#### Example
In this example (*Figure 1*):
* Midaz takes BRL 30.00 from `@account1` (balance `credit`).
* It sends 100% to `@destinationAccount1` (balance `operational`)
**Code examples**
```json JSON Example expandable theme={null}
{
"description": "single source transaction",
"send": {
"asset": "BRL",
"value": "30.00",
"source": {
"from": [
{
"accountAlias": "@account1",
"balanceKey": "credit", // optional
"amount": {
"asset": "BRL",
"value": "30.00"
}
}
]
},
"distribute": {
"to": [
{
"accountAlias": "@destinationAccount1",
"balanceKey": "operational", // optional
"share": {
"percentage": 100
}
}
]
}
}
}
```
### Multi-source
In a multi-source transaction, Midaz draws funds from multiple accounts or balances.
#### Example
In this example (*Figure 2*):
* Midaz sends BRL 30.00 to the destination account (`@destinationAccount1`).
* BRL 15.00 from `@account1` (balance `default`).
* BRL 15.00 from `@account2` (balance `investment`).
* The destination account receives 100% of the amount.
**Code examples**
```json JSON Example theme={null}
{
"description": "multi-source transaction",
"send": {
"asset": "BRL",
"value": "30.00",
"source": {
"from": [
{
"accountAlias": "@account1",
"balanceKey": "default",
"amount": {
"asset": "BRL",
"value": "15.00"
}
},
{
"accountAlias": "@account2",
"balanceKey": "investment",
"amount": {
"asset": "BRL",
"value": "15.00"
}
}
]
},
"distribute": {
"to": [
{
"accountAlias": "@destinationAccount1",
"share": {
"percentage": 100
}
}
]
}
}
}
```
## Transaction destination
***
Like sources, destinations can be single or multiple.
### Single destination
In a single-destination transaction, Midaz sends the amount to only one destination account.
#### Example
In this example (*Figure 3*):
* Midaz takes BRL 30.00 from an external account (`@external/BRL`).
* It sends 100% to the destination account (`@destinationAccount1`).
**Code examples**
```json JSON Example theme={null}
{
"description":"single destination transaction",
"send":{
"asset":"BRL",
"value":"30.00",
"source":{
"from":[
{
"accountAlias":"@external/BRL",
"amount":{
"asset":"BRL",
"value":"30.00"
}
}
]
},
"distribute":{
"to":[
{
"accountAlias":"@destinationAccount1",
"share":{
"percentage":100
}
}
]
}
}
}
```
### Multi-destination
In a multi-destination transaction, Midaz divides the amount among multiple destination accounts. You can distribute values by shares, fixed amounts, or the remaining balance.
#### Example
In this example (*Figure 4*):
* Midaz takes BRL 100 from the source account (`@account1`).
* 38% of the amount goes to account 2 (`@account2`).
* 50% goes to account 3 (`@account3`).
* A fixed BRL 2.00 goes to account 4 (`@account4`).
* The remaining amount goes to account 5 (`@account5`).
**Code example**
```json JSON Example theme={null}
{
"description":"multi-destination transaction",
"send":{
"asset":"BRL",
"value":"100.00",
"source":{
"from":[
{
"accountAlias":"@account1",
"amount":{
"asset":"BRL",
"value":"100.00"
}
}
]
},
"distribute":{
"to":[
{
"accountAlias":"@account2",
"share":{
"percentage":38
}
},
{
"accountAlias":"@account3",
"share":{
"percentage":50
}
},
{
"accountAlias":"@account4",
"amount":{
"asset":"BRL",
"value":"2.00"
}
},
{
"accountAlias":"@account5",
"remaining":"remaining"
}
]
}
}
}
```
## Multi-source and multi-destination
***
These transactions use multiple sources and multiple destinations. They are useful for cases like a crowdfunding campaign. Midaz pools the contributions and distributes them among multiple recipients.
#### Example
In this example (*Figure 5*):
* The donation is BRL 4,000.00. Midaz takes it from four different accounts.
* 25% comes from account 1 (`@account1`).
* 25% comes from account 2 (`@account2`).
* 40% comes from account 3 (`@account3`)
* 10% comes from account 4 (`@account4`).
* Midaz distributes the donations to four separate accounts. Each account receives a 25% share of the total.
**Code examples**
```json JSON Example theme={null}
{
"description":"multi-source and multi-destination transaction",
"send":{
"asset":"BRL",
"value":"4000.00",
"source":{
"from":[
{
"accountAlias":"@account1",
"share":{
"percentage":25
}
},
{
"accountAlias":"@account2",
"share":{
"percentage":25
}
},
{
"accountAlias":"@account3",
"share":{
"percentage":40
}
},
{
"accountAlias":"@account4",
"share":{
"percentage":10
}
}
]
},
"distribute":{
"to":[
{
"accountAlias":"@donation1",
"share":{
"percentage":25
}
},
{
"accountAlias":"@donation2",
"share":{
"percentage":25
}
},
{
"accountAlias":"@donation3",
"share":{
"percentage":25
}
},
{
"accountAlias":"@donation4",
"share":{
"percentage":25
}
}
]
}
}
}
```
## Transaction statuses
***
Every transaction in Midaz has a status. The status reflects its current stage in the lifecycle. You need these statuses to design transaction flows, configure event consumers, and read ledger data.
| Status | What it means | Affects balances? | How it's created |
| :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------- |
| `CREATED` | A reversal transaction was initiated and is being processed. This is a transient status that automatically progresses to `APPROVED` once the reversal completes. | Yes | [Revert a Transaction](/en/reference/midaz/revert-a-transaction) |
| `APPROVED` | The transaction completed successfully. Funds have been moved between accounts. | Yes | Direct transaction (no `pending` flag), commit of a `PENDING` transaction, or automatic progression from `CREATED` |
| `PENDING` | A two-phase transaction is awaiting confirmation. Funds are reserved in `on_hold` but not yet transferred. | Yes (reservation) | [Create a Transaction](/en/reference/midaz/create-a-transaction-using-json) with `"pending": true` |
| `CANCELED` | A two-phase transaction was canceled. Reserved funds are released back to `available`. | Yes (release) | [Cancel a Pending Transaction](/en/reference/midaz/cancel-a-pending-transaction) |
| `NOTED` | An annotation transaction recorded in the ledger without affecting balances. Operations preserve the double-entry structure but all balance fields are zeroed. | No | [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) |
Use the `NOTED` status to import legacy transactions, record audit trails, and log compliance events. It fits any case where the transaction must exist in the ledger but the balances settled elsewhere already.
### Status transitions
Transactions follow predictable paths through these statuses:
* **Standard flow:** → `APPROVED` (single step)
* **Two-phase flow:** → `PENDING` → `APPROVED` (commit) or `CANCELED` (cancel)
* **Reversal flow:** → `CREATED` → `APPROVED` (automatic)
* **Annotation flow:** → `NOTED` (terminal, no transitions)
Once a transaction reaches `NOTED` or `CANCELED`, it cannot transition further. Both are terminal statuses.
## Transaction flow
***
When a transaction starts, Midaz validates:
* The accounts involved.
* The specified balances (`balanceKey`, or `default` if not given).
* Permissions (`allowSending`, `allowReceiving`).
* Sufficient available funds in the selected balance.
If validation passes and the transaction is **not** pending (Two-Phase Transaction flow), Midaz transfers the amount **immediately**. It moves the amount from the source account to the destination account, from the available balance.
This process is synchronous. On success, the transaction status becomes `APPROVED`.
Initiate this type of transaction **only** if you intend to commit it to the ledger immediately.
For transactions that need validation or approval first, use the `pending` flag to create a **Two-Phase Transaction**.
## Two-Phase Transaction
***
In this flow, Midaz creates the transaction with status `PENDING`. Midaz does not move funds right away. Instead, it reserves the amount in the correct balance (`balanceKey`, or `default` if you do not provide one).
* Midaz moves the reserved funds from `available` to `on_hold`.
* Midaz records **one** operation, typed `ON_HOLD`, on the source balance. The destination balance is untouched: no debit or credit is posted yet.
* You must explicitly `commit` to execute the transfer, or `cancel` to release the funds.
The Two-Phase Transaction feature supports [Flowker](/en/flowker/what-is-flowker). You reserve funds at the start of a workflow and run validations later. Midaz guarantees execution if the workflow approves the transaction.
In *Figure 6*, you can see an example of a two-phase transaction with anti-fraud.
### Two-Phase Transaction flow
#### 1. Create a Two-Phase Transaction
* Use the [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) endpoint with `"pending": true`.
Midaz validates accounts, the specified balances (`balanceKey`), permissions (`allowSending`, `allowReceiving`), and available funds. If valid:
* Midaz reserves funds in the correct balance.
* Midaz sets the transaction status to `PENDING`.
* Midaz stores the metadata and posts the source `ON_HOLD` operation; no debit or credit reaches the destination yet.
#### 2. Commit or cancel the pending transaction
* **Commit**: finalizes the transaction. Funds move from `on_hold` to the destination balance, and Midaz adds the `DEBIT` and `CREDIT` operations — so a committed two-phase transaction carries three operations in total (`ON_HOLD`, `DEBIT`, `CREDIT`).
* Use the [Commit a Pending Transaction](/en/reference/midaz/commit-a-pending-transaction) endpoint.
* Status: `APPROVED`.
* **Cancel**: releases the reserved funds back to `available` in the same balance.
* Use the [Cancel a Pending Transaction](/en/reference/midaz/cancel-a-pending-transaction) endpoint.
* Status: `CANCELED`.
## Past Transactions
***
Midaz also supports past transactions. Institutions can import legacy financial events and keep historical accuracy.
* Use the optional `transactionDate` field to set the original date of the transaction.
* Transactions with financial impact recalculate the historical balance state as if Midaz processed them on that date.
* Transactions created through the [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) endpoint validate structure but do **not** affect balances. They suit audits, compliance, and imports where balances must stay unchanged.
### Example
```json theme={null}
{
"description": "past transaction example",
"transactionDate": "2025-01-01T13:38:31.064Z", // optional
"send": {
"asset": "BRL",
"value": "1000",
"source": {
"from": [
{
"accountAlias": "@external/BRL",
"amount": {
"asset": "BRL",
"value": "1000"
}
}
]
},
"distribute": {
"to": [
{
"accountAlias": "@account1_BRL",
"amount": {
"asset": "BRL",
"value": "1000"
}
}
]
}
}
}
```
Submit all past transactions before you start live operations. Midaz then recalculates balances consistently across the ledger.
## Transactions with no financial impact
***
Midaz can create transactions that it records in the ledger but that do not affect account balances. These transactions keep structural integrity and leave balances unchanged.
This feature is useful when you need to:
* Import **legacy transactions** but keep balances unchanged.
* Record **audit or compliance events**.
* Add **business operations** that the ledger must track but that do not move funds.
### How does it work?
When you create a transaction without financial impact:
* Midaz stores the `balance` and `balanceAfter` fields as 0 to preserve double-entry validation.
* Each operation has a `balanceAffected` (boolean) field:
* true → the operation affects the account balance.
* false → Midaz records the operation in the ledger but does not change balances.
Even when Midaz updates no balances, it enforces double-entry rules. This keeps consistency across all transactions in the ledger.
#### Example
```json theme={null}
{
"description": "annotation example",
"transactionDate": "2025-01-01T13:38:31.064Z",
"send": {
"asset": "BRL",
"value": "1000",
"source": {
"from": [
{
"accountAlias": "@external/BRL",
"amount": {
"asset": "BRL",
"value": "1000"
},
"balanceAffected": false
}
]
},
"distribute": {
"to": [
{
"accountAlias": "@account1_BRL",
"amount": {
"asset": "BRL",
"value": "1000"
},
"balanceAffected": false
}
]
}
}
}
```
#### Related endpoint
* [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) — Record a transaction without financial impact in the ledger.
## Real-time event publishing
***
Midaz supports real-time event publishing through RabbitMQ. You can track the status of your transactions as they happen.
After you enable it, every transaction generates an event: `APPROVED`, `PENDING`, `CANCELED`, `CREATED`, or `NOTED`. External systems subscribe to these events through topic-based routing.
For more about how to publish and consume transaction events, see the [Event publisher](/en/midaz/event-publisher) page.
## Inflows, outflows, and external accounts
***
Midaz uses a double-entry ledger. All value that enters or leaves the system must pass through one special account: the **External Account**. Midaz represents this account as `@external/{{assetCode}}`. It acts as the bridge between Midaz and the external financial world (banks, PSPs, payment rails, and so on).
### Why does this matter?
When you first initialize the ledger, all accounts — including `@external` — start with a zero balance. To reflect real-world balances, such as institutional funds held outside Midaz, you must **initiate a transaction that injects funds into Midaz accounts and debits the external account**.
This is the only way to bring funds into Midaz.
### Inflows – Adding value into the Ledger
To credit an internal account from outside the ledger:
* **Source**: `@external/{{assetCode}}` (e.g., `@external/BRL`).
* **Destination**: One or more internal accounts (e.g., `@organization.main`).
**Example: First deposit into the Ledger**
Your institution holds R\$10,000 in a real-world bank and wants to bring it into Midaz.
You create a transaction:
| Source | Destination | Amount |
| :------------ | :---------- | :--------- |
| @external/BRL | @accountA | BRL 10,000 |
This debits the external account and credits your internal account. The external account now shows a negative balance. This is expected: it represents the total amount your organization brought into the ledger.
### Outflows – Moving value out of the Ledger
To move value from the ledger to an external destination:
* **Source**: One or more Midaz accounts.
* **Destination**: `@external/{{assetCode}}`.
**Example: A Pix transfer from Ledger to an external bank**
| Source | Destination | Amount |
| :-------- | :------------ | :-------- |
| @accountA | @external/BRL | BRL 1,000 |
This debits `@accountA` and credits the external account. Your system then transfers the funds to the recipient through SPI or another integration.
### Behavior and balance rules
* `@external/{{assetCode}}` can have a **zero or negative** balance, but never **positive**.
* Its balance is always the **inverse** of the combined balance of all Midaz accounts that hold that asset.
* Every inflow increases internal liquidity and reduces the external account balance (i.e., simulates a deposit).
* Every outflow does the reverse.
All value that moves between the outside world and the Midaz ledger must go through the external account.
Nothing enters or leaves the system without a formal transaction. This gives you full traceability, balance integrity, and compliance with double-entry principles.
## Setting a custom transaction date
***
The `transactionDate` field lets you set a custom date for a transaction, independent of when you submit it to the API.
* **Optional.** If you omit it, Midaz uses the current timestamp.
* **Accepted formats:**
* ISO 8601 with timezone: `2026-01-15T10:30:00Z`
* ISO 8601 without timezone: `2026-01-15T10:30:00`
* Date only: `2026-01-15`
* **Constraint:** you cannot use a future date. A future date returns error `0121`.
* **Constraint:** you cannot use it on `PENDING` transactions. A `transactionDate` with `"pending": true` returns error `0122`.
### Use cases
* Record transactions that occurred in the past (for example, same-day corrections)
* Import historical financial data into a new ledger
* Reconcile with external systems that use a different posting date
## Transaction Routes
***
The **Transaction Routes** API enables structured, validated transaction processing in Midaz.
The Lerian Console and product documentation call this concept **Accounting Routes**. The API resource and endpoints keep the `transactionRoute` / Transaction Routes name. Both refer to the same transaction-level route.
The Transactions API runs financial events: debits and credits between accounts. Transaction Routes define templates for **how** to structure and validate these events. This keeps them consistent and correct.
Think of it as the validation layer. It makes business transactions follow **predefined patterns** and keep a **proper financial structure**.
For example, a **fee**, a **deposit**, or a **payout** may need different account types, validation rules, and structures. You do not handle validation separately for each transaction. Instead, you configure predefined rules. These rules tell Midaz: "*When the user submits this type of transaction, validate it against these account requirements and structure patterns.*"
Each Transaction Route combines multiple **Operation Routes**. An Operation Route defines one component of a transaction. It sets the account requirements, the direction (source or destination), and the validation rules for each "leg" of the financial event.
Do not use the `route` field in `FromTo` inputs — use `routeId` instead. `routeId` accepts a UUID that references an Operation Route created through the Operation Routes API. Midaz keeps the `route` field for backward compatibility, but it will remove the field in a future version.
### Why does it matter?
With **Transaction Routes**, you:
* Keep a consistent transaction structure across your application.
* Make your ledger more maintainable, predictable, and reliable.
* Validate financial events against predefined patterns.
* Configure transaction templates without code changes.
* Maintain data integrity through structured validation.
## Initiating a transaction
***
When you create transactions through the API, always implement **idempotency** to prevent duplicate processing. Midaz provides built-in idempotency support through the `X-Idempotency` header. Validate the `X-Idempotency-Replayed` response header to tell new transactions from cached replays. See [Retries and idempotency](/en/reference/retries-idempotency) for details.
Use the JSON transaction API to initiate a transaction.
### JSON request shape in v2
Each transaction side uses one representation: `from` or `sources`, and independently `to` or `destinations`. Do not send both representations for the same side, and do not send an explicit `null` for an unused scalar field.
Each `sources` or `destinations` item requires `account` and exactly one value expression: `amount` or `share`. The `remaining` expression is not accepted in v2. Each array accepts at most 500 items. `share.percentage` is 1–100; `share.percentageOfPercentage` is 0–100, where `0` means no narrowing. V2 create requests have a 1 MiB body limit.
### Using the JSON endpoint
JSON endpoints provide a flexible, developer-friendly standard for data interchange. They give you precise control over request structures for custom workflows and specific use cases. They work with many programming languages, which makes integration and debugging easier.
* To create a transaction with JSON, use the [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) endpoint.
If you need to reserve funds **before** you complete the transfer, set the `pending` field to `true` (Two-Phase Transaction flow).
## Reverting a transaction
***
Midaz supports transaction reversal. You can undo an approved transaction. Midaz creates a mirror transaction that inverts the original debits and credits. This mechanism keeps full audit trails and cancels the financial impact on account balances.
Reversal creates a **new transaction** that compensates for the original. The original transaction remains in the ledger history for complete traceability.
Revert sends no idempotency key of its own, so Midaz derives one. Read the `X-Idempotency-Replayed` response header: `true` means you received a cached reverse rather than a freshly created one. Treat a replay as a signal to verify the origin's state before retrying.
### How does it work?
When you revert a transaction, Midaz automatically:
1. **Inverts operations**:
* CREDIT operations become source operations (`from`).
* DEBIT operations become destination operations (`to`).
2. **Creates a new transaction** with:
* Same amount and asset code.
* Same description and metadata.
* Inverted operations (recipients become senders, senders become recipients).
* Initial status: `CREATED` (not `PENDING`) → then progresses to `APPROVED`.
* `parentTransactionID` that references the original transaction.
3. **Processes the reversal** through the standard transaction flow: validation, balance updates, and history recording.
### Example
Consider this scenario:
**Original transaction**:
* Account A (debit -100) → Account B (credit +100)
**Reversal transaction created**:
* Account B (debit -100) → Account A (credit +100)
**Result**:
* Account A returns to its previous balance (receives back the -100).
* Account B returns to its previous balance (loses the +100).
* Both transactions remain in the ledger history for audit purposes.
* The reversal transaction includes a `parentTransactionID` that points to the original.
### Reversal restrictions
Midaz enforces strict rules to keep ledger integrity. A reversal fails in these cases:
#### 1. Transaction already has a reversal
* Midaz allows only one reversal per transaction.
* This prevents multiple reversals of the same transaction.
#### 2. Transaction is already a reversal
* You cannot revert a transaction that is itself a reversal.
* This prevents "reversals of reversals."
#### 3. Transaction status is not APPROVED
* You can revert only approved transactions.
* You cannot revert a transaction with status `PENDING`, `CREATED`, or `CANCELED`.
#### 4. Transaction cannot be reverted
* This happens when the transaction has no valid operations to invert.
* For example, a transaction without standard CREDIT or DEBIT operations.
#### 5. An operation route on the transaction is not bidirectional
* Every operation that carries a `routeId` must reference an Operation Route whose `operationType` is `bidirectional`.
* A `source` or `destination` route cannot be reverted: Midaz returns error `0150` (Route Not Bidirectional).
* Plan for this when you design routes — see [Accounting Routes](/en/midaz/transaction-routing-entities).
Midaz reverses the CREDIT and DEBIT operations. It does not reverse ON\_HOLD or RELEASE operations.
### Use cases
Transaction reversal helps in several operational scenarios:
#### 1. Incorrect payment reversal
A customer paid BRL 500 to the wrong supplier.
* Revert the transaction.
* Funds return to the customer's account.
* The customer can start a new payment to the correct supplier.
#### 2. Purchase cancellation
A store processed a BRL 1,000 sale, but the customer cancels the purchase.
* Revert the sale transaction.
* Funds return to the customer's account.
#### 3. Operational error correction
An operator created a transaction with the wrong amount.
* Revert the incorrect transaction.
* Create a new transaction with the correct amount.
#### 4. Product return
A customer purchased and paid BRL 200, but returned the product.
* Revert the payment transaction.
* The customer receives a refund.
#### 5. Integration failure compensation
A transaction is approved but fails in an external system.
* Revert to undo the accounting operation.
* Balances return to their previous state.
## Blocking and unblocking funds
***
Some scenarios require you to flag funds as **blocked** — a compliance hold, a court order, a fraud investigation — and later **release** them. Midaz supports this with two dedicated endpoints. These endpoints create transactions whose operations are typed `BLOCK` and `UNBLOCK`.
These transactions accept the **same body** as the [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) endpoint, with two key differences:
* **Always posted immediately.** Midaz ignores the `pending` field in the request body and overrides it to `false`. Block and unblock transactions are never two-phase. They go straight to `APPROVED`.
* **Operations are typed `BLOCK` or `UNBLOCK`.** This classification distinguishes them in the ledger and in operation queries. You can audit blocked-fund movements without a look at the metadata.
Midaz is **agnostic about the business reason** to block or unblock funds. Record the reason in the `metadata` field.
A Block transaction records a **ledger movement** with `BLOCK`-typed operations. This differs from the balance-level controls in [Balances](/en/midaz/balances): permission flags (`allowSending` / `allowReceiving`) and collateral balances. Those controls restrict movement but record no transaction. Use a collateral balance for a standing operational restriction. Use a Block transaction when you need an auditable ledger entry.
* Use the [Create a Block Transaction](/en/reference/midaz/create-a-block-transaction) endpoint to block funds.
* Use the [Create an Unblock Transaction](/en/reference/midaz/create-an-unblock-transaction) endpoint to release previously blocked funds.
## Managing transactions
***
You can manage your Transactions through the API or Lerian Console.
### Via API
* [Create a Transaction using JSON](/en/reference/midaz/create-a-transaction-using-json) — Submit a transaction directly using a JSON payload.
* [Commit a pending transaction](/en/reference/midaz/commit-a-pending-transaction) — Finalize a reserved transaction.
* [Cancel a pending transaction](/en/reference/midaz/cancel-a-pending-transaction) — Release reserved funds without executing.
* [Revert a Transaction](/en/reference/midaz/revert-a-transaction) — Create a reversal transaction to undo an approved transaction.
* [Create an Inflow Transaction](/en/reference/midaz/create-an-inflow-transaction) — Register incoming funds from external sources into the Ledger.
* [Create an Outflow Transaction](/en/reference/midaz/create-an-outflow-transaction) — Move funds from internal accounts to the external world.
* [Create a Block Transaction](/en/reference/midaz/create-a-block-transaction) — Flag funds as blocked with `BLOCK`-typed operations.
* [Create an Unblock Transaction](/en/reference/midaz/create-an-unblock-transaction) — Release previously blocked funds with `UNBLOCK`-typed operations.
* [List Transactions](/en/reference/midaz/list-transactions) — View all Transactions in your workspace.
* [Retrieve a Transaction](/en/reference/midaz/retrieve-a-transaction) — Get details of a specific Transaction.
* [Update a Transaction](/en/reference/midaz/update-a-transaction) — Edit the metadata of an existing Transaction.
* [Create a Transaction Annotation](/en/reference/midaz/create-a-transaction-annotation) — Record a transaction without financial impact in the ledger.
### Via Lerian Console
You can do all Transaction management actions — view, create, and cancel — through Lerian Console.
Learn more in the [Managing Transactions](/en/midaz/console/managing-transactions) guide.
# Transactions
Source: https://docs.lerian.studio/en/midaz/transactions-overview
Overview of Transactions in Midaz — balances, overdraft, events, async processing, bulk recording, and operations that keep financial value moving safely.
Transactions are where financial value moves. This section covers balances, transaction processing, events, and operations.
## Content
***
Here, you'll find:
* [**Balances**](/en/midaz/balances): The value an account holds. Balances reflect every operation over time and support multiple balance keys to segment funds within one account.
* [**Balance Overdraft**](/en/midaz/balance-overdraft): Controlled negative balances. When a balance goes negative, Midaz caps its available amount at zero and records the deficit as a debit on a companion overdraft balance.
* [**Transactions**](/en/midaz/transactions): Complete financial events built on Midaz's double-entry accounting. Every transaction pairs debits and credits, and they must balance before Midaz commits it.
* [**Transaction amount**](/en/midaz/amount): How Midaz stores financial values as precise decimals. This keeps values accurate across all asset types, from fiat currency to high-precision crypto tokens.
* [**Event publisher**](/en/midaz/event-publisher): Real-time event publishing for transaction updates. When you enable it, every transaction emits an event to a dedicated RabbitMQ exchange. External applications consume it without a tight dependency.
* [**Receiving events from Midaz**](/en/midaz/receiving-midaz-events): Consume ledger events in two ways. Bind directly to Midaz's RabbitMQ exchanges, or subscribe through the Streaming Hub.
* [**Async transaction processing**](/en/midaz/async-transaction-processing): How Midaz decouples transaction writes with RabbitMQ for higher throughput and lower latency. It covers sync and async modes, built-in resilience, and balance synchronization.
* [**Bulk Recorder**](/en/midaz/bulk-recorder): Batch transaction processing that groups multiple writes into one bulk operation. It reduces database round trips and increases throughput for high-volume workloads.
* [**Operations**](/en/midaz/operations): The smallest unit of financial activity — a single ledger entry, either a debit or a credit on a specific account. Operations form the building blocks of every transaction.
# Midaz use cases
Source: https://docs.lerian.studio/en/midaz/use-cases
Explore Midaz use cases across banking, multi-account customers, account closure, global portfolios, academic records, and supply chain traceability scenarios.
See how organizations across industries use Midaz to solve operational problems, from banking and payments to supply chain traceability and academic records.
Each use case shows what changes with Midaz. It maps the platform's entities to the domain and gives step-by-step transaction examples.
### Content
* [**Banking**](/en/midaz/midaz-in-banking): Understand how Midaz supports banking operations.
* [**Midaz for multi-account customers**](/en/midaz/midaz-for-multi-account-customers): Model multiple accounts under a single customer, with segregated balances, rules, and statements.
* [**Closing a customer account**](/en/midaz/closing-a-customer-account): How to close a customer account in line with BACEN account-closure rules.
* [**Global portfolio**](/en/midaz/midaz-in-a-global-portfolio): Configure a global account structure similar to those at major banks.
* [**Academic records**](/en/midaz/midaz-for-academic-record-management): Manage academic records for an educational institution.
* [**Supply chain**](/en/midaz/midaz-for-supply-chain-management): Manage the supply chain for a manufacturing company.
See [Plugin use cases](/en/rails/pix/pix-use-cases) for scenarios that combine Midaz with Pix or the Fees Engine.
# What is Midaz?
Source: https://docs.lerian.studio/en/midaz/what-is-midaz
Learn what Midaz is: Lerian's source-available core ledger for double-entry accounting, multi-currency operations, and audit-ready compliance reporting.
**Midaz** is Lerian's source-available core ledger for financial infrastructure. It covers account management, transaction processing, multi-currency operations, and compliance reporting. The source code is public on [GitHub](https://github.com/LerianStudio/midaz).
You configure your financial structure once — organizations, ledgers, accounts, and transaction rules. Midaz handles the rest: double-entry accounting, balance tracking, audit trails, and event-driven integrations.
## Why use Midaz
***
A legacy core means rigid schemas, vendor lock-in, and slow release cycles. Every new product or regulation requires months of integration work.
Midaz solves this:
* **Own your ledger**: Source-available means full transparency, customization, and no vendor lock-in
* **Move fast**: Modular architecture lets you add products and integrations without re-architecting
* **Stay compliant**: Immutable audit trails, double-entry enforcement, and regulatory-ready reporting out of the box
* **Scale confidently**: Handle thousands to millions of transactions per day with horizontal scaling
## What Midaz does
***
* **Central ledger**: Real-time account and transaction management with double-entry guarantees
* **Multi-currency support**: Manage traditional currencies, digital assets, and loyalty points in the same ledger
* **Accounting Routes**: Define reusable transaction templates that enforce business rules automatically
* **Automated triggers**: Respond to transactions in real time — trigger notifications, compliance workflows, or analytics feeds
* **Plugin ecosystem**: Extend with Pix, TED, and more — or build your own
### Built for every financial player
* **Fintechs**: Power digital wallets, credit platforms, and embedded finance products
* **Banks**: Modernize legacy systems incrementally without full migration risk
* **Enterprises**: Streamline payments, collections, and reconciliation across business units
## Common use cases
***
Launch checking accounts, savings products, and instant transfers. Structure your accounts with portfolios per customer, enforce business rules through transaction routes, and generate compliance reports automatically.
Manage multi-currency accounts for international customers. Each currency gets its own asset accounts while portfolios tie everything together for a unified customer view.
Offer Pix connectivity with automated fee management. Combine Midaz's ledger and Fees Engine with the Pix plugin to process instant payments, calculate fees, and maintain settlement accounts — all in real time.
Handle complex multi-party settlements where a single payment splits into seller payouts, platform fees, and tax withholdings — all processed atomically as one transaction.
Use Midaz beyond finance: track academic credits, supply chain materials, loyalty points, or carbon credits. Any domain that needs immutable, auditable tracking of units of value.
## How Midaz works
***
Create your Organization, Ledgers, Assets, and Account Types to reflect your business model.
Set up customer Portfolios and Accounts. Group them into Segments for differentiated rules and pricing.
Configure Accounting Routes and Operation Routes to enforce how money moves — including validations, fees, and double-entry compliance. (Accounting Routes are exposed as `transactionRoute` in the API.)
Process transactions through the API. Midaz handles balance updates, operation creation, and audit logging atomically.
Enable event publishing to trigger downstream systems — notifications, analytics, compliance workflows — the moment a transaction completes.
Use Reporter to generate regulatory and business reports from your ledger data, or query balances and transactions directly via the API.
## Integrations with the Lerian ecosystem
***
Midaz is a stand-alone product that can optionally integrate with other components of the Lerian ecosystem.
| Product | Integration |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [Matcher](/en/matcher/what-is-matcher) | Optional: Reconcile ledger transactions against external data sources |
| [Reporter](/en/reporter/what-is-reporter) | Optional: Generate regulatory and business reports from ledger data |
| [Flowker](/en/flowker/what-is-flowker) | Optional: Orchestrate pre-ledger validation workflows (anti-fraud, approvals) |
| [Tracer](/en/tracer/what-is-tracer) | Optional: Real-time transaction validation and spending controls |
Midaz works independently or as part of the Lerian ecosystem. Use it alone or connect it to other Lerian products as your needs evolve.
## Next steps
***
Understand Midaz's domains, entities, and how they relate to each other.
Install and configure Midaz to start building your financial infrastructure.
Explore detailed use cases for banking, global portfolios, Pix, and more.
Browse endpoints, request/response schemas, and try the API directly.
# Open by design
Source: https://docs.lerian.studio/en/open-by-design
Why Lerian builds open-by-design financial infrastructure around Midaz, our source-available core — transparency, independence, extensibility, and trust.
We believe financial infrastructure should be inspectable, extensible, and owned by those who operate it.
Open by design is not a positioning strategy for us. It is an architectural principle.
Midaz, our core ledger, is source-available under Elastic License 2.0 because financial systems require transparency, control, and long-term sustainability. Fetcher, our data ingestion service, is source-available too. The rest of the Lerian portfolio — Reporter, Matcher, Flowker, Tracer, Lender, Streaming Hub, and our plugins — is available to licensed customers, with repositories maintained internally, while preserving the same principles of transparency, auditability, and customer ownership of data and operations.
## Why Source-available at the core?
***
A source-available core creates structural advantages — not just community engagement.
By keeping Midaz source-available, we enable:
* **Transparency**: Every line of the core ledger is visible and auditable. No hidden logic. No opaque behavior.
* **Independence**: Institutions control their stack and retain access to the source code of the ledger that holds their money.
* **Extensibility**: Teams can adapt the ledger to their regulatory, operational, and product requirements.
* **Trust by design**: Visibility reduces systemic risk. You understand how your core infrastructure behaves — because you can inspect it.
## What this means for your business
***
For decision makers evaluating Lerian, an open-by-design platform translates into concrete business benefits:
* **Source-code access**: Midaz is source-available under Elastic License 2.0, so your team retains access to the ledger source code.
* **Third-party auditability**: Your security team, auditors, or regulators can inspect the Midaz code directly.
* **Extensibility**: Your engineers can inspect and extend Midaz to meet operational requirements.
## Join the community
***
Building an open core only works when it is collaborative.
Whether you are a developer, architect, or financial operator, you can:
* Contribute code
* Propose architectural improvements
* Report issues
* Build extensions and integrations
* Share implementation patterns
We welcome practical contributions — not just discussions.
[Join our Discord community →](https://discord.lerian.studio/)
# Our values
Source: https://docs.lerian.studio/en/our-values
Learn Lerian's mission and the values that guide our work — strength, courage, integrity, audacity, and leadership in financial infrastructure.
At Lerian, we combine technology and financial expertise to help institutions manage their operations through a modular platform.
We help institutions build systems they can control as their needs evolve.
## Our Mission
***
We help financial institutions adopt a modular platform that supports independent operations and evolution.
We design adaptable financial software anchored by Midaz, our source-available core under Elastic License 2.0. Institutions can inspect and extend the ledger while assembling the products and platform services they need.
## Our Commitment
***
Our commitment to scalable design ensures our solutions grow alongside the needs of our clients. We believe in building technology that adapts to your needs, not the other way around.
## Our Values
***
At the core of our work are values that inspire confidence, trust, and innovation:
### Be Your Own Strength
Pursue excellence, face challenges with resilience, and embrace growth. We believe that true strength comes from within—from the ability to adapt, learn, and overcome obstacles.
### Be Your Own Courage
Navigate uncertainty boldly and embrace risk with confidence. In a rapidly changing financial landscape, courage means taking calculated risks and pushing boundaries.
### Be Your Own Integrity
Uphold transparency, ethics, and trust in every decision. Integrity is not negotiable—it's the foundation of everything we build and every relationship we nurture.
### Be Your Own Audacity
Innovate fearlessly, push boundaries, and take initiative. Audacity drives us to challenge conventions and create solutions that others might consider impossible.
### Be Your Own Leader
Forge your own path with vision, self-development, and confidence. Leadership is about taking ownership, inspiring others, and creating lasting impact.
# Products, plugins & plans
Source: https://docs.lerian.studio/en/products-and-plugins
How Lerian's offering is structured — products, platform services, Brazil Rails, plugins, and their licensing.
Lerian is a complete, modular core banking platform. Its products, shared platform services, Brazil Rails, and plugins let you control what you adopt and when. Plugins extend a host product for specific markets or regulations — you adopt them as your needs evolve.
**Products** are standalone solutions built to deliver end-to-end capabilities without relying on another platform. Each product offers a complete feature set and can be deployed and evolved independently. [Midaz](https://github.com/LerianStudio/midaz) and [Fetcher](https://github.com/LerianStudio/fetcher) are source-available under Elastic License 2.0 (ELv2); other Lerian products are closed source and available to licensed customers.
**Plugins** are optional modules that extend the functionality of a specific product. They cannot run independently — they are built to work inside a product's architecture, leveraging its data models and infrastructure.
| | Products | Plugins |
| ---------------- | --------------------------------------------------------------------------------- | ---------------------------- |
| **Deployment** | Standalone | Requires a host product |
| **Licensing** | Midaz & Fetcher: source-available under ELv2 · Others: closed source and licensed | Premium (Enterprise) |
| **Scope** | Full platform capabilities | Specialized business logic |
| **Independence** | Runs on its own | Depends on a product context |
## Plans and what's included
***
There are two ways to adopt Lerian:
* **Midaz and Fetcher are source-available under ELv2.** You can inspect, self-host, and build on the core ledger — including its embedded CRM — and the data extraction service under the license terms.
* **The Enterprise plan** bundles everything you need to operate at scale: consulting for Midaz, the Midaz plugins (such as Pix and TED), and the **Lerian Console** — the web interface for managing it all. The Console isn't sold on its own; it comes with Enterprise.
Lerian's other products — **Reporter**, **Matcher**, **Tracer**, **Flowker**, **Lender**, and **Streaming Hub** — are standalone and licensed separately. Reporter, Matcher, Tracer, and Flowker are built to work with a ledger (Lerian's Midaz or another), not to depend on it, while Lender runs its credit journeys on the Midaz ledger; once licensed, you can also operate them from the Console as part of an Enterprise setup. Streaming Hub sits at the platform's delivery edge, fanning out platform events to your external systems through signed webhooks, queues, or a pull API.
Pricing depends on your setup and scale. [Talk to our team](https://lerian.studio/contact) to find the right plan for your use case.
## Our products
***
Lerian products are modular, composable platforms designed to cover core financial operations. They work together but can also be adopted individually.
Each product is built for production use.
[Explore our products →](/en/about-products)
Shared **[Platform services](/en/platform/platform-introduction)** provide access management, lifecycle management, observability, and deployment support across the products you adopt. **[Brazil Rails](/en/rails/what-are-brazil-rails)** connect the platform to Brazil's payment and regulatory rails.
## Our plugins
***
Plugins add business-specific capabilities to a product without modifying its core. They follow a microservices architecture, with their own versioning, deployment, and monitoring.
Today, all available plugins are part of the **Midaz ecosystem**, offering specialized capabilities for payment orchestration — Pix, TED, and the Payments module for boletos and bill payment.
[Explore our plugins →](/en/about-plugins)
## Brazil rails
***
Plugins are one of two ways the platform reaches Brazil's payment rails. Interface plugins integrate a third-party connectivity provider (JD, BTG), while **Lerian native messaging** is Lerian-owned software that connects your institution straight to BACEN and the market infrastructures under its supervision — no intermediary. Both models settle through Midaz.
[Explore Brazil Rails →](/en/rails/what-are-brazil-rails)
# Before you begin
Source: https://docs.lerian.studio/en/reference/getting-started
Review the sandbox, Try It, and licensing pages before integrating with Lerian APIs so you know how to authenticate, test, and access each product.
This section covers the tools and policies you should know before integrating with Lerian's APIs. Use the sandbox to test your requests safely, and review the licensing pages to understand usage rights and third-party attributions.
## Available pages
***
Test your integrations in a secure, isolated environment with simulated data.
Interact with our APIs directly from the documentation.
Understand how Lerian's software license works, including usage rights and conditions.
Review the third-party licenses and credits used by Lerian products.
# Lerian's License
Source: https://docs.lerian.studio/en/reference/lerians-license
Lerian licenses authorize access to paid products and plugins with organization-scoped and global license types, plus activation and validation rules.
A **license** is a key that authorizes access to Lerian features or services under specific contractual conditions, typically aligned with billing cycles (e.g., monthly, annual). This mechanism is similar to how software products like Microsoft Office or GitHub Enterprise manage access through activation keys.
In Lerian's ecosystem, licenses play a critical role in:
* Ensuring customers are in good standing before accessing paid features, such as **products**, **plugins or enterprise features**.
* Managing which features are available per client.
* Enabling Lerian to scale support and sales operations securely and efficiently.
In short, only clients with valid licenses, as defined in their contracts, can access specific software components.
A valid license must be associated with the organization making the request. Otherwise, access will be denied.
## License types
***
In Lerian’s ecosystem, licenses can have two different scopes, depending on the plugin:
### Organization-specific licenses (non-global)
Applies to a **single organization**.
Example: **Fees Engine**, **Reporter**.
You can use the license with any number of ledgers inside that organization.
### Global licenses
Valid for **multiple organizations**.
Example: **Access Manager.**
## License usage workflow
***
The steps below apply to both license types:
* **For organization-specific plugins**: provide the `organization_id` that will use the license.
* **For global plugins**: inform Lerian that you need a global license; no `organization_id` is required.
Lerian’s team follows the internal process to create and activate the license using the provided details.
Lerian sends the **License Key** (`LICENSE_KEY`) to the client.
Once you receive your license key, you must configure your `.env` file so the system knows **which license to validate** and **which organizations it applies to**.
Always configure the correct environment variables before using a plugin. Incorrect or missing values will result in license validation errors.
**Organization-specific licenses (non-global)**
These licenses apply to one organization only.
```bash Bash theme={null}
# LICENSE
LICENSE_KEY=
ORGANIZATION_IDS=,
```
* **LICENSE\_KEY**: The key provided by Lerian.
* **ORGANIZATION\_IDS**: The ID of the organization linked to the license.
**Global licenses**
These licenses have a global scope and must **always** use the value `global` in the `ORGANIZATION_IDS` variable.
```bash Bash theme={null}
# LICENSE
LICENSE_KEY=
ORGANIZATION_IDS=global
```
* **LICENSE\_KEY**: The key provided by Lerian.
* **ORGANIZATION\_IDS**: Must be set to `global` for global licenses.
## License-related errors
***
When a license is invalid or expired, API responses or plugin behaviors may return one of the specific error messages:
| Code | Title | Message |
| :------- | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LCS-0001 | Internal server error | An internal server error occurred. Please try again later or contact support if the problem persists. |
| LCS-0002 | No organization IDs configured | The application is not configured with any organization IDs. |
| LCS-0003 | No valid licenses found for any organization | No valid licenses were found for any of the configured organizations. Please check your license keys and ensure at least one organization has a valid license. |
| LCS-0010 | Organization ID header is missing | The X-Organization-ID header is missing. Please ensure the header is included in the request. |
| LCS-0011 | Organization ID header is unknown | The organization ID '12345' was not found in the list of configured organizations. Please verify you are using a valid organization ID. |
| LCS-0012 | Failed to validate organization license | License validation failed for organization ID 12345. This could be due to network issues, invalid license keys, or server-side problems. Please verify your license key is correct and that you have network connectivity to the license server. |
| LCS-0013 | Organization license is invalid | The license for organization ID '12345' is not valid and has no grace period active. Please renew your license or contact support for assistance. |
| LCS-1001 | Validation error | Could not validate license key. Please try again later or contact your system administrator. |
| LCS-1002 | License not found | There is no license for this license key. |
| LCS-1003 | License expired | The license key has expired. |
| LCS-1004 | License suspended | The license key has been suspended due to payment issues or administrative action. |
| LCS-1005 | Fingerprint mismatch | The license key or organization ID are not valid for this license. |
| LCS-1006 | Product mismatch | There is no license for this product. |
For more information about your license or to request an upgrade, please contact the Lerian commercial team.
# Licenses & credits
Source: https://docs.lerian.studio/en/reference/licenses-and-credits
Third-party open source components used by Lerian products and the licenses under which each dependency is distributed to customers.
Lerian is committed to the responsible and transparent use of third-party open source software. When our products or plugins include external components that require attribution, we list them here and comply with all licensing terms.
## Garble (BSD-3-Clause License)
***
Some Go-based plugins use [Garble](https://github.com/burrowers/garble) to obfuscate binaries during the build process.
Garble is distributed under the [BSD-3-Clause License](https://github.com/burrowers/garble?tab=BSD-3-Clause-1-ov-file#readme). Lerian acknowledges and credits the original authors of the Garble project, in compliance with license terms.
We periodically review the licenses of our dependencies to ensure full compliance. If you have any questions about our use of third-party software, please don't hesitate to contact us.
# Using our Sandbox
Source: https://docs.lerian.studio/en/reference/using-our-sandbox
An isolated Lerian environment that mirrors production behavior and powers the Try It experience inside the documentation for safe hands-on API testing.
The sandbox helps you explore Lerian’s APIs in a safe and predictable environment. It mirrors production behavior while keeping your real data untouched. Every request you send from our documentation is routed through this environment, giving you a complete view of how each endpoint behaves.
The **sandbox fully powers the Try It experience**. When you test an endpoint, the response you see comes from this environment. If you haven't seen how the Try It feature works yet, check out the [Using the Try It feature](/en/reference/using-try-it) page.
The sandbox is only available inside the documentation. You cannot access it directly through external tools.
## Base URLs
***
These are the URLs the documentation uses behind the scenes when you test an endpoint:
| Service | Sandbox Base URL |
| -------------- | --------------------------------------------------------------------------------------------------- |
| Midaz | `https://ledger.sandbox.lerian.net` |
| Access Manager | `https://auth.sandbox.lerian.net` (Auth APIs) `https://identity.sandbox.lerian.net` (Identity APIs) |
| CRM | `https://midaz-crm.sandbox.lerian.net` |
| Fees Engine | `https://fees.sandbox.lerian.net` |
| Reporter | `https://reporter.sandbox.lerian.net` |
All examples and test boxes in the docs already use these URLs.
## What you can do
***
The sandbox lets you:
* Run live requests from the documentation
* Inspect request and response structures
* Understand authentication headers and data formats
* Learn how each API behaves without setting up your own environment
Click **Try It** on any endpoint to run a live request and check how the API responds.
## Limitations
***
The sandbox is designed for exploration. Some restrictions are intentional:
* It only works inside the documentation
* Data resets frequently
* Some endpoints use fixed or sample responses
* Authentication rules may be simplified depending on the API
For full integration tests, use your own environment or reach out to our team to request a dedicated development space.
# Using the Try It feature
Source: https://docs.lerian.studio/en/reference/using-try-it
Send live requests to the Lerian sandbox directly from the documentation with real API responses in a controlled environment — no local setup required.
The **Try It** feature lets you interact with our APIs directly from the documentation. It gives you an embedded playground where you can test real requests and responses in a safe and controlled environment.
Every request sent through Try It runs against our sandbox. If you want to understand how the sandbox works under the hood, check the page [Using our Sandbox](/en/reference/using-our-sandbox).
## What you can do
***
With **Try It**, you can:
* Send live requests to our sandbox environment
* Inspect full HTTP request and response details
* Adjust headers, parameters, and body payloads
* Test different authentication tokens or organization IDs
* Explore error handling and validation scenarios with mock data
## Before you start
***
You will need the required headers for each API. Examples include `Authorization`, `X-Lerian-Id`, or `X-Organization-Id`. Each endpoint specifies exactly which headers you must use.
The sandbox resets automatically every day.
Any entity you create, such as Organizations, Ledgers, or Portfolios, will be deleted within 24 hours.
When testing dependent endpoints, create the required entities again before sending new requests.
For example, you must create an Organization before creating a Ledger. Use the returned `organization_id` in the path.
## How to use it
***
The **Try It** feature is available on every endpoint page in the **API Reference**.
If the Try It button is missing, reload the page or check that you are browsing the API Reference section.
Open any endpoint in the **API Reference**.
Click the **Try It** button next to the endpoint path.
Fill in the input fields such as query parameters, body content, or headers.
Click **Send Request** to run the call.
Review the response body, status code, and headers shown below the request.
You can copy your request as `curl` or `fetch` using the code tabs. It is a simple way to move from testing in the docs to running calls in your local environment.
# Our release cycle
Source: https://docs.lerian.studio/en/updates-and-versioning/release-cycle
Understand how Lerian plans, builds, tests, and releases — from continuous weekly delivery to production releases — so teams know what to expect.
Lerian plans and reviews work on a **weekly rhythm**, delivering improvements continuously as release candidates. **Production releases** follow a separate, more deliberate cadence to keep environments stable.
We're currently refining this cadence, so production timing may be adjusted as the process evolves.
Throughout this work, we focus on **evolving all our products**, continuously improving existing capabilities and, when applicable, **developing new products**. Every change is carefully validated before reaching production.
## How it works
***
* **Planning**: We align scope, priorities, and technical direction. This is where requirements are defined and the roadmap takes shape.
* **Development**: The team builds and iterates on new features, enhancements, and platform improvements based on the planned scope.
* **Testing and validation**: Changes go through thorough testing and validation to ensure reliability, performance, and consistency across the platform.
* **Release**: Changes are consolidated into a production release, with documentation and release notes published alongside the rollout.
# 2025 release schedule
Source: https://docs.lerian.studio/en/updates-and-versioning/release-cycle-2025
Track Lerian's 2025 release cycles with the planned start and end dates for each stage so you can align your delivery and rollouts.
In 2025, our release cycles were the following:
## First cycle
***
| Stage | Start Date | End Date |
| :------------ | :--------- | :------- |
| **Weeks 1–6** | April 7 | May 16 |
| **Weeks 7–8** | May 19 | May 30 |
## Second cycle
***
| Stage | Start Date | End Date |
| :------------ | :--------- | :------- |
| **Weeks 1–6** | June 2 | July 11 |
| **Weeks 7–8** | July 14 | July 25 |
## Third cycle
***
| Stage | Start Date | End Date |
| :------------ | :---------- | :----------- |
| **Weeks 1–6** | July 28 | September 5 |
| **Weeks 7–8** | September 8 | September 19 |
## Fourth cycle
***
| Stage | Start Date | End Date |
| :------------ | :----------- | :---------- |
| **Weeks 1–6** | September 22 | October 31 |
| **Weeks 7–8** | November 3 | November 14 |
## Fifth cycle
***
| Stage | Start Date | End Date |
| :------------ | :---------- | :-------------- |
| **Weeks 1–6** | November 17 | December 26 |
| **Weeks 7–8** | December 29 | January 9, 2026 |
# 2026 release schedule
Source: https://docs.lerian.studio/en/updates-and-versioning/release-cycle-2026
Lerian's 2026 release cadence is being revised — the updated schedule, cycle stages, and target dates will be published here once the new structure is defined.
We're revising our release cadence for 2026. As soon as the new structure is defined, we'll publish the updated schedule here.
In the meantime, we announce each release as it happens in our [News](/en/news/news).
# Our release schedule
Source: https://docs.lerian.studio/en/updates-and-versioning/release-schedule
Track Lerian's release schedules organized by year — planned cycles, feature freeze windows, and the start and end dates of every stage across products.
Here you can find the detailed schedules for each Lerian release cycle, organized by year. Each schedule lists the planned cycles with the start and end dates for each stage.
To understand how our release cycle works, see the [Our release cycle](/en/updates-and-versioning/release-cycle) page.
Planned cycles for 2026, including start and end dates for each stage.
Cycles completed in 2025 with dates for each stage.
# Version governance
Source: https://docs.lerian.studio/en/updates-and-versioning/versioning-governance
Understand Lerian's versioning governance — how changes are communicated, how dependencies stay compatible, and our backward-compatibility promise.
Clear governance keeps the versioning process transparent and reliable. Here we detail how changes are communicated, how dependencies are managed, and our long-term commitment to backward compatibility.
This gives you full visibility into how Midaz evolves and how we protect your stability along the way.
## Communication of changes
***
We communicate every change clearly so teams can prepare and adapt with confidence.
* **Release notes** include new features, improvements, bug fixes, known issues, and breaking changes with migration paths.
* **Documentation updates** reflect API changes, new configurations, and updated behaviors.
* **Pre-release announcements** are shared in advance for major versions.
## Versioning of dependencies
***
Dependencies are versioned with the same principles to ensure stability and compatibility.
* **Internal dependencies** follow the same versioning scheme as Midaz.
* **External dependencies** are managed to stay compatible throughout a major version lifecycle.
## Backward compatibility commitment
***
This versioning policy applies to every new release, ensuring stability across the platform. We commit to maintaining the defined **support windows** for all future versions, so teams can adopt upgrades with confidence and without unexpected disruptions.
By preserving backward compatibility, we reduce the risk of breaking changes and give organizations the flexibility to plan upgrades on their own schedule. This approach keeps integrations stable while allowing Midaz to evolve at a sustainable pace.
For details about timelines and schedules, see the [Release cycle](/en/updates-and-versioning/release-cycle) page.
# Versioning process
Source: https://docs.lerian.studio/en/updates-and-versioning/versioning-process
Understand how versioning works in Lerian — what to expect between releases, how semantic versioning is automated, and how to plan upgrades across majors.
Versioning is not just a naming convention; it’s an automated process that ensures consistency across releases. This section explains how automation drives version generation, how support timelines are defined, and what to expect when moving between versions.
## Versioning automation
***
To ensure an automated and predictable versioning process, we follow **semantic versioning** principles.
### How it works
***
Identifies commits made since the last version.
Determines the new version based on predefined rules.
Automatically generates and publishes the new version.
## Version support timeline
***
Midaz follows a clear support policy that defines what you can expect from each version throughout its lifecycle.
* **Active support (current major version):** includes feature enhancements, bug fixes, and security patches.
* **Security support (post-active support):** limited to critical security patches only.
Older versions do not receive further updates or support.
## Upgrade path recommendations
***
Upgrading Midaz versions follows clear recommendations to keep the process predictable and safe.
* **Minor version upgrades** (`1.0.0 → 1.1.0`) are simple and require little effort.
* **Major version upgrades** (`1.2.0 → 2.0.0`) may involve planning and testing.
* Every major version comes with a clear **upgrade guide** to support the transition.
# Versioning scheme
Source: https://docs.lerian.studio/en/updates-and-versioning/versioning-scheme
Learn how Lerian applies semantic versioning, how we manage breaking changes, and how to interpret release candidate designations used for pre-release builds.
The versioning scheme describes how we assign and increment version numbers. It clarifies the meaning of major, minor, and patch releases, how we handle breaking changes, and the role of pre-release tags.
By following this structure, you can anticipate the impact of each update and plan migrations confidently.
## Semantic versioning
***
We follow a **modified semantic versioning** scheme in the format **X.Y.Z \[-designation]**, where:
| Version type | Increment frequency | Characteristics | Example |
| --------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------- |
| **X (Major Version)** | Evaluated every two development cycles, but **only released if there are significant changes**. | **May introduce breaking changes**.
Introduces new features and functionality. | *1.0.0 → 1.1.0* |
| **Z (Patch Version)** | As needed, outside the regular cycle | **For hotfixes, security patches, and critical updates**.
Maintains backward compatibility. | *1.1.0 → 1.1.1* |
## Breaking changes
***
Breaking changes are part of the natural evolution of the platform. They happen when an update alters or removes behavior in a way that is not fully compatible with previous versions.
To keep this process predictable, we follow strict rules and communicate early, allowing your teams to prepare with confidence.
* Breaking changes are introduced **only in a major version**.
* They are **announced in advance**, always with clear migration guides and practical examples.
* Deprecated features **emit warnings** before removal, giving teams time to adjust without sudden impact.
* We aim to **minimize disruption** by grouping breaking changes together and providing alternative solutions whenever possible.
### Examples
* **Field replacement**: The free-text `route` field in transaction payloads was superseded by `routeId` (UUID); `route` remains accepted for backward compatibility but no longer drives validation.
* **Validation rules**: The `ACCOUNT_TYPE_VALIDATION` and `TRANSACTION_ROUTE_VALIDATION` environment variables were replaced by the Ledger Settings API, which controls accounting validation per ledger without redeployment.
* **Deprecation cycle**: The `scale` field was removed from transaction amounts in v3, when amount handling moved to a numeric system.
## Pre-release designations
***
We use the following pre-release designations **when applicable**:
| Designation | Description | Characteristics | Example |
| :----------- | :------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------- | :-------------- |
| **-alpha.N** | Early development builds. | **Potentially unstable, not for production use**.
May contain incomplete features. | *1.1.0-alpha.1* |
| **-beta.N** | Feature-complete builds undergoing testing. | **Suitable for testing environments.**
Feature-frozen, focusing on stability and bug fixes. | *1.1.0-beta.1* |
| **-rc.N** | Release Candidates. | **Expected to be stable and production-ready**.
Only critical bug fixes applied before the final release. | *1.1.0-rc.1* |
### Examples
**Potential releases**
* *1.0.0* → Initial stable release
* *1.0.1* → Patch with bug fixes
* *1.1.0-alpha.1* → Alpha for next minor
* *1.1.0-beta.1* → Beta for next minor
* *1.1.0-rc.1* → Release candidate
* *1.1.0* → Stable minor release
* *1.2.0* → Next minor release
* *2.0.0* → New major version
# Our versioning strategy
Source: https://docs.lerian.studio/en/updates-and-versioning/versions
Understand Lerian's versioning strategy — increments, pre-release tags, automation, upgrade paths, and how changes are communicated.
Our versioning strategy defines how versions are incremented, how pre-release tags are applied, and how automation keeps everything consistent across the system. Each release follows a [clear cycle](/en/updates-and-versioning/release-cycle) that helps teams plan ahead and adopt updates with confidence.
Alongside the technical rules, we provide guidance on upgrade paths, outline our support policy, and communicate changes in a transparent way. This ensures that moving between versions is safe, predictable, and aligned with your operational needs.
# Welcome to our guides
Source: https://docs.lerian.studio/en/welcome-to-our-guides
Navigate Lerian's product guides — practical walkthroughs that turn complex financial infrastructure into real-world solutions across Midaz and plugins.
At Lerian, **Guides are more than walkthroughs. They're your go-to resource for turning complex infrastructure into real-world solutions**.
## How to navigate
***
Each guide is structured to help you integrate confidently and move fast. To access a product’s guide:
You’re already in the **Guides** section of our documentation.
Open the dropdown menu in the left sidebar and select the product you’re working with.
You’ll land in a dedicated space with product-specific content, including related plugins and real-world examples.
# Who we are
Source: https://docs.lerian.studio/en/who-we-are
Learn about Lerian's mission, identity, and the principles that guide our approach to modern, open financial infrastructure and core banking software.
At Lerian, we build financial infrastructure that gives institutions control. We combine deep financial expertise with modern, open technology to help organizations operate independently, scale confidently, and adapt without friction.
We don’t believe in black boxes. We believe in clarity, ownership, and long-term autonomy.
## What we do
***
We help financial institutions design and operate modern core banking ecosystems using open technologies. By providing a programmable, modular foundation, we enable teams to move faster without compromising control over their operations or their architecture.
Our platform is built for institutions that demand more than off-the-shelf solutions:
* **Digital banks** launching new financial products
* **Traditional banks** modernizing legacy cores incrementally
* **Fintechs** building embedded finance and payment platforms
* **Enterprises** streamlining payments, collections, and reconciliation across business units
## Our principles
***
Our approach to building financial software is guided by three core principles that prioritize the long-term success of our clients:
#### Independence over vendor lock-in
We build systems that you control. By using standard interfaces and open formats, we ensure that your roadmap is never dictated by a single vendor's priorities.
#### Transparency over opacity
Financial systems require absolute clarity. We design our infrastructure to be inspectable and auditable, eliminating hidden logic and opaque behaviors that introduce systemic risk.
#### Architecture over shortcuts
We prioritize robust, scalable design over quick fixes. Our solutions are built to handle real-world complexity, ensuring that your infrastructure grows alongside your business needs without requiring constant re-platforming.
## Explore further
***
Understand the problems we solve and how our platform helps financial institutions regain control.
Discover the core values and commitments that drive our team and shape our culture.
## Get in touch
***
Have questions? Exploring a partnership? Evaluating architecture? Let’s talk.
Reach out to our team to discuss how Lerian can support your infrastructure needs.
# Why Lerian
Source: https://docs.lerian.studio/en/why-lerian
Understand how Lerian helps financial institutions modernize core banking without vendor lock-in — open ledgers, modular plugins, and flexible deployment.
Modern financial institutions face a common set of challenges: legacy systems that cost too much to maintain, adapt too slowly, and lock you into vendors who dictate the roadmap.
Lerian gives you back control.
**New to core banking?** If terms like *Ledger*, *debit and credit*, or *chart of accounts* aren't yet second nature, start with the fundamentals first — they'll make everything here click into place.
**[Begin with Core Banking Fundamentals](/en/fundamentals/core-banking/what-is-core-banking)**
## The problems we solve
***
| Challenge | How Lerian helps |
| :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Vendor lock-in** | Midaz, our core ledger, is source-available, so you own the foundation of your infrastructure. No single vendor controls your roadmap |
| **Slow time-to-market** | Modular platform lets you launch new products in weeks, not months |
| **Compliance burden** | Built-in audit trails, regulatory reporting, and immutable records reduce manual compliance work |
| **Reconciliation errors** | Automated reconciliation replaces manual spreadsheet matching, catching discrepancies in real time |
| **Operational cost** | Replace multiple disconnected systems with one composable platform |
| **Scaling limitations** | Handle millions of transactions per day without re-platforming |
## What the platform includes
***
| Business need | Lerian product | What it does |
| :----------------------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------- |
| Central financial record-keeping | **Midaz** | Manages accounts, balances, and transactions with full audit trails |
| Regulatory and business reporting | **Reporter** | Generates compliance reports (BACEN, SOX) and business analytics automatically |
| Transaction reconciliation | **Matcher** | Compares internal records against bank statements and payment processors — flags discrepancies |
| Real-time transaction controls | **Tracer** | Validates transactions against spending limits and business rules before they execute |
| Workflow automation | **Flowker** | Automates approval flows, anti-fraud checks, and payment orchestration |
| Data ingestion from external systems | **Fetcher** | Connects to external databases and extracts data on schedule, feeding Reporter, Matcher, and your own services |
| Credit products and loan management | **Lender** | Defines loan products and manages the loan lifecycle — applications, disbursement, and interest accrual |
| Event delivery to your systems | **Streaming Hub** | Fans platform events out to your systems through signed webhooks, queues, or a pull API |
## Deployment flexibility
***
You choose how to run the platform:
* **Fully managed (SaaS)**: Lerian handles everything. Your team focuses on configuration and business logic. Fastest path to production.
* **Your infrastructure (BYOC)**: Deploy in your own cloud or on-premises. Full control over data residency, security, and compliance boundaries.
Both options support the full product portfolio. Choose based on your regulatory requirements and operational maturity.
[Compare deployment models in detail →](/en/deployment-models)
## Built for trust and independence
***
* **Source-available core**: Midaz, our ledger, is source-available — inspect, audit, and extend the core that holds your money. No black boxes at the foundation
* **No vendor lock-in**: Standard interfaces and open formats across the platform. You can always move
* **Compliance-ready**: Immutable audit trails, double-entry enforcement, and regulatory reporting ship built in — not bolted on
* **Incremental adoption**: Start with one product and expand as needed. No big-bang migration required
## Who uses Lerian
***
* **Digital banks** launching new financial products
* **Traditional banks** modernizing legacy cores incrementally
* **Fintechs** building embedded finance and payment platforms
* **Enterprises** streamlining payments, collections, and reconciliation across business units
## Next steps
***
Understand the four pillars that form our architectural philosophy.
Choose your path — evaluate the platform or start building.
Understand the differences between fully managed and self-hosted deployment.
Have questions? Exploring a partnership? Let's talk.
# About Midaz Console
Source: https://docs.lerian.studio/en/midaz/about-midaz-console
See how the standalone Midaz Console moved into the Lerian Console Midaz Module in v3.5.0+, and what maintenance mode means for older source-available installs.
The **Midaz Console** was the visual administrative interface for Midaz. You used it to configure organizations, ledgers, accounts, assets, and transactions, with no direct API calls.
From Midaz v3.5.0, the Midaz source-available repository no longer includes the Console. Its features moved into the **Lerian Console** as the [Midaz Module](/en/midaz/console/midaz-module) — a dedicated product module in Lerian's unified platform.
## What changed
***
The Midaz Console evolved from a standalone source-available interface into an integrated module in the Lerian Console. This change separates Midaz, the core ledger engine, from the platform tools that manage it.
| Aspect | Before (prior to v3.5.0) | Now (v3.5.0+) |
| :--------------- | :-------------------------------- | :-------------------------------------------------------- |
| **Distribution** | Bundled with the Midaz repository | Part of the Lerian Console |
| **License** | Source-available | Commercial (Lerian Console) |
| **Scope** | Standalone admin interface | Integrated product module with cross-product capabilities |
| **Development** | Community-driven | Maintained and developed by Lerian |
Midaz itself remains fully source available. Only the Console — the visual management interface — moved to the Lerian Console platform.
## For existing users
***
If you run a Midaz version before v3.5.0, the Console remains part of your source-available installation. It works as expected in those versions.
The standalone Midaz Console is in **maintenance mode**. It receives critical security patches only, with no new features or improvements. To keep active development and support, migrate to the Lerian Console.
## Moving forward
***
The [Midaz Module](/en/midaz/console/midaz-module) in the Lerian Console provides everything the standalone Console offered. It adds unified authentication, cross-product navigation, and ongoing feature development.
To see what the Midaz Module offers, read the documentation:
Full documentation for the Midaz Module within the Lerian Console.
Learn about the unified platform for managing all Lerian products.
# Auditability
Source: https://docs.lerian.studio/en/midaz/auditability
Understand the scoped audit mechanisms in Midaz: CRM protection events and optional Ledger transaction audit messages.
Midaz does not provide one universal audit trail for every platform action. Its audit behavior is scoped to the mechanism that produces the data.
## CRM protection audit events
***
When CRM runs with envelope encryption, Midaz emits **Protection Audit Events** for encryption-protection activity. These events are persisted per Organization and can be read through the protection-audit endpoint.
This mechanism is not available in legacy encryption mode. The audit writer is best-effort: a failed audit emission is logged, but it does not block the CRM operation.
Use protection audit events to investigate encryption-protection activity for an Organization. Do not interpret them as a complete record of every Ledger or platform action.
## Ledger transaction audit messages
***
The Ledger publishes transaction audit messages unless `AUDIT_LOG_ENABLED` is explicitly set to `false`. The built-in environment example sets this flag to `false`. Keep the flag set to `false` until your downstream consumer, its retention, and its investigation workflows are configured.
These messages are sent to the configured audit queue for downstream storage and processing. Midaz does not expose them as a general queryable Ledger audit-record API; the receiving system owns retention, indexing, and investigation workflows.
## Designing your audit trail
***
Choose the mechanism that matches the data you need to observe:
* Use **Protection Audit Events** for CRM envelope-encryption activity.
* Enable **Ledger transaction audit messages** only when your downstream consumer is configured to retain and process them.
* Keep controls, retention, and evidence requirements in the system responsible for the data after Midaz emits it.
For help designing an audit and retention architecture around Midaz, [contact us](https://lerian.studio/contact).
# Community support
Source: https://docs.lerian.studio/en/midaz/community-support
Connect with the Midaz community on GitHub Discussions and Discord to ask questions and share feedback, or use GitHub Issues to report bugs.
Midaz is a source-available, mission-critical service, and a growing community of builders powers its evolution. If you're using the Community version, this page outlines the channels where you can ask questions, report bugs, and share ideas.
If you're an **Enterprise** customer, use the [**Lerian Support Portal**](/en/midaz/lerian-support-portal) to get guaranteed response times and access to our dedicated support team.
## GitHub Discussions
***
Use the [Discussions tab](https://github.com/LerianStudio/midaz/discussions) in our public repository to:
* Ask questions or request help.
* Propose enhancements.
* Share use cases or feedback.
* Participate in existing threads.
We monitor this space regularly and encourage open conversation. Don’t be shy — your question might help others too.
## Report a bug
***
Use [GitHub Issues](https://github.com/LerianStudio/midaz/issues) to report a bug. Include the behavior you observed, the expected result, and enough context for maintainers to reproduce it.
## Join our Discord
***
For real-time conversations, join the [Lerian Discord community](https://discord.gg/DnhqKwkGv3). This is the place for:
* Quick questions and peer support.
* Staying in the loop with the latest updates.
* Talking directly with maintainers and other users.
Look for the **#midaz** channel to dive right in.
## Contribute
***
If you’re ready to go a step further:
* Check out issues labeled `good first issue` or `help wanted`.
* Fork the repo, make your changes, and submit a PR.
* Share your own plugin ideas or integrations.
All contributions are welcome, whether you're fixing a typo or designing a new feature.
New to source available? Start by reading our [contribution guidelines](https://github.com/LerianStudio/midaz/blob/main/CONTRIBUTING.md). We keep them beginner-friendly.
## Not sure where to start?
***
You don’t need permission to get involved. Ask. Suggest. Explore.
Midaz gets better with every question and every idea shared. Let’s build something amazing, together.
# Setting up accounting in the Console
Source: https://docs.lerian.studio/en/midaz/console/accounting-setup-in-console
Walk through a Console-only guide to build your accounting model in Midaz — from planning a chart of accounts to running your first Pix payment.
This guide shows you how to set up accounting in the Midaz Module of the Lerian Console. You use only the screens and forms in the Console — no code. It is the Console companion to the developer-focused [Accounting Walkthrough](/en/midaz/accounting-walkthrough). You reach the same result, but through **what you click and configure**, not what you send to an API.
It targets product managers, implementation teams, and developers who prefer to model their accounting in the UI. We keep technical terms where they matter. We always explain what they mean when you see them on screen.
You will not see any JSON or API calls here. You do everything below through Console forms. To automate the same setup later, follow the technical reference each section links to.
## What you are building
***
Accounting in Midaz uses **double-entry bookkeeping**. You do not do the bookkeeping by hand — the Console and the ledger enforce it for you. Keep one rule in mind:
> Every transaction moves value **from** one place **to** another. One side gives (debit), one side receives (credit), and the two always balance.
You model your accounts and routes once. From then on, the ledger balances every transaction automatically and keeps it auditable.
The setup follows a logical order. Each layer builds on the one before it:
Decide which balances your product needs (customer funds, fees, settlement, treasury, revenue).
Define the categories that classify your accounts.
Open the actual balance containers, each with a **Type** and an Asset. You select the Type for non-external Accounts; enabling **External account** sets it to `external` automatically. When **Validate Account Type** is enabled, the Type of every non-external Account must match a registered Account Type.
Set the rules for which accounts can join each transaction and how the ledger posts entries.
Work top to bottom. Routes are much easier to build once you already know which accounts represent customers, treasury, fees, and settlement.
## Step 1 — Plan your chart of accounts
***
In traditional accounting, a **Chart of Accounts (CoA)** is the master list of every account category your business uses: assets, liabilities, revenue, and expenses. It also defines how you classify each movement against those categories.
In Midaz there is **no single "Chart of Accounts" screen to fill in**. Instead, your CoA emerges from the building blocks you create in the Console: Assets, Account Types, and Accounts. You plan it up front, mostly as a paper (or whiteboard) exercise.
Before you open the Console, list the balances your product needs. For a typical payments product that might be:
| Balance you need | What it represents |
| ---------------- | --------------------------------- |
| Customer funds | Money your end users hold |
| Settlement | Funds waiting to clear |
| Fee revenue | Fees you collect |
| Fee expense | Fees you pay to providers |
| Treasury | Your own internal operating funds |
This list is your blueprint. The next steps turn each line into something concrete in the Console.
Before any account can exist, it needs an **Asset** — the unit of value it holds (for example `BRL`). If you have not created your assets yet, start with [Creating an Asset](/en/midaz/console/creating-an-asset).
## Step 2 — Create your Account Types
***
**Account Types** are the categories that classify your accounts. Think of them as labels like `customer`, `treasury`, or `fee` that group accounts by their role. Later, Accounting Routes use these labels to decide which accounts a transaction can use.
In the Console you create one Account Type per category from your blueprint — not one per individual customer.
Open the New Account Type form and define a category with a clear name and a stable key value.
A typical payments setup uses these Account Types:
| Account Type | Use it for |
| ------------ | -------------------------- |
| `customer` | Liquid customer balances |
| `settlement` | Funds awaiting clearing |
| `fee` | Fees collected as revenue |
| `treasury` | Internal operations |
| `expense` | Fees paid out to providers |
The **Key Value** of an Account Type (for example `customer`) is what routes and accounts rely on. Keep it short, lowercase, and stable — changing it later means recreating the accounts and routes that depend on it.
The Account Types menu appears only after you enable **Validate Account Type** in your Ledger settings. To turn it on, see [Managing Ledgers](/en/midaz/console/managing-ledgers-via-console#ledger-settings).
## Step 3 — Create your Accounts
***
**Accounts** are the balance containers — the things that hold value and that money moves between. Each account has a **Type** and one Asset (its currency). You select the Type for non-external Accounts; enabling **External account** sets it to `external` automatically. When **Validate Account Type** is enabled, the Type of a non-external Account must match a registered Account Type (external Accounts skip this check). A human-readable **alias** identifies it; `@customer_123_brl` is one common convention.
For each line in your blueprint, create one Account in the Console.
Open the New Account form, choose its Type and Asset, and give it a clear alias.
When you fill in the form, a few choices are permanent and worth getting right the first time:
| Field | Why it matters |
| ----------------- | ------------------------------------------------------------------------------------------- |
| **Account Alias** | The name routes and transactions use to find the account. Cannot be changed after creation. |
| **Type** | The Account Type that classifies it. Cannot be changed after creation. |
| **Asset** | The currency or unit it holds. Cannot be changed after creation. |
The Console **locks Alias, Type, and Asset when you save the account**. To change any of them, create a new account. Double-check before you save.
### Understanding what a balance actually shows
When you open an account in the Console, its balance is not a single number. Midaz splits every balance into two **buckets**. You always know what you can spend and what the ledger holds back. In plain language:
| Bucket | What it means when you look at an account |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Available** | Money that is free to spend or send **right now**. This is the number that goes up and down with normal payments. |
| **On hold** | Money that a pending operation has **reserved** but not yet finalized. It still belongs to the account. The ledger sets it aside, and you cannot spend it until the hold is confirmed or cancelled. |
Both amounts appear as exact decimal values (for example, `12.50`). There is no scale factor to apply when you read them.
**On hold** powers two-step payments. When you authorize a payment but do not yet capture it, the amount moves from **Available** to **On hold**. Confirming the payment releases it to the destination. Cancelling returns it to Available. You see each of these movements on the account at every stage.
## Step 4 — Create your Accounting Routes
***
With **Validate Routes** enabled, each transaction must specify a valid Accounting Route in the ledger. An Accounting Route is a reusable rule for one kind of transaction, such as a *Pix transfer* or a *fee charge*. It answers three questions:
* Which accounts can send on the **source** side?
* Which accounts can receive on the **destination** side?
* Which debit and credit **entries** should the ledger post when it runs?
The Console builds these through a guided 3-step wizard, so you do not have to assemble anything by hand.
See how the Accounting Routes page works and what each part of the wizard does.
Walk through the 3-step wizard to define a route, its operation rules, and its entries.
To decide how to shape a route, see [Accounting rules](/en/midaz/console/mc-accounting) — it explains the choices in plain terms. The essentials:
Each rule inside a route applies to one side of a transaction:
* **Source** — the sending side (where value comes from).
* **Destination** — the receiving side (where value lands).
* **Bidirectional** — the same rule applies to both sides, for cases where one kind of account can both send and receive.
A valid route needs at least one Source **and** one Destination, or a single Bidirectional rule.
Each rule checks accounts in one of two ways:
* **Account Type** — the rule accepts any account of a given category (for example, any `customer` account can send). Use this for flexible, scalable flows.
* **@Alias** — the rule accepts only one exact account (for example, only `@fee_revenue` can receive). Use this for fixed operational accounts like treasury, fees, or settlement.
Scenarios decide how the debit and credit entries get recorded:
* **Direct** — a one-step movement, posted immediately.
* **Two-Step** — a hold-then-commit flow. It uses separate entries to reserve, confirm, and cancel funds, and it drives the **On hold** bucket.
* **Reversal** — entries the ledger records to undo a completed transaction.
* **Overdraft** — entries the ledger records when a debit exceeds the account's available funds and draws on an overdraft line.
* **Block** — entries the ledger records when funds on an account are blocked or unblocked.
Turn on **Validate Routes** in Ledger settings **only after** the routes you need already exist. If validation is on but a matching route is missing, those transactions will fail.
## Putting it together — a simple Pix payment
***
Let's run the whole flow in the Console for a basic **Pix cash-out**: a customer sends BRL out of their wallet to a settlement account. Assume your `BRL` asset already exists.
From the **Account Types** page, create:
* `customer` — for end-user balances.
* `settlement` — for funds leaving to the outside world.
See [Creating an Account Type](/en/midaz/console/creating-an-account-type).
From the **Accounts** page, create:
* `@customer_123_brl` — Type `customer`, Asset `BRL`. The customer's wallet.
* `@external_brl` — Type `settlement`, Asset `BRL`. Where funds settle when they leave the ledger.
See [Creating an Account](/en/midaz/console/creating-an-account).
`@external_brl` is a regular, ledger-owned settlement Account. This example uses it so the route can validate the `settlement` Account Type. It is not the canonical external Account `@external/BRL`, which Midaz creates automatically together with the `BRL` Asset; the `@external/` alias prefix is reserved, so you cannot create that Account yourself. For money that actually enters or leaves Midaz, use `@external/BRL`. See [Common mistakes to avoid](/en/midaz/common-mistakes-to-avoid).
From the **Accounting Routes** page, start the wizard and build a `Pix cash-out` route:
* A **Source** operation rule validating Account Type `customer` (the wallet sends).
* A **Destination** operation rule validating Account Type `settlement` (the settlement account receives).
* A **Direct** accounting scenario, with a debit entry on the source and a credit entry on the destination.
See [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route).
Create a transaction that moves, say, `100.00 BRL` from `@customer_123_brl` to `@external_brl` using your `Pix cash-out` route. See [Creating a Transaction](/en/midaz/console/creating-a-transaction).
Open each account and look at the balance:
* `@customer_123_brl` — **Available** drops by `100.00`.
* `@external_brl` — **Available** rises by `100.00`.
Both movements share the same transaction, giving you a clean, balanced audit trail.
Need an authorize-then-capture flow instead of an instant one? Use a **Two-Step** scenario on the route. You will then watch the amount move into **On hold** when reserved, and out of it when you confirm or cancel.
## What to do next
***
You now have a working accounting model, built entirely in the Console. To go deeper, use these technical references. They cover how to automate the setup, understand the entities, and turn ledger activity into reports:
The end-to-end developer version of this guide, including the data model and double-entry detail.
How the core accounting primitives relate to one another.
The technical model behind Accounting Routes, operation routes, and entries.
The full balance model behind available and on-hold amounts.
# Adding a Portfolio to an Account
Source: https://docs.lerian.studio/en/midaz/console/adding-portfolio-to-account
Link a Midaz Account to a Portfolio from the Console — either from the Accounts list or the edit form — to group Accounts under one customer or wallet.
Use Portfolios to group Accounts that belong to the same Entity. To add an Account to a Portfolio, follow one of these procedures:
## Adding from the Accounts page
If the Account does not belong to a Portfolio yet, add it directly from the **Accounts** page. Follow these steps:
Click the **Link** option in the **Portfolio** column of the Account you want to add.
The Console displays the form to edit the Account.
Switch to the **Portfolio** tab and select the **Portfolio** you want.
Click the **Save** button to link the Account to the selected Portfolio.
## Add by editing the Account
You can also edit the **Account** to add or change its Portfolio:
On the **Accounts** page, find the Account you want to edit. Click the three dots () in the **Actions** column. Select **Details** to open the Account details page.
Click the **Edit** button. In the edit form, select the **Portfolio** you want.
Click the **Save** button to confirm the procedure.
[Update an Account endpoint](/en/reference/midaz/update-an-account)
# Adding a Segment to an Account
Source: https://docs.lerian.studio/en/midaz/console/adding-segment-to-account
Assign a Segment to an existing Account from the Console to classify it by shared characteristics and apply the associated benefits or business rules.
Segments group Accounts by shared characteristics and benefits. To learn more about Segments, see the [Segments](/en/midaz/segments) page.
To add a Segment to an Account, follow these steps:
Open the **Accounts** page. Find the Account you want to add to the Segment. In the **Actions** column, click the three dots (). Select **Details**.
Select the **Account Details** tab. Select the Segment from the drop-down list.
Click the **Save** button to confirm.
[Update an Account endpoint](/en/reference/midaz/update-an-account)
# Checking Transaction details
Source: https://docs.lerian.studio/en/midaz/console/checking-transaction-details
Open the full details of a Transaction in the Console to audit its movements, review the Transaction Data tab, and edit its description when needed.
To check the details of a transaction, follow these steps:
On the **Transactions** page, find the transaction to check. In its **Actions** column, click the three dots (). Select **See details**.
A page opens with the transaction summary.
Then select the **Transaction Data** tab to see more information.
In the **Transaction Data** tab, edit the description if needed.
# Creating a Ledger
Source: https://docs.lerian.studio/en/midaz/console/creating-a-new-ledger
Create a new Ledger in the Console to isolate Assets, Accounts, Routes, and Transactions, and configure Account Type or Route validation while you set it up.
A Ledger isolates Assets, Accounts, Routes, and Transactions in one accounting workspace. Create a new Ledger when you need a separate balance domain, product line, environment, or accounting model.
## Before you start
***
You need an existing Organization. If this is the first setup, create the Organization first in [Creating an Organization](/en/midaz/console/creating-an-organization).
Decide what the Ledger boundary means before you create it. Do not use one Ledger for unrelated products that need different account structures, routes, or operational controls.
## Create the Ledger
***
From the **Ledgers** page, click the **New Ledger** button in the top-right corner.
The **New Ledger** form appears.
In the **Ledger Details** tab, enter the Ledger name in the **Ledger Name** field.
(Optional) To include metadata, select the **Metadata** tab. This tab displays two fields: **Key** and **Value**.
* To add more metadata, click the plus (+) icon next to the first row.
(Optional) Select the **Settings** tab to configure validation rules for this Ledger.
* Enable **Validate Account Type** if Accounts must use registered Account Types.
* Enable **Validate Routes** if Transactions must follow pre-configured Accounting Routes.
After you complete the form, click **Save**.
A success message appears in the top-right corner. The main **Ledgers** page now lists the new Ledger.
## Field guide
***
| Field | What to enter | Example |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| Ledger Name | A clear name for the accounting workspace. Use a name that shows the product, country, environment, or business boundary. | `Brazil Payments Ledger` |
| Metadata Key | Optional classification or integration field name. | `environment` |
| Metadata Value | Value for the metadata key. | `production` |
| Validate Account Type | Optional setting. Turn on when every non-external Account in this Ledger must use a previously registered Account Type. Leave off if you will not classify Accounts by type yet. | `Enabled` |
| Validate Routes | Optional setting. Turn on only after the Accounting Routes for this Ledger are planned or configured, because Transactions must match a valid route when this is enabled. | `Disabled` |
The fields in the Metadata tab are open. The tab has no predefined keys or values. Use consistent key names across your Organization, such as `environment`, `businessLine`, or `region`, so you can filter and audit Ledgers later.
## Ledger settings
***
The **Settings** tab controls the validation rules for this Ledger.
### Validate Account Type
When you enable this setting, every new non-external Account must use an Account Type registered for this Ledger. Enable it when you want operators to classify Accounts consistently, such as `customer`, `merchant`, `treasury`, or `fee`.
After you enable **Validate Account Type**, the Ledger rejects any non-external Account without a valid Account Type.
### Validate Routes
When you enable this setting, Transactions must match the Accounting Routes configured for this Ledger. Enable it only after you plan or configure your route model.
If you enable **Validate Routes** before routes exist, any Transaction without a matching route fails validation.
## What to do next
***
After you create the Ledger:
1. Review whether **Validate Account Type** and **Validate Routes** are correct for this Ledger.
2. Create at least one Asset in [Creating an Asset](/en/midaz/console/creating-an-asset).
3. If you need to change the validation rules later, use the **Settings** tab described in [Managing Ledgers](/en/midaz/console/managing-ledgers-via-console#ledger-settings).
[Create a Ledger endpoint](/en/reference/midaz/create-a-ledger)
# Creating a Portfolio
Source: https://docs.lerian.studio/en/midaz/console/creating-a-portfolio
Create a new Portfolio in the Console to group Accounts under a customer, business unit, or reporting boundary, and set an Entity ID for external systems.
A Portfolio groups Accounts under a customer, business unit, or reporting boundary.
## Before you start
***
You need an existing Organization and Ledger. Create a Portfolio when you want to group Accounts under a customer, business unit, product, or reporting boundary.
Entity ID is optional. You can update it later through the Portfolio update API, provided the new value is non-empty.
## Create the Portfolio
***
To create a Portfolio, follow these steps:
From the **Portfolios** page, click the **New Portfolio** button.
The **New Portfolio** form opens on the right side of the screen.
Fill in the fields and click the **Save** button.
## Field guide
***
| Field | What to enter | Example |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| Portfolio Name | Required name for the customer, business unit, or reporting group. | `Acme Retail Portfolio` |
| Entity ID | Optional external entity or system identifier. You can update it later through the Portfolio update API when the new value is non-empty. | `customer_acme` |
| Metadata Key | Optional classification or integration field name. | `relationshipManager` |
| Metadata Value | Value for the metadata key. | `ops-team-1` |
Entity ID is optional. You can update it later through the Portfolio update API, provided the new value is non-empty.
## What to do next
***
Now link the Portfolio to Accounts from [Adding Portfolio to Account](/en/midaz/console/adding-portfolio-to-account). You can also create Accounts that belong to it in [Creating an Account](/en/midaz/console/creating-an-account).
[Create a Portfolio endpoint](/en/reference/midaz/create-a-portfolio)
# Creating a Segment
Source: https://docs.lerian.studio/en/midaz/console/creating-a-segment
Create a new Segment in the Console to classify Accounts by shared characteristics, business rules, or reporting needs across your Ledger.
A Segment classifies Accounts by shared characteristics or business rules.
## Before you start
***
You need an existing Organization and Ledger. Create a Segment when a group of Accounts needs a shared classification for reporting, filtering, or fee rules.
## Create the Segment
***
To create a Segment, follow these steps:
From the **Segments** page, click the **New Segment** button.
The **New Segment** form opens on the right side of the screen.
In the **Segment Details** tab, enter the **Segment Name**.
(Optional) Select the **Metadata** tab to add metadata.
When the details are complete, click the **Save** button.
The Segment appears in the list of Segments for your Ledger.
## Field guide
***
| Field | What to enter | Example |
| -------------- | --------------------------------------------------------------------------- | ------------------ |
| Segment Name | Required name for a group of Accounts with shared characteristics or rules. | `Retail Customers` |
| Metadata Key | Optional classification or integration field name. | `region` |
| Metadata Value | Value for the metadata key. | `BR-SP` |
The Metadata fields have no predefined keys or values. Use consistent key names across your Organization, such as `region`, `businessLine`, or `channel`. Consistent keys keep Segments useful for filtering and reporting.
## What to do next
***
After you create the Segment, link it to Accounts from [Adding Segment to Account](/en/midaz/console/adding-segment-to-account). You can also create Accounts that belong to it in [Creating an Account](/en/midaz/console/creating-an-account).
[Create a Segment endpoint](/en/reference/midaz/create-a-segment)
# Creating a Transaction
Source: https://docs.lerian.studio/en/midaz/console/creating-a-transaction
Create a new Transaction in the Console using the 1:1 or n:n wizard to move Asset balances between source and destination Accounts by alias.
A Transaction moves value between Accounts in a Ledger. Create one only after the Asset, source account, destination account, and any required Accounting Route are ready.
## Before you start
***
Confirm that:
* The source and destination Accounts exist in the selected Ledger.
* Both Accounts use the Asset selected for the Transaction.
* You know each Account alias. A leading `@` is a common convention, not a requirement.
* Source accounts allow sending and destination accounts allow receiving.
* If **Validate Routes** is on, the Transaction must match an existing Accounting Route.
## Create the Transaction
***
From the **Transactions** page, click the **New Transaction** button.
The **New Transaction** modal will open. Select the type of transaction you want to create:
* **Simple 1:1** - Transactions using one source and one destination
* **Complex n:n** - Transactions using more than one source or destination
After selecting the transaction type, the **New Transaction** page opens with a step-by-step form:
Fill in the basic transaction information:
* (Optional) Add a **Description** for the transaction
* (Optional) Specify an **Accounting route group** for classification
* Enter the **Value** (amount) to transfer
* Select the **Asset** for the transaction
Click **Next** to proceed to the accounts selection step.
Specify the accounts involved:
* Search for the **source account** by alias and select it
* Search for the **destination account** by alias and select it
* For complex transactions, you can add multiple sources and/or destinations
You **must** use the account alias to perform transactions. Make sure you save it when you create your account.
If you forget your alias, you can find it by [editing account information](/en/midaz/console/editing-account-information).
Click **Next** to proceed to the operations step.
Review and adjust the operation details:
* For simple transactions (1:1), the Console fills the amounts automatically
* For complex transactions (n:n), specify the amount for each source (debit) and destination (credit)
* Expand each operation to add optional descriptions and metadata
For complex transactions, the sum of all source amounts must equal the sum of all destination amounts.
When all the information is correct, click the **Go to Review** button.
The Console shows a review page with a summary of all the transaction information:
* Transaction value and asset
* Source and destination accounts
* Individual debit and credit operations
* Metadata records (if any)
Review the information carefully. If everything is correct:
* Click **Send Transaction** to submit and view the transaction details
* Click **Send and Create another** to submit and immediately start a new transaction
On success, the Console opens the transaction details page with the transaction summary, data, and status.
## Field guide
***
**Transaction Data**
| Field | What to enter | Example |
| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------ |
| Transaction description | Optional business description of the movement. | `Customer payment to merchant` |
| Accounting route group | Optional route classification when your setup uses route validation or grouping. | `Pix Transfer Group` |
| Transaction Route | Select the Transaction Route associated with the transaction. | `Pix Transfer Route` |
| Asset | Asset used by the Transaction. It must match the selected Accounts. | `BRL` |
| Value | Total amount to move. For complex transactions, source totals must equal destination totals. | `100.00` |
**Source/Destination**
| Field | What to enter | Example |
| ----------- | ------------------------------------ | --------------- |
| Source | Account alias that will be debited. | `@customer_123` |
| Destination | Account alias that will be credited. | `@merchant_456` |
**Operations**
| Field | What to enter | Example |
| --------------------- | --------------------------------------------------------------------- | ------------------------------------- |
| Operation description | Optional description for the operation. | `Debit for the source account` |
| Chart of accounts | Optional name of the Chart-of-Accounts that the operation belongs to. | `Pix debit - source` |
| Operation Route | Optional operation route for this transaction leg. | `Checking account validation for Pix` |
| Metadata Key | Optional classification or integration field name. | `externalReference` |
| Metadata Value | Value for the metadata key. | `order_98765` |
## Transaction types explained
***
### Simple Transaction (1:1)
Use for straightforward transfers between two accounts:
* One source account (debit)
* One destination account (credit)
* Same amount debited and credited
**Example**: Transfer \$100 from Customer A to Merchant B.
### Complex Transaction (n:n)
Use for transactions involving multiple parties:
* Multiple source accounts (debits) and/or multiple destination accounts (credits)
* You can distribute amounts across accounts
* Total debits must equal total credits
**Examples**:
* Payment with fees:
* Customer pays \$103
* Merchant receives \$100
* Fee account receives \$3
* Consolidated payment: Multiple customers fund a single vendor payment
The fields in the Metadata tab are open. There are no default keys or values. Use consistent key names across your Organization, such as `externalReference`, `orderId`, or `channel`, to link Transactions back to source systems for reconciliation and audit.
## What to do next
***
After sending the Transaction, review the details page and confirm the status, source operation, destination operation, and account balances.
Create a Transaction programmatically with the JSON endpoint.
Inspect the Transaction status and operations via API.
# Creating an Account
Source: https://docs.lerian.studio/en/midaz/console/creating-an-account
Create a new Account in the Console, tie it to an Asset, set its alias and type, and optionally link it to a Portfolio or add metadata during setup.
You **must** link an Account to an existing Asset. [Create the Asset](/en/midaz/console/creating-an-asset) before you create the Account.
## Before you start
***
You need an existing Organization, Ledger, and Asset. If the Ledger uses **Validate Account Type**, create the required Account Types before you open this form.
Decide what the account represents before you choose its type and alias. Common examples include customer, merchant, treasury, fee revenue, settlement, suspense, and expense accounts.
## Create the Account
***
To create an Account, follow these steps:
From the **Accounts** page, click the **New Account** button.
The **New Account** form will open on the right side of the screen with three tabs: **Account Details**, **Portfolio**, and **Metadata**.
Fill in the required information in the **Account Details** tab.
For more information about the fields in the form, refer to the [Field guide](#field-guide) section.
*(Optional)* Switch to the **Portfolio** tab to link this account to a portfolio for reports and management.
* Select a portfolio from the dropdown list
* If no portfolios exist, you can [create one first](/en/midaz/console/managing-portfolios)
* You can link or unlink accounts from portfolios at any time
*(Optional)* Switch to the **Metadata** tab to add custom key-value pairs for additional context or integration purposes.
Click the **Save** button to create the Account.
## Field guide
***
| Field | What to enter | Example |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| Account Name | Optional descriptive name for the account. Midaz accepts up to 256 characters and supplies a default name if no name is submitted. | `John Doe Checking Account` |
| Account Alias | Unique human-readable identifier. A leading `@` is a common convention, not a requirement. This cannot be changed after creation. | `@customer_john_checking` |
| Type | Account classification. Required. When Account Type validation is enabled, you select from the predefined Account Types; otherwise you type it freely. This cannot be changed after creation. | `customer` |
| External account | Switch available when creating an account. Turn it on to create an external account — the type is set to `external` automatically and represents balances held outside this ledger. | — |
| Entity ID | Optional external holder, customer, department, or partner identifier. | `customer_123` |
| Asset | Asset this account will use. Select an existing Asset. This cannot be changed after creation. | `BRL` |
| Segment | Optional grouping category, available when Segments exist. | `Retail` |
| Portfolio | Optional reporting or management group for this Account. | `Customer Portfolio` |
| Metadata Key | Optional classification or integration field name. | `costCenter` |
| Metadata Value | Value for the metadata key. | `BR_11101997` |
## Quick examples
***
**Scenario**: Create a checking account for a customer.
| Field | Value |
| --------------- | ------------------------ |
| Account Name | `John Doe Checking` |
| Account Alias | `@customer_123_checking` |
| Type | `checking` |
| Entity ID | `customer_123` |
| Asset | `USD` |
| Allow Sending | `true` |
| Allow Receiving | `true` |
**Scenario**: Create a main treasury account for company operations.
| Field | Value |
| --------------- | ------------------- |
| Account Name | `Main Treasury` |
| Account Alias | `@treasury_main` |
| Type | `treasury` |
| Entity ID | `org_treasury_dept` |
| Asset | `USD` |
| Segment | `Finance` |
| Allow Sending | `true` |
| Allow Receiving | `true` |
**Scenario**: Create a budget account for the engineering department.
| Field | Value |
| --------------- | ------------------------- |
| Account Name | `Engineering Budget 2024` |
| Account Alias | `@eng_budget_2024` |
| Type | `expense` |
| Entity ID | `dept_engineering` |
| Asset | `USD` |
| Segment | `Engineering` |
| Allow Sending | `true` |
| Allow Receiving | `true` |
## Important notes
***
You cannot change the following fields after you create the account:
* **Account Alias** - Choose it carefully, because transactions use the alias
* **Type** - You cannot reclassify it after you set it
* **Asset Code** - You cannot change the account's currency or commodity
To change any of these fields, create a new account.
If your ledger uses Account Type validation:
* You select the **Type** from your predefined Account Types instead of typing it freely
* You must select from predefined account types or create them first
* This ensures consistency across your organization's chart of accounts
See [Managing Accounts](/en/midaz/console/managing-accounts#account-type-validation) for more information.
These flags control transaction permissions:
* **Allow Sending = false**: You cannot use the account as a source (debit side) in transactions
* **Allow Receiving = false**: You cannot use the account as a destination (credit side) in transactions
Both flags default to on. You can change them only after you create the account. Use these flags to:
* Freeze an account temporarily
* Create a receive-only account (for example, escrow)
* Create a send-only account (rare case)
After you create an account, you can use it in transactions immediately. Make sure all the information is correct before you save, especially the immutable fields.
## What to do next
***
After you create the source and destination Accounts:
1. Create or review Accounting Routes in [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route).
2. Create a Transaction in [Creating a Transaction](/en/midaz/console/creating-a-transaction).
[Create an Account endpoint](/en/reference/midaz/create-an-account)
# Creating an Account Type
Source: https://docs.lerian.studio/en/midaz/console/creating-an-account-type
Create a new Account Type in the Console to classify Accounts and enforce validation used by Accounting Routes when transactions are recorded.
Account Types classify Accounts. Accounting Routes use these types to define which accounts an operation can use. Create them before Accounts if you enabled **Validate Account Type** for the Ledger.
## Before you start
***
You need an existing Organization and Ledger. If you cannot see the Account Types menu, enable **Validate Account Type** in [Ledger Settings](/en/midaz/console/managing-ledgers-via-console#ledger-settings).
Define Account Types from the accounting model, not from individual customer names. Examples: `customer`, `merchant`, `treasury`, `fee`, `settlement`, `revenue`, `expense`.
## Create the Account Type
***
From the **Account Types** page, click the **New Account Type** button.
The **New Account Type** form will open on the right side of the screen.
Fill in the information and click the **Save** button.
## Field guide
***
| Field | What to enter | Example |
| ----------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| Account Type Name | Clear operator-facing name for this Account Type. | `Customer` |
| Description | Optional explanation of when operators should use this type. | `Accounts that represent end customers and can send or receive BRL payments.` |
| Key Value | Stable value used to classify Accounts and route rules. Use a short lowercase value that will remain stable. | `customer` |
Accounts and Accounting Routes reference the **Key Value**. Keep it short, lowercase, and aligned with your accounting model. You cannot change it after you create the Account Type. To use a different value, create a replacement Account Type, recreate dependent Accounts with the new type, and update dependent routes.
## What to do next
***
After creating Account Types, create Accounts and select the correct type in [Creating an Account](/en/midaz/console/creating-an-account).
[Create an Account Type endpoint](/en/reference/midaz/create-an-account-type)
# Creating an Accounting Route
Source: https://docs.lerian.studio/en/midaz/console/creating-an-accounting-route
Create a new Accounting Route in the Console using the 3-step wizard to define transaction routes, operation routes, and accounting scenarios.
You create an Accounting Route through a guided 3-step wizard. The wizard walks you through the transaction route, the operation routes with their accounting scenarios, and a final review.
## Before you start
***
Create the Account Types and Accounts that the route validates. If you have not chosen the operation type, the validation type, and the accounting scenarios yet, read [Accounting rules](/en/midaz/console/mc-accounting) first.
If you enable **Validate Routes** for the Ledger, every transaction must match an existing route. Do not enable route validation until the required Accounting Routes are ready.
## Step 1: Basic information
***
From the **Accounting Routes** page, click **New Route**.
The creation wizard opens on **Step 1 — Basic information**.
Fill in the basic information:
* **Title** (required): a descriptive name for this transaction route. You must fill in this field to continue.
* **Description** (optional): a description of the route's purpose. If you leave it empty, the wizard submits and stores an empty description.
Click **Next** to proceed to Step 2.
## Step 2: Operation Routes
***
In this step, you add and configure the operation routes. Operation routes define how the ledger validates accounts and records accounting entries.
A **Bidirectional** operation route acts as both source and destination. When you later create a transaction, the ledger matches a bidirectional route on **both** the source side and the destination side. A single route can then validate both ends of the flow.
Click **Add Operation Route** to create your first operation route card.
Configure the operation route:
* **Title** (required): a name for this operation route. The operation route has a single required text field and no separate description field. You cannot advance to the next step until you fill it in.
* **Operation Type**: `Source`, `Destination`, or `Bidirectional`.
* **Validation Type**: Optionally configure `Account Type` or `@Alias` to validate accounts. An Operation Route can omit the account validation rule.
* **Account Type or Alias**: the selected Account Type values or the specific account alias.
*(Optional)* Configure **Accounting Scenarios** to define how the ledger records entries. Toggle on the scenarios you need:
Standard one-step transaction. Define the **Account Code** and **Description** for:
* **Debit** — The account code debited on execution
* **Credit** — The account code credited on execution
For Direct Transactions, which sides appear depends on the operation type. Source routes show debit only, destination routes show credit only, and bidirectional routes show both. Other scenarios may differ — see the Two-Step Transaction tab for details.
Transactions with a hold-then-commit cycle. Configure entries for each phase:
* **Hold (Value Reserve)** — Entries that record reserved funds
* **Commit (Reservation Confirmation)** — Entries that confirm the reservation
* **Cancel (Reservation Cancellation)** — Entries that cancel the reservation. Fill these fields too: the ledger validates Hold, Commit, and Cancel as an atomic group and rejects the route when the Cancel mapping is missing.
The available entry fields vary by operation type:
* **Source** — Hold and Cancel show both debit and credit fields. Commit shows debit only.
* **Destination** — Only the Commit phase appears, with credit only. Hold and Cancel are not available.
* **Bidirectional** — All phases show both debit and credit fields.
Entries that record the reversal of a completed transaction. Define the debit and credit account codes and descriptions for the reversal entries.
The Reversal scenario is only available for **Bidirectional** operation routes.
Entries that apply when a debit exceeds the account's available funds and draws on an overdraft line. Define the debit and credit account codes and descriptions for the overdraft draw and later repayment.
Overdraft is available for **Source**, **Destination**, and **Bidirectional** operation routes. When you enable Overdraft, the wizard also enables the Direct scenario, because an overdraft draw settles through a direct entry.
Entries that record blocking and unblocking funds on an account. Configure two independent sections, **Block** and **Unblock**. Each section is all-or-nothing: leave it empty to use the default entries, or fill in both the account code and the description for every visible side.
Block is available for all operation types. Source routes show debit entries only, destination routes show credit entries only, and bidirectional routes show both.
*(Optional)* Click **Add Operation Route** again to add more operation routes to this Accounting Route.
You must add at least one source and one destination operation route, or at least one bidirectional route. The wizard will not proceed without a valid route configuration.
Click **Next** to proceed to Step 3.
## Step 3: Review
***
Review the complete configuration:
* Transaction route title and description
* Each operation route with its type, validation rules, and accounting entries
* Accounting scenarios with debit and credit entries per transaction type
To adjust anything, click **Previous** to go back.
Click **Create Transaction Route** to create the Accounting Route.
## Field guide
***
### Basic information
| Field | What to enter | Example |
| ----------- | --------------------------------------------------------------------- | -------------------------------------------------- |
| Route Title | Required route name. Keep it short and specific to the business flow. | `Pix Transfer Route` |
| Description | Optional explanation of what the route validates or records. | `Handles Pix transfers between customer accounts.` |
### Operation route
| Field | What to enter | Example |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| Title | Required name for this operation route card. Must be filled before advancing; there is no separate description field. | `Checking account validation for Pix` |
| Type | Accounting side this operation route applies to: `Source`, `Destination`, or `Bidirectional`. This cannot be changed after creation. | `Bidirectional` |
| Validation Type | Optional account-validation rule. Use `Account Type` for account classes or `@Alias` for a specific account. An Operation Route can omit this rule. | `Account Type` |
| Account Type or Alias | Account Types or aliases accepted by this operation route. | `customer` |
| Accounting Scenarios | Transaction scenarios this route should record: Direct, Two-Step, Reversal, Overdraft, or Block. | `Direct Transaction` |
| Account Code | Accounting code used in the debit or credit entry for the selected scenario. | `1.1.1.001` |
| Entry Description | Entry-level description shown in the scenario configuration. | `Customer checking - outbound` |
## Quick example
***
**Transaction Route**
| Field | Value |
| ----------- | --------------------------------------------------------- |
| Title | `Pix Transfer Route` |
| Description | `Handles Pix instant transfers between checking accounts` |
**Operation Route 1 — Bidirectional**
| Field | Value |
| --------------- | ------------------------------------- |
| Title | `Checking account validation for Pix` |
| Operation Type | `Bidirectional` |
| Validation Type | `Account Type` |
| Account Types | `checking` |
**Direct Transaction scenario**
| Side | Account Code | Description |
| ------ | ------------ | ------------------------------ |
| Debit | `1.1.1.001` | `Customer checking — outbound` |
| Credit | `1.1.1.002` | `Customer checking — inbound` |
**Two-Step Transaction — Hold**
| Side | Account Code | Description |
| ------ | ------------ | ----------------------------- |
| Debit | `1.1.1.001` | `Customer checking — reserve` |
| Credit | `2.1.1.001` | `Pending settlement — hold` |
**Two-Step Transaction — Commit**
| Side | Account Code | Description |
| ------ | ------------ | ------------------------------ |
| Debit | `2.1.1.001` | `Pending settlement — release` |
| Credit | `1.1.1.002` | `Customer checking — settled` |
## What to do next
***
After you create the route, create a transaction that matches the same account types, aliases, and scenario in [Creating a Transaction](/en/midaz/console/creating-a-transaction).
Create the transaction-level route via API.
Create operation-level routing rules via API.
# Creating an Asset
Source: https://docs.lerian.studio/en/midaz/console/creating-an-asset
Create a new Asset in the Console — currency, crypto, commodity, or custom — and set the code that Accounts and Transactions use.
An Asset defines the unit of value that Accounts and Transactions use inside a Ledger. Create Assets before you create Accounts, because every Account uses one Asset.
## Before you start
***
You need an existing Organization and Ledger. Decide whether the Asset is a currency, crypto asset, commodity, or another custom unit of value.
Choose Asset codes carefully. Accounts and Transactions depend on them.
## Create the Asset
***
From the **Assets** page, click the **New Asset** button.
The **New Asset** form opens on the right side of the screen.
Select the **Type** of Asset.
* *Crypto*: for cryptocurrencies such as Bitcoin, Ethereum, and Ripple.
* *Commodity*: for tradable raw materials such as gold, natural gas, soybeans, and corn.
* *Currency*: for currencies such as Brazilian Real, US Dollar, and Euro.
* *Others*: for any other type of Asset.
You **cannot** reuse an Asset name or code in the same Ledger. Both the name and the code must be **unique**.
Specify the **Asset Name**.
Specify the **Code** for the Asset. Use uppercase letters only.
* If the Asset is a currency, select its code from the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) list.
\[Optional] Select the **Metadata** tab to add custom metadata.
After you fill in the fields, click the **Save** button.
The Asset appears in the list of available Assets for your Ledger.
Saving an Asset also provisions a system-managed External Account when one does not already exist for that Asset code. Its canonical alias is `@external/`.
The fields in the Metadata tab are open. There are no predefined keys or values. Use consistent key names across your Organization, such as `country`, `settlementNetwork`, or `regulatoryClass`, so you can filter and audit Assets later.
## Field guide
***
| Field | What to enter | Example |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| Type | Asset category. Use `Currency` for fiat money, `Crypto` for crypto assets, `Commodity` for tradable commodities, or `Others` for custom units. | `Currency` |
| Asset Name | Human-readable Asset name shown to operators. | `Brazilian Real` |
| Code | Asset code in uppercase letters. For currencies, use the ISO 4217 code. | `BRL` |
| Metadata Key | Optional classification or integration field name. | `settlementNetwork` |
| Metadata Value | Value for the metadata key. | `pix` |
## What to do next
***
After creating the Asset, create Accounts that use it in [Creating an Account](/en/midaz/console/creating-an-account).
[Create an Asset endpoint](/en/reference/midaz/create-an-asset)
# Creating an Organization
Source: https://docs.lerian.studio/en/midaz/console/creating-an-organization
Create a new Organization in the Midaz Module to set the business boundary that will own your Ledgers, Assets, Accounts, and Transactions.
An Organization is a container for Midaz operations. It can be a top-level Organization or a child of another Organization. Create it before you create Ledgers, Assets, Accounts, or Transactions.
## Before you start
***
Decide which business boundary this Organization represents. In most setups, it maps to the company, business unit, or regulated entity that owns one or more Ledgers.
If you set up Midaz for the first time, follow the [Setup path](/en/midaz/console/midaz-console-setup-path) before you create downstream entities.
## Create the Organization
***
Go to the **Organizations** page. Click the **New Organization** button.
The **New Organization** page opens.
Fill in the required details about the Organization.
* **Optional**: To include metadata, expand the **Metadata** section. The section then shows the **Key** and **Value** fields.
* To add more metadata entries, click the plus (+) sign next to the first row.
See the [Field guide](#field-guide) section for details about each field.
After you enter all the details, click **Save**.
Your new Organization appears on the Organizations page. You can now manage and configure it.
## Field guide
***
| Field | What to enter | Example |
| ---------- | --------------------------------------------------------------------------------- | ------------------------ |
| Legal Name | Official registered name. Use a name operators will recognize later. | `Lerian Payments Brazil` |
| Trade Name | Commercial or customer-facing name. | `Lerian` |
| Document | Official tax ID or registration number. Enter the identification document number. | `12345678000190` |
| Address | Street name, number, and main address line. | `Avenida Paulista, 1234` |
| Complement | Extra address details, such as suite, floor, or apartment. | `CJ 123` |
| Country | Two-letter ISO 3166-1 alpha-2 country code where the Organization is registered. | `BR` |
| State | State, province, or region. | `São Paulo` |
| City | City where the Organization is registered. | `São Paulo` |
| ZIP Code | Postal code, up to 20 characters. | `04696040` |
### Metadata
Metadata is optional key-value data. Use it for internal classification, integration references, or reporting.
| Field | What to enter | Example |
| ----- | ---------------------------------- | ------------- |
| Key | The metadata field name. | `costCenter` |
| Value | The value for that metadata field. | `BR_11101997` |
## What to do next
***
After you create the Organization, create its first Ledger in [Creating a new Ledger](/en/midaz/console/creating-a-new-ledger).
[Create an Organization endpoint](/en/reference/midaz/create-an-organization)
# Creating an Alias from the Holder
Source: https://docs.lerian.studio/en/midaz/console/crm-alias-from-holder
Create an Alias Account directly from the Holders page in the Console so the Holder ID is preset and the new Alias is linked to its owner in one step.
You can also create an Alias Account from the **Holders** page. To do so, follow these steps:
On the **Holders** page, find the Holder that you want to link to the Alias Account.
In the Actions column, click the three dots () and select **Create Alias Account**.
The Console opens the Alias Accounts page and shows the New Alias Account form.
Fill in the fields as required, then click **Create Alias Account**.
The Console pre-fills the `Holder ID` field. You cannot change it.
# Creating a Holder
Source: https://docs.lerian.studio/en/midaz/console/crm-creating-a-holder
Create a new Holder in the Midaz Module as an Individual or Corporate record to represent the person or company that owns one or more Accounts.
A Holder represents the person or company behind one or more Accounts. Create the Holder before you create Alias Accounts. An Alias Account connects customer-facing account information to a ledger Account.
## Before you start
***
Decide whether the Holder is an **Individual** or **Corporate** record. The type you choose controls which identity and registration fields the form shows.
## Create the Holder
***
Go to the **Holders** page. Click the **New Holder** button.
A window opens. Choose the Holder type you want to create.
* If you select **Individual** (Natural Person), the **New Holder** page opens for you to enter the individual's information.
* If you select **Corporate** (Legal Person), the **New Holder** page opens for you to enter the company's information.
Follow the on-screen instructions to enter the required information. Click **Create Holder**.
When the creation succeeds, you see a confirmation message. The list refreshes and shows the new Holder.
## Field guide
***
### Individual
**Basic Information**
| Field | What to enter | Example |
| ----------- | ------------------------------------------------------------------------------- | ---------------- |
| External ID | Optional ID from your CRM, core banking, KYC, or customer system. | `customer_123` |
| Holder type | A read-only field that indicates the type of Holder. | `Natural Person` |
| Name | The full name of the Holder. | `John Doe` |
| Document | Tax ID, national ID, or company registration number expected by your operation. | `12345678900` |
**Contact Information**
| Field | What to enter | Example |
| --------------- | -------------------------------------------------------------------------- | ----------------------- |
| Primary Email | The main email used to contact the Holder. | `john.doe@example.com` |
| Secondary Email | An alternative email used to contact the Holder. | `john.doe2@example.com` |
| Mobile Phone | Contact phone number, including country or area code when required. | `+5511999999999` |
| Other Phone | An alternative phone number, including country or area code when required. | `+551188888888` |
**Addresses**
| Field | What to enter | Example |
| -------------- | -------------------------------------------------------------- | ----------------- |
| Address Line 1 | Street name and building number. | `Rua Botucatu 10` |
| Address Line 2 | Extra address details, for example, apartment or suite number. | `Casa B` |
| Zip Code | Postal or ZIP code. | `04023060` |
| City | City where the address is located. | `São Paulo` |
| State | State, province, or region. | `SP` |
| Country | Two-letter country code (ISO 3166-1 alpha-2). | `BR` |
**Personal Details**
| Field | What to enter | Example |
| ------------- | --------------------------------------------------------- | ------------- |
| Favorite Name | Nickname or preferred name. | `Johnny` |
| Social Name | Social name, if different from the legal name. | `Johnny Doe` |
| Gender | Gender identity of the person. | `Male` |
| Birth Date | Date of birth in YYYY-MM-DD format. | `1990-01-01` |
| Civil Status | Civil status — for example: Single, Married, or Divorced. | `Married` |
| Nationality | Nationality, for example, Brazilian. | `Brazilian` |
| Mother's Name | Mother's full name. | `Jane Doe` |
| Father's Name | Father's full name. | `Richard Doe` |
| Status | Current status of the individual in the system. | `Active` |
**Metadata**
| Field | What to enter | Example |
| ----- | -------------------------------------------------- | ----------- |
| Key | Optional classification or integration field name. | `kycStatus` |
| Value | Value for the metadata key. | `approved` |
### Corporate
**Basic Information**
| Field | What to enter | Example |
| ----------- | ------------------------------------------------------------------------------- | ---------------- |
| External ID | Optional ID from your CRM, core banking, KYC, or customer system. | `company_123` |
| Type | A read-only field that indicates the type of Holder. | `Legal Person` |
| Name | The name of the company in the system. | `ACME` |
| Document | Tax ID, national ID, or company registration number expected by your operation. | `59014899000183` |
**Contact Information**
| Field | What to enter | Example |
| --------------- | -------------------------------------------------------------------------- | ------------------- |
| Primary Email | The main email used to contact the company. | `acme@example.com` |
| Secondary Email | An alternative email used to contact the Holder. | `acme2@example.com` |
| Mobile Phone | Contact phone number, including country or area code when required. | `+5511999999999` |
| Other Phone | An alternative phone number, including country or area code when required. | `+551188888888` |
**Addresses**
| Field | What to enter | Example |
| -------------- | -------------------------------------------------------------- | --------------- |
| Address Line 1 | Street name and building number. | `Rua Silva 100` |
| Address Line 2 | Extra address details, for example, apartment or suite number. | `Cj 12` |
| Zip Code | Postal or ZIP code. | `04013060` |
| City | City where the address is located. | `São Paulo` |
| State | State, province, or region. | `SP` |
| Country | Two-letter country code (ISO 3166-1 alpha-2). | `BR` |
**Company Details**
| Field | What to enter | Example |
| ----------------------- | ---------------------------------------------------------------- | -------------------------------- |
| Trade Name | The registered business name of the company, if applicable. | `ACME BR` |
| Activity | The type of business or activity the company engages in. | `Electronic devices development` |
| Legal Type | The legal structure of the company. | `Limited Liability` |
| Founding Date | The date when the company was established, in YYYY-MM-DD format. | `2025-01-01` |
| Company Size | The size classification of the company. | `Medium` |
| Status | Current status of the company in the system. | `Active` |
| Representative Name | The legal representative's name. | `Jane Doe` |
| Representative Document | The document number of the legal representative. | `43522116992` |
| Representative Email | The email address of the legal representative. | `janedoe@corporate.com` |
| Role | The role of the legal representative within the company. | `CFO` |
**Metadata**
| Field | What to enter | Example |
| ----- | -------------------------------------------------- | ----------- |
| Key | Optional classification or integration field name. | `kycStatus` |
| Value | Value for the metadata key. | `approved` |
## What to do next
***
After you create the Holder, create an Alias Account to connect the Holder to a Midaz ledger Account. See [Creating an Alias Account](/en/midaz/console/crm-creating-alias-account).
[Create Holder endpoint](/en/reference/midaz/crm/create-holder)
# Creating an Alias Account
Source: https://docs.lerian.studio/en/midaz/console/crm-creating-alias-account
Create a new Alias Account in the Midaz Module to link a Holder to a Midaz Account with banking, regulatory, and customer-facing account details.
An Alias Account links a Holder to an underlying Midaz Account and stores customer-facing account identifiers separately from ledger balances.
## Before you start
***
You need an existing Holder and an existing Midaz Account. Keep the Account ID available because this page connects Holder information to the ledger Account.
## Create the Alias Account
***
Go to the **Alias Accounts** page and click the **New Alias Account** button.
A window will open where you can specify the information about the account.
Fill in all the necessary information and click **Create Alias Account**.
If the creation is successful, you will receive a confirmation message, and the list will refresh to show the new Alias Account.
## Field guide
***
**Alias Account Details**
| Field | What to enter | Example |
| ---------- | ------------------------------------------------------------------------------------ | -------------------------------------- |
| Holder ID | The unique identifier of the Holder that owns or controls this Alias Account. | `019ed1d9-7694-7bba-a721-d8e53911ab08` |
| Account ID | The unique identifier of the Account for which you want to create the Alias Account. | `019ed1eb-21f8-781c-a30f-f5fca470497d` |
| Ledger | The name of the Ledger of which the account is part. | `Test Ledger` |
**Banking Details**
| Field | What to enter | Example |
| ------------ | ------------------------------------------------------------------------------------------ | ------------------------ |
| IBAN | International Bank Account Number (IBAN) tied to the account. | `US12345678901234567890` |
| Bank ID | Bank identifier used to link the account to its institution. | `12345` |
| Branch | Branch number or code where the account is held. | `0001` |
| Account | Customer-facing account number or external account identifier. | `1234567` |
| Type | ISO 20022 account type code (for example, CACC for a current account or SVGS for savings). | `CACC` |
| Opening Date | Date when the account was opened. | `2010-01-01` |
| Country Code | ISO 3166-1 alpha-2 country code where the bank is based. | `US` |
**Regulatory**
The related party fields below (Document, Name, Role, Start Date, and End Date) belong to a repeatable **Related Parties** group — click **Add related party** to add each related party entry.
| Field | What to enter | Example |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| Participant Document | The document number of the participant institution (e.g., CNPJ for Brazilian financial institutions). Optional field. | `---` |
| Document | Identification document of the related party (e.g., CPF or CNPJ). | `98765432100` |
| Name | Full legal name of the related party. | `Jane Doe` |
| Role | The type of relationship between the related party and the Alias Account. The select shows **Primary Holder**, **Legal Representative**, and **Responsible Party**. | `Legal Representative` |
| Start Date | Start date of the relationship with the Alias Account. | `2024-01-01` |
| End Date | End date of the relationship with the Alias Account. If not provided, the relationship is considered ongoing. | `2025-01-01` |
## What to do next
***
After creating the Alias Account, use it to identify the customer account in operational flows, or return to [Managing Alias Accounts](/en/midaz/console/managing-crm-alias-accounts) to review the record.
[Create Alias Account endpoint](/en/reference/midaz/crm/create-alias-account)
# Deleting a Holder
Source: https://docs.lerian.studio/en/midaz/console/crm-deleting-a-holder
Delete a Holder from the Midaz Module only after the Holder has no linked Alias Accounts or Accounts, within your deployment's data-retention and compliance policies.
Confirm your data-retention, compliance, and regulatory policies before you delete a Holder. In regulated environments, you may need to retain Holder records to meet legal, regulatory, audit, reporting, or compliance obligations.
To delete a Holder, follow these steps:
On the **Holders** page, find the Holder you want to delete.
In the **Actions** column, click the three dots (). Select **Delete**.
A confirmation dialog appears.
Click **Confirm** to delete the Holder.
## Data deletion options
***
**Regulated deployments**
In environments under financial regulation (for example, BACEN, LGPD, or GDPR), mandatory retention periods can apply to Holder records tied to financial transactions. Before you hard delete in production, ask your Legal and Compliance team to review the action.
When you delete a Holder in the Console, the Midaz Module performs a **soft delete**: it marks the Holder as deleted and keeps the record in the database. The record stays available for audit.
A **hard delete** — permanently removing the record from the database — is available only through the API, using the `?hard_delete=true` query parameter on the delete Holder endpoint. This action is irreversible, so use it only when your retention policy allows it.
Deletion fails while the Holder still has linked Alias Accounts or Accounts. Remove or unlink them before you delete the Holder.
For more information about data security and deletion, see [CRM Data Security](/en/midaz/crm/crm-data-security).
Keep Holder data accurate and current. Accurate Holder records support smooth operations and regulatory compliance.
# Deleting an Alias Account
Source: https://docs.lerian.studio/en/midaz/console/crm-deleting-alias-account
Delete an Alias Account from the Midaz Module. Your deployment's data-retention and compliance policies can keep the underlying record.
When you delete an Alias Account, the Midaz Module removes it from the active list. Your deployment's data-retention and compliance policies still apply. In a regulated environment, the underlying record can stay for audit, regulatory, or reporting purposes. Confirm your retention policy before you delete.
To delete an Alias Account, follow these steps:
From the **Alias Accounts** page, find the alias account to delete.
In the Actions column, click the three dots ().
Select **Delete**.
A confirmation dialog opens.
Click **Confirm** to delete the alias account.
# Editing a Holder
Source: https://docs.lerian.studio/en/midaz/console/crm-editing-a-holder
Edit an existing Holder in the Midaz Module to update personal, corporate, contact, or address information and keep customer records accurate.
To edit the information of a Holder, follow these steps:
From the **Holders** page, find the Holder that you want to edit.
Click the three dots () from the Actions column, and select **Edit**.
The Holders form will open.
Edit the information as needed and click **Update Holder**.
The **Document** field is read-only when editing — it cannot be changed after the Holder is created.
# Editing an Alias Account
Source: https://docs.lerian.studio/en/midaz/console/crm-editing-alias-account
Edit an existing Alias Account in the Midaz Module to update banking details, regulatory data, or metadata linked to the underlying Midaz Account.
To edit the information of an Alias Account, follow these steps:
From the **Alias Accounts** page, find the alias account that you want to edit.
Click the three dots () from the Actions column, and select **Edit**.
The Alias Account form will open.
Edit the information as needed and click **Update Alias Account**.
# Deleting a Ledger
Source: https://docs.lerian.studio/en/midaz/console/deleting-a-ledger
Delete a Ledger from your Organization in the Console when it is no longer used — data retention and compliance policies may keep audit records available.
To delete a Ledger, follow these steps:
Check your data retention and compliance policy before you delete a Ledger. The Console removes the Ledger from active listings, but keeps the underlying record. Your retention policy controls how long the record stays available for audit, regulatory, or reporting purposes.
On the **Ledgers** page, find the Ledger you want to delete.
Click the three-dot menu () in the **Actions** column. Select **Delete**.
A confirmation dialog will appear.
Click **Confirm** to delete the Ledger.
Midaz rejects Ledger deletion when `ENV_NAME=production`.
After you delete a Ledger, you can no longer use it. Its data may stay available for audit, regulatory, or reporting purposes, based on your retention policy.
[Delete a Ledger endpoint](/en/reference/midaz/delete-a-ledger)
# Deleting a Portfolio
Source: https://docs.lerian.studio/en/midaz/console/deleting-a-portfolio
Delete a Portfolio from the Console when its Accounts no longer need to be grouped together — subject to your deployment's data retention rules.
Check your data retention policy before you delete a Portfolio. The Console removes it from active listings. Your deployment can keep the underlying record for audit, regulatory, or reporting purposes.
To delete a Portfolio, follow these steps:
On the **Portfolios** page, find the Portfolio to delete.
In the **Actions** column, click the three-dot menu () and select **Delete**.
A confirmation dialog opens.
Click **Confirm** to delete the Portfolio.
[Delete a Portfolio endpoint](/en/reference/midaz/delete-a-portfolio)
# Deleting a Segment
Source: https://docs.lerian.studio/en/midaz/console/deleting-a-segment
Delete a Segment from the Console when it no longer classifies any Accounts — subject to your deployment's data retention and compliance policies.
When you delete a Segment, the Console removes it from active listings. Deletion follows your deployment's data retention and compliance policies. In regulated environments, the underlying record remains for audit, regulatory, or reporting purposes. Confirm your retention policy first.
To delete a Segment, follow these steps:
On the **Segments** page, find the Segment to delete. In the **Actions** column, click the three dots (). Select **Delete**.
A confirmation dialog opens.
Click **Confirm** to delete the Segment.
After you confirm, the Console removes the **Segment** from active listings.
[Delete a Segment endpoint](/en/reference/midaz/delete-a-segment)
# Deleting an Account
Source: https://docs.lerian.studio/en/midaz/console/deleting-an-account
Delete an Account from your Ledger in the Console when it is no longer used — External Accounts cannot be removed and retention policies still apply.
When you delete an Account, Midaz removes it from active listings. Your deployment's data retention and compliance policies apply. In regulated environments, your deployment can keep the record for audit, regulatory, or reporting purposes. Review your retention policy before you delete an Account.
To delete an Account, follow these steps:
On the **Accounts** page, find the Account to delete. In the **Actions** column, click the three dots (), then select **Delete**.
A confirmation dialog appears.
Click **Confirm** to delete the Account.
Midaz removes the **Account** from active listings. Your deployment's data retention and compliance policies still apply.
Before deleting an Account, ensure every balance has `available` and `onHold` equal to zero and that no transaction is in progress. Otherwise, Midaz rejects the deletion.
You **cannot directly** delete or modify External Accounts. Midaz creates them automatically with each Asset. Deleting an Asset soft-deletes active External Accounts with the same Asset code in the same Organization and Ledger.
[Delete an Account endpoint](/en/reference/midaz/delete-an-account)
# Deleting an Asset
Source: https://docs.lerian.studio/en/midaz/console/deleting-an-asset
Delete an Asset from your Ledger in the Console when it is no longer used — your deployment's data retention and compliance policies still apply.
When you delete an Asset, Midaz removes it from active listings. Your deployment's data retention and compliance policies apply. In regulated environments, your deployment can keep the record for audit, regulatory, or reporting purposes. Review your retention policy before you delete an Asset.
To delete an Asset, follow these steps:
On the **Assets** page, find the Asset to delete. In the **Actions** column, click the three dots (), then select **Delete**.
A confirmation dialog appears.
Click **Confirm** to delete the Asset.
Midaz removes the **Asset** from active listings. Your deployment's data retention and compliance policies still apply.
Deleting an Asset soft-deletes all active External Accounts with the same Asset code in that Organization and Ledger.
[Delete an Asset endpoint](/en/reference/midaz/delete-an-asset)
# Deleting an Organization
Source: https://docs.lerian.studio/en/midaz/console/deleting-an-organization
Remove an Organization from the Midaz Module — subject to your deployment's data retention and compliance policies.
To delete an Organization in **Midaz Module**, follow these steps:
When you delete an Organization, it no longer appears in active listings. Your deployment's data retention and compliance policies govern the record after that. In regulated environments, the record remains for audit, regulatory, or reporting purposes. Confirm your retention policy before you delete.
On the **Organizations** page, find the Organization you want to delete.
In the **Actions** column, click the three-dot menu ( ). Then select **Delete**.
A confirmation dialog will appear.
Click **Confirm** to delete the Organization.
Midaz rejects Organization deletion when `ENV_NAME=production`.
After you delete the Organization, you cannot use it anymore. The record may remain on the platform for audit, regulatory, or reporting purposes, under your deployment's retention policies.
[Delete an Organization endpoint](/en/reference/midaz/delete-an-organization)
# Editing a Ledger
Source: https://docs.lerian.studio/en/midaz/console/editing-a-ledger
Edit a Ledger from the Console to update its name, metadata, or the Validate Account Type and Validate Routes settings without any redeployment.
To view or edit the details of a Ledger, follow these steps:
On the **Ledgers** page, locate the Ledger you want to view or update. Click the three dots () in the **Actions** column and select **Details**.
A sidebar will open with multiple tabs: **Ledger Details**, **Metadata**, and **Settings**. Use the **Ledger Details** tab to edit the Ledger name, and the **Metadata** tab to manage custom key-value pairs.
To configure validation rules for this Ledger, click the **Settings** tab. From there you can enable **Validate Account Type** and **Validate Routes** for this Ledger.
Learn how to enable Validate Account Type and Validate Routes for this Ledger.
[Update a Ledger endpoint](/en/reference/midaz/update-a-ledger)
[Update Ledger Settings endpoint](/en/reference/midaz/update-ledger-settings)
# Editing Account information
Source: https://docs.lerian.studio/en/midaz/console/editing-account-information
Edit an existing Account in the Console to update its name, metadata, or other details — External Accounts created with an Asset cannot be changed.
To edit an Account, follow these steps:
On the **Accounts** page, find the Account that you want to edit. In the **Actions** column, click the three dots () and select **Details**.
The Console navigates to the account details page. Click the **Edit** button.
The edit sheet opens with two tabs: **Account Details** and **Metadata**.
Edit the information as needed and click **Save**.
You **cannot** edit External Accounts. Midaz creates one automatically for each Asset.
[Update an Account endpoint](/en/reference/midaz/update-an-account)
# Editing an Account Type
Source: https://docs.lerian.studio/en/midaz/console/editing-account-type
Edit an existing Account Type from the Console to refine how Accounts are classified and how validation rules are applied across your Ledger.
To edit the information of an Account Type, follow these steps:
From the **Account Types** page, find the Account Type that you want to edit, click the three dots ( ) from the **Actions** column, and select **Details**.
The **Edit** form will open on the right side of the screen.
Edit the information as needed and click **Save**.
You cannot change the **Key Value** when editing an Account Type — only the name, description, and metadata are editable.
[Update an Account Type endpoint](/en/reference/midaz/update-an-account-type)
# Editing an Accounting Route
Source: https://docs.lerian.studio/en/midaz/console/editing-an-accounting-route
Edit an existing Accounting Route in the Console to update its operation routes, validation rules, and accounting scenarios.
You can edit an existing Accounting Route. The Console edit flow changes its operation routes.
Go to the **Accounting Routes** page. Find the Accounting Route to edit. Click the three dots () in the **Actions** column. Select **Details**.
A detail sheet opens on the right side of the screen. It shows the route configuration: all operation routes, their types, validation rules, and accounting entries.
Click **Edit** at the bottom of the detail sheet.
The edit form opens with the existing configuration already filled in. It uses the same multi-step layout as the creation wizard.
Change the information as needed:
* Edit existing operation routes (title, validation type, account types or alias, accounting scenarios)
* Add new operation routes
* Remove an operation route with the delete button on its card
You cannot change the transaction route **Title** or **Description** in the Console edit flow — the edit wizard skips the basic-info step and submits operation-route changes only. You set the title and description at creation. To change them later, use the [Update Transaction Route](/en/reference/midaz/update-a-transaction-route) API.
You cannot change the **Operation Type** (Source, Destination, or Bidirectional) of an existing operation route. To change the direction, delete the operation route and create a new one.
Click **Update Transaction Route** to apply your changes.
## Viewing route details
***
To view an Accounting Route without editing it:
Go to the **Accounting Routes** page. Click the three dots () in the **Actions** column. Select **Details**.
A detail sheet opens on the right side of the screen. It shows the route configuration: all operation routes, their types, validation rules, and accounting entries. If an operation route has a **Code** assigned (set via API), the detail sheet also shows it.
You can also expand a route's card in the Accounting Routes list. The card shows a summary of its operation routes in the card-accordion view.
## Removing individual operation routes
***
You can remove a single operation route from an Accounting Route without affecting the rest of the configuration:
Open the Accounting Route in **Edit** mode (via **Details** → **Edit**).
Find the operation route card to remove. Click the delete button.
Confirm the deletion in the dialog that appears.
When editing, the resulting Accounting Route must contain 2–100 Operation Routes. It must include a Source and a Destination route, or at least one Bidirectional route. If you remove a route and break this rule, you must correct it before you save.
Update the transaction-level route via API.
Update operation-level routing rules via API.
# Editing an Organization
Source: https://docs.lerian.studio/en/midaz/console/editing-an-organization
Edit an Organization in the Midaz Module through the icon shortcut or the Organizations page to update legal name, address, metadata, or avatar.
There are two ways of editing the details of an Organization in **Midaz Module**.
## Editing via icon
***
To edit the Organization via the icon, follow these steps:
Select your Organization icon from the top of the navigation bar on the left side of the screen.
Select **Edit** from the displayed window.
You can update the following supported Organization fields:
* **Legal and Trade names**
* **Address information**
* **Parent Organization**
* **Metadata**
After making your changes, click **Save** to persist the supported changes.
## Editing via the Organization page
***
Go to the **Organizations** page and locate the Organization you want to update.
Click the three-dot menu ( ) in the **Actions** column, then select **Edit**.
The **Organization Details** page will open, allowing you to edit the supported Organization fields.
After making your changes, click **Save** to apply them.
[Update an Organization endpoint](/en/reference/midaz/update-an-organization)
# Editing an Asset
Source: https://docs.lerian.studio/en/midaz/console/editing-asset-information
Edit an existing Asset from the Console to update its name or metadata without affecting balances — the Asset type itself cannot be changed later.
To edit the information of an Asset, follow these steps:
From the **Assets** page, find the Asset that you want to edit, click the three dots () from the **Actions** column, and select **Details**.
The **Edit Asset** form will open on the right side of the screen.
Edit the information as needed and click **Save**.
* You cannot edit the type or the **Code** of the Asset after creation — only the name, status, and metadata are editable.
[Update an Asset endpoint](/en/reference/midaz/update-an-asset)
# Editing Portfolio information
Source: https://docs.lerian.studio/en/midaz/console/editing-portfolio-information
Edit an existing Portfolio in the Console to update its name, metadata, or details and keep your Account groupings accurate over time.
To edit the information of a Portfolio, follow these steps:
From the **Portfolio** page, find the Portfolio that you want to edit, click the three dots () from the **Actions** column, and select **Details** .
The **Portfolio Details** form will open on the right side of the screen.
Edit the information as needed and click **Save**.
[Update a Portfolio endpoint](/en/reference/midaz/update-a-portfolio)
# Editing Segment information
Source: https://docs.lerian.studio/en/midaz/console/editing-segment-information
Edit an existing Segment in the Console to update its name or metadata and refine how Accounts are classified for reporting and business rules.
To edit the information of a Segment, follow these steps:
From the **Segments** page, find the Segment that you want to edit, click the three dots () from the **Actions** column, and select **Details**.
The **Segment Details** form will open on the right side of the screen.
Edit the information as needed and click **Save**.
[Update a Segment endpoint](/en/reference/midaz/update-a-segment)
# Managing Account Types
Source: https://docs.lerian.studio/en/midaz/console/managing-account-types
Manage Account Types from the Midaz Module — enable Validate Account Type on a Ledger, then create and edit the categories that classify Accounts.
You use the Midaz Module to manage Account Types. Account Types classify accounts by their role in your Organization’s financial structure. When you enable validation on a Ledger, each new account must use a registered Account Type.
For more details, refer to the **Account Types** section on the [Accounts](/en/midaz/accounts#account-types) page.
### Enabling Account Type validation
You enable Account Type validation for a specific Ledger from the **Lerian Console**. You do not edit environment configuration files.
On the **Ledgers** page, click the three dots () next to the Ledger. Select **Details**. In the sidebar, click the **Settings** tab.
Toggle on the **Validate Account Type** switch. The change takes effect immediately for the selected Ledger.
Enabling Account Type validation does not make the Type field mandatory — it is always required when you create an account. Instead, validation changes the Type field from free text to a dropdown restricted to the registered Account Types.
## Accessing the Account Types page
***
To open the **Account Types** page, select the **Account Types** option in the **Foundation** section of the left sidebar menu.
The **Account Types** menu item appears only when **Validate Account Type** is enabled for the active Ledger. If validation is off, the menu item is hidden.
The **Account Types** page lists all Account Types you created for the current Ledger.
**Account Type validation disabled** — If **Validate Account Type** is off, a banner appears at the top of this page. You can view and edit the registered account types. The Ledger does not validate these rules when you create accounts. To enforce the rules, turn on **Validate Account Type** in the Ledger settings.
### Available actions
From the **Account Types** page, you can:
# Managing Accounting Routes
Source: https://docs.lerian.studio/en/midaz/console/managing-accounting-routes
Manage Accounting Routes in the Console — create, edit, or delete Source, Destination, and Bidirectional operation routes and accounting scenarios.
Use the **Midaz Module** to manage **Accounting Routes**, the configuration for transaction routing in Midaz. A guided wizard walks you through three steps: define the transaction route, add the operation routes, and review the setup.
An Accounting Route defines the full routing logic for a transaction. It has three parts:
* The transaction-level metadata — the title and description.
* The operation routes — they set which accounts participate and in which direction.
* The accounting scenarios — they control how the ledger records entries for each transaction type.
Start here if you need to decide whether to use Source, Destination, Bidirectional, Account Type validation, alias validation, Direct, Two-Step, or Reversal scenarios.
For technical details on the underlying entities, refer to the [Accounting Routes entities](/en/midaz/transaction-routing-entities) page.
## Key concepts
***
### Operation route types
Each operation route within an Accounting Route has one of three types:
* **Source** — Defines validation rules for accounts where funds originate (debit side)
* **Destination** — Defines validation rules for accounts that receive funds (credit side)
* **Bidirectional** — Applies to both sides of the transaction as both source and destination
A valid Accounting Route requires at least one source and one destination operation route, or at least one bidirectional route.
A bidirectional route works as both source and destination. Midaz matches it on **both** sides when you create a transaction. It appears in both the source and the destination route lists, so a single route can cover both ends of a flow.
### Validation types
Each operation route validates accounts by one of two methods:
* **Account Type** — Validates against one or more account type categories (e.g., `checking`, `savings`). Use when any account of a certain type is valid.
* **@Alias** — Validates against a specific account alias (e.g., `@treasury_main`). Use when you need to target an exact account.
### Accounting scenarios
Accounting scenarios define how the ledger records debit and credit entries for each operation route. You can configure entries for five transaction types:
* **Direct Transaction** — Standard one-step transaction. Define the debit and credit account codes and descriptions for immediate settlement.
* **Two-Step Transaction** — Transactions that go through a hold-then-commit cycle. Configure entries for:
* **Hold (Value Reserve)** — Entries recorded when the ledger reserves funds
* **Commit (Reservation Confirmation)** — Entries recorded when you confirm the reservation
* **Cancel (Reservation Cancellation)** — Entries recorded when you cancel the reservation
* **Reversal** — Entries recorded when you reverse a completed transaction
* **Overdraft** — Entries recorded when a debit exceeds the account's available funds and draws on an overdraft line. Available for Source and Bidirectional operation routes, and requires both debit and credit entries on every supported route type. When you enable it, Midaz also enables the Direct scenario.
* **Block/Unblock** — Entries recorded when you hold and release funds on the account's balance. The scenario tab label is **Block**. Available for all operation route types.
Each scenario entry includes an **Account Code** and a **Description** for both the debit and credit sides.
The available entry sides depend on the operation route type and the scenario phase. The general rule is:
* **Source** — Debit entries in Direct, Hold, Commit, and Cancel. Credit entries only in Hold and Cancel.
* **Destination** — Credit entries only, in Direct and Commit. Hold and Cancel are not available.
* **Bidirectional** — Both debit and credit entries in all phases, including Reversal.
* **Block/Unblock** — Available for all route types. It holds and releases funds on the account's balance.
## Accessing the Accounting Routes page
***
The **Accounting Routes** menu appears only when you enable the **Validate Routes** setting for the current Ledger. To enable it, open the Ledger details, go to the **Settings** tab, and toggle the **Validate Routes** switch.
**Accounting Routes validation disabled** — If you open this page by URL while **Validate Routes** is off, a banner appears at the top of the screen. The Ledger does not yet process the accounting routes registered here. Entries follow these rules only after you enable **Validate Routes** in the Ledger settings. You can still view and edit routes, but they have no effect until you enable validation.
To open the **Accounting Routes** page, select **Accounting Routes** in the **Accounting** section of the left sidebar menu.
The **Accounting Routes** page displays your routes for the current Ledger as a **card-accordion list**. Each transaction route is a collapsible card. The card shows its title, transaction route ID, and an operation-count badge.
Expand a card to view its operation routes. Each one shows its ID, title, code, and operation type. The Console color-codes operation types: **source** in red, **destination** in green, and **bidirectional** in gray. A route with no operation routes appears as a static (non-expandable) card.
### Available actions
From the **Accounting Routes** page, you can:
Define a new transaction route with operation routes and accounting scenarios.
Modify an existing accounting route and its operation routes.
## Common use cases
***
Create an Accounting Route with a bidirectional operation route that validates `checking` and `savings` account types. Enable Direct Transaction scenarios with the appropriate chart of accounts codes for Pix settlement entries.
Create an Accounting Route with separate source and destination routes. Enable Two-Step Transaction scenarios to record the hold (reservation), commit (confirmation), and optional cancel (cancellation) entries independently.
Create an Accounting Route with a bidirectional operation route that uses alias validation (`@treasury_main`). Enable both Direct Transaction and Reversal scenarios to handle standard movements and their reversals. Reversal entries require a bidirectional route.
## Best practices
***
Name your Accounting Routes clearly to indicate the type of transaction they handle (e.g., `Pix Transfer Route` instead of `Route 1`).
Use **Bidirectional** when the same validation rules apply to both sides. Use separate **Source** and **Destination** routes when each side has different account requirements.
Define your chart of accounts entries during route creation. This ensures that transactions processed through the route produce correct accounting records from the start.
The wizard includes a Review step. Use it to verify all operation routes, validation rules, and accounting entries before you create the route.
# Managing Accounts
Source: https://docs.lerian.studio/en/midaz/console/managing-accounts
Manage Accounts from the Lerian Console — create, edit, and delete internal Accounts, view balances, and classify them by type, portfolio, or segment.
Use **Midaz Module** to manage **Accounts** associated with your Ledger. Accounts are the fundamental units that hold balances and participate in transactions within your financial system.
## What are Accounts?
***
Accounts represent individual balance holders within your ledger structure. Each account:
* **Holds balances** in a specific asset (currency or commodity)
* **Participates in transactions** as sources or destinations of funds
* **Belongs to an entity** such as a customer, business unit, or external party
* **Supports portfolios, segments, and account types** for organization and reporting
* **Tracks history** with complete audit trails of all movements
Accounts can be classified as:
* **Internal Accounts** - Accounts you create and manage for your organization's operations
* **External Accounts** - Midaz creates these automatically for external assets or liabilities
For technical details, refer to the [Accounts](/en/midaz/accounts) page.
## Key concepts
***
* **Account Types** - Classify accounts according to your organization's financial structure (e.g., `checking`, `savings`, `revenue`). Enable validation to enforce account type rules.
* **Account Alias** - A unique, human-readable identifier (e.g., `@customer_john_doe`) that makes it easier to reference accounts in transactions and queries.
* **Portfolios** - Group related accounts together for reporting and management purposes. Useful for tracking customer portfolios or business divisions.
* **Segments** - Organize accounts into hierarchical structures for complex organizational needs like cost centers or departments.
## Accessing the Accounts page
***
To open the **Accounts** page, select the **Accounts** option in the **Accounts** section of the left sidebar menu.
The **Accounts** page displays a table with all Accounts in the current Ledger, including:
* **Account Name** - The account's display name
* **ID** - The account's unique identifier
* **Account Alias** - The unique, human-readable alias (if set)
* **Assets** - The asset (currency or commodity) the account holds
* **Metadata** - The number of custom key-value pairs on the account
* **Portfolio** - The portfolio the account belongs to (if any)
* **Actions** - The three-dots menu with the available actions — external accounts show a padlock icon instead
The padlock icon marks an account you **cannot** edit or delete. Midaz creates these **external accounts** automatically when you create an Asset.
### Available actions
From the **Accounts** page, you can:
Add internal accounts for customers, departments, or business operations
Modify account details, metadata, and organizational settings
Delete accounts you no longer need (internal accounts only)
## Account organization strategies
***
Create separate accounts for each customer, organized by account type and asset code.
**Example**:
* Customer checking account: `@customer_123_checking` (Type: `checking`, Asset: `USD`)
* Customer savings account: `@customer_123_savings` (Type: `savings`, Asset: `USD`)
* Group both under a Portfolio: `Customer 123 Portfolio`
Create dedicated treasury accounts to manage liquidity and operational funds.
**Example**:
* Main treasury: `@treasury_main` (Type: `treasury`, Asset: `USD`)
* Reserve fund: `@treasury_reserve` (Type: `reserve`, Asset: `USD`)
* Float account: `@float_operations` (Type: `float`, Asset: `USD`)
Create separate accounts for each currency your organization handles.
**Example**:
* USD operations: `@ops_usd` (Type: `operations`, Asset: `USD`)
* EUR operations: `@ops_eur` (Type: `operations`, Asset: `EUR`)
* BRL operations: `@ops_brl` (Type: `operations`, Asset: `BRL`)
Use segments and account types to track departmental expenses.
**Example**:
* Engineering expenses: `@eng_expenses` (Type: `expense`, Segment: `Engineering`)
* Marketing expenses: `@mkt_expenses` (Type: `expense`, Segment: `Marketing`)
* Consolidate with parent segment for organization-wide reporting
## Account type validation
***
Account type validation makes each account follow your organization's financial structure.
### Enabling Account type validation
Account type validation is a per-ledger setting. To turn it on in the Console: on the **Ledgers** page, open the Ledger's **Details**, click the **Settings** tab, and toggle on **Validate Account Type**.
As a programmatic alternative, send a `PATCH` request to `/v1/organizations/{organization_id}/ledgers/{ledger_id}/settings` with this body:
```json theme={null}
{
"accounting": {
"validateAccountType": true
}
}
```
When you enable account type validation, each new account must have a registered account type. Update existing accounts that have no type.
For more information about managing account types, see [Managing Account Types](/en/midaz/console/managing-account-types).
## Best practices
***
Name accounts clearly and use aliases for easy reference (e.g., `@customer_john_checking` instead of UUID references).
Enable account type validation to enforce your chart-of-accounts structure and prevent misclassification.
Group related accounts into portfolios for better reporting and management, especially for customer relationships.
Use consistent asset code conventions (ISO 4217 for currencies) to simplify multi-currency operations.
Use metadata fields to store additional context like customer IDs, department codes, or integration references.
Accounts are the foundation of your ledger. A clear account structure improves your financial operations and reporting.
# Managing Assets
Source: https://docs.lerian.studio/en/midaz/console/managing-assets
Manage the Assets registered in your Ledger from the Midaz Module — create, edit, and delete the currencies, tokens, or commodities you use.
You can manage the Assets associated with your Ledger directly from **Midaz Module**.
## Accessing the Asset page
***
To open the **Assets** page, select the **Assets** option in the **Foundation** section of the left sidebar menu.
The **Assets** page will display a list of **Assets** created for the current Ledger.
### Available actions
From the **Assets** page, you can:
# Managing Balances
Source: https://docs.lerian.studio/en/midaz/console/managing-balances
Manage Account Balances from the Lerian Console — list, read, and create balances, and control sending and receiving permissions without leaving the interface.
Use the **Midaz Module** to manage the **Balances** in your Accounts. A balance tracks one asset in one account. It records the available amount and the on-hold amount.
## What you can do
* **List balances** for an account to see the available and on-hold amounts per asset.
* **Read a balance** to inspect its current state and metadata.
* **Create an additional balance** for an account under a new balance key. The balance inherits the account's asset.
* **Update a balance** to toggle whether it can send or receive funds (**Allow Sending** and **Allow Receiving**).
Overdraft limits are managed through the update-balance API, not the Console.
The Console UI does not support balance history or balance deletion. This page does not cover them.
# Managing Alias Accounts
Source: https://docs.lerian.studio/en/midaz/console/managing-crm-alias-accounts
Manage Alias Accounts in the Midaz Module — create, edit, and delete customer-facing account records that link Holders to their underlying Accounts.
Use **Midaz Module** to manage **Alias Accounts**. Alias Accounts create the link between Holders and Midaz ledger accounts, adding business context like bank account numbers and financial institution details.
## What are Alias Accounts?
***
An **Alias Account** connects a Holder (individual or company) to a specific Midaz ledger account. In Midaz Module, each alias account stores:
* **Account identification** - Bank ID, branch, account, and type
* **Holder reference** - Link to the associated holder (Natural or Legal Person)
* **Ledger account link** - Connection to the underlying Midaz account
* **Banking details** - Opening date, IBAN, country code, and the Participant Document under Regulatory
* **Custom metadata** - Additional fields for integration and business-specific needs
Alias Accounts enable you to maintain customer-facing account information separately from the transactional ledger, providing flexibility for multi-bank scenarios and external system integration.
For technical details about the underlying CRM capabilities, refer to the [CRM Overview](/en/midaz/crm/crm-overview) page.
## Accessing the Alias Accounts page
***
To open the **Alias Accounts** page, select the **Alias** option from the Accounts section of the left-side menu.
The **Alias Accounts** page displays a table listing all Alias Accounts in your system, including:
* **Holder Name** - The associated holder name
* **Holder ID** - The identifier of the associated holder
* **Account ID** - The identifier of the linked ledger account
* **Ledger** - The ledger the linked account belongs to
* **Type** - Classification of the account (checking, savings, etc.)
* **Actions** - The actions available for each alias account
If you do not have any Alias Accounts, the list will appear empty with a prompt to create your first alias account.
### Available actions
From the **Alias Accounts** page, you can:
Link a holder to a Midaz ledger account with bank details
Update account information and financial details
Remove the link between a holder and ledger account
## Common use cases
***
When a customer opens an account:
1. Create a Holder with customer information (Natural or Legal Person)
2. Create an Alias Account linking the holder to a Midaz ledger account
3. Add banking details (bank ID, branch, account, type)
4. The customer can now transact using their familiar bank account number
When customers have accounts at multiple institutions:
* Create multiple Alias Accounts for the same Holder
* Each alias account references a different Midaz ledger account
* Track different account numbers and bank relationships
* Maintain clear separation between institutional relationships
When integrating with external banking systems:
* Use Alias Accounts to map external account numbers to internal ledger accounts
* Store regulatory identifiers such as the Participant Document
* Maintain metadata for integration-specific fields
* Synchronize status changes between systems
## Best practices
***
Always create the Holder record first, then create Alias Accounts to link them to ledger accounts. This ensures proper data relationships.
Follow your institution's account numbering standards when creating alias accounts to avoid confusion and integration issues.
Ensure Bank IDs and other banking details are correct for proper payment routing and regulatory compliance.
Use metadata to store additional context about account relationships, especially in multi-bank or integration scenarios.
Update alias account status to reflect changes in the account relationship (active, inactive, closed).
# Managing Holders
Source: https://docs.lerian.studio/en/midaz/console/managing-crm-holders
Manage Holders in the Midaz Module — create, edit, and delete individuals or companies with their identity, contact, and address information.
Use the **Midaz Module** to manage **Holders**. Holders represent the people or organizations behind your accounts. In the console, you create, view, edit, and manage their information.
## What are Holders?
***
A **Holder** represents an individual (Natural Person) or a company (Legal Person) linked to your Midaz accounts. Each holder stores:
* **Identity information** - Name, document number (CPF/CNPJ), and an optional free-text status
* **Contact details** - Email addresses and phone numbers for communication
* **Address information** - Primary and additional addresses with complete location data
* **Person-specific data** - Personal details for individuals or business information for companies
* **Custom metadata** - Additional fields for integration and business-specific needs
You manage Holders in the CRM area of the Midaz Module. Holders connect to ledger accounts through Alias Accounts. This keeps customer data separate from transactional operations.
For technical details about the underlying CRM capabilities, refer to the [CRM Overview](/en/midaz/crm/crm-overview) page.
## Accessing the Holders page
***
To open the **Holders** page, select the **Holders** option from the Accounts section of the left-side menu.
The **Holders** page shows a table with all Holders in your system.
### Available actions
From the **Holders** page, you can:
Add new individuals or companies as Holders
Update holder information, contact details, and status
Remove holders (soft delete). Hard delete is available only through the API (`?hard_delete=true`)
## Understanding Holder types
***
When you create a Holder, you select the holder type. The type determines which fields you can manage:
### Natural Person (Individual)
For individual customers, the console shows fields for:
* **Basic information**: Full name, document (CPF), contact details
* **Personal details**: Favorite name, social name, gender, birth date
* **Family information**: Civil status, mother's name, father's name
* **Demographics**: Nationality
### Legal Person (Company)
For corporate customers, the console shows fields for:
* **Basic information**: Company name, document (CNPJ), contact details
* **Business details**: Trade name, company activity, founding date
* **Structure**: Company size, company type, business status
* **Legal representation**: Representative name, document, role, contact
## Managing Holder status
***
**Status** is an optional free-text field on the holder form. Use it to record any classification your business needs, such as `active` or `under-review`.
To change the status, edit the holder. The holders list shows the **Name**, **ID**, **Type**, and **Primary E-mail** columns; open a holder to see its status.
## Common use cases
***
To onboard individual customers in the console:
1. Create a Natural Person holder with personal details
2. Add primary address and contact information
3. Link to checking/savings accounts via Alias Accounts
4. Use metadata for customer-specific attributes (loyalty tier, risk profile, etc.)
To manage corporate accounts in the console:
1. Create a Legal Person holder with company information
2. Add legal representative details for compliance
3. Include business addresses and contact points
4. Link to corporate treasury and operational accounts
5. Track company size and activity sector for reporting
Use the External ID field in the console to keep systems in sync:
* Map each holder to your existing CRM using External ID
* Keep document numbers consistent across systems
* Use metadata for integration-specific fields
* Update status in the console to reflect external system changes
## Best practices
***
Select Natural Person for individuals and Legal Person for companies. You cannot change the type later. The type sets which fields the console forms show.
Document numbers (CPF, CNPJ) must be unique and correctly formatted. The document field is required, but the console does not validate its format — verify the numbers yourself for regulatory compliance.
Update email addresses and phone numbers in the console regularly. This keeps customer communication effective.
When you create a holder, fill in the External ID field. It maps the holder to your existing customer database.
Use the Metadata step in the holder form to store extra information. You do not change the core structure.
If you use the optional status field, define a consistent set of values for your team and update it to reflect customer relationship changes before deletion.
# Managing Ledgers
Source: https://docs.lerian.studio/en/midaz/console/managing-ledgers-via-console
Manage Ledgers in your Organization from the Midaz Module — create, edit, and delete them, and enable Validate Account Type or Validate Routes.
A Ledger groups the Accounts and Transactions in your Organization. You manage these Ledgers from the **Midaz Module**.
## Current Ledger
***
The name of the current Ledger appears in the header of the home page (*Figure 1*).
## Accessing the Ledgers page
***
To open the **Ledgers** page, click the **Ledgers** option in the **Foundation** section of the left sidebar menu.
The **Ledgers** page lists every Ledger in your Organization. The current Ledger appears selected.
### Available actions
From the **Ledgers** page, you can:
## Ledger settings
***
Each Ledger has a **Settings** tab where you can enable validation rules for Accounts and Transactions.
On the **Ledgers** page, find the Ledger you want to configure. Click the three dots () in the **Actions** column. Select **Details**.
In the ledger details sheet, click the **Settings** tab.
Enable or disable **Validate Account Type** and **Validate Routes** for this Ledger.
### Available settings
| Setting | What it controls | When to enable |
| --------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| Validate Account Type | Requires new Accounts to use an Account Type registered for this Ledger. | Enable when Accounts must follow a controlled classification model. |
| Validate Routes | Requires Transactions to match configured Accounting Routes. | Enable after your Accounting Routes are planned or configured. |
Do not enable **Validate Routes** before your Accounting Routes exist. A Transaction that does not match a route fails validation.
# Managing the Metadata Index
Source: https://docs.lerian.studio/en/midaz/console/managing-metadata-index
Manage the Metadata Index from the Lerian Console — list, create, and remove indexed metadata keys to speed up searches across your Midaz entities.
Use the **Midaz Module** to manage the **Metadata Index**. Metadata indexes let you efficiently query entities by the custom key/value metadata you attach to organizations, ledgers, accounts, transactions, and other resources.
## What you can do
* **List indexes** to see which metadata keys have an index.
* **Create an index** on a metadata key to speed up lookups and filters on that key.
* **Remove an index** when you no longer query that metadata key.
Create an index for a supported entity and metadata key. `unique` defaults to `false`; `sparse` defaults to `true`.
Keep the index focused on the keys you actually query. This keeps search fast and predictable.
# Viewing Operations
Source: https://docs.lerian.studio/en/midaz/console/managing-operations
View the Operations that make up a Transaction in the Lerian Console — inspect debit, credit, hold, and release movements per account and their metadata.
Use the **Midaz Module** to review the **Operations** that compose each Transaction. Operations are individual balance movements. Depending on the transaction lifecycle, their type can be `DEBIT`, `CREDIT`, `ON_HOLD`, or `RELEASE`.
## What you can do
* **View operation details** to see the source or destination account, asset, amount, and resulting balance impact, along with any metadata.
The operation details sheet is read-only. To update operation metadata, use the update-operation API.
Operations always belong to a parent Transaction. You open them from the transaction detail view or from the operations list on the account details page.
# Managing Organizations
Source: https://docs.lerian.studio/en/midaz/console/managing-organizations
Manage Organizations in the Midaz Module — create, edit, delete, and switch between the top-level entities that own your Ledgers and Accounts.
The Midaz module lets you manage your Organizations directly. You can create, edit, delete, and switch between them.
## Accessing the Organizations page
***
To open the **Organizations** page, click the **Organizations** option in the **Foundation** section of the left sidebar menu.
The **Organizations** page lists all your Organizations in one place.
### Available actions
From the **Organizations** page, you can:
## Multiple Organizations
***
If you have more than one Organization, you can also open the **Organizations** page from the navigation bar. Click the Organization icon in the left navigation bar. Then select **Organizations** in the pop-up window (*Figure 1*).
### Changing Organizations
To work on a different Organization, use the same pop-up window. Select the cog icon ().
The **Organizations** page opens. Find the Organization you want. Click the three dots () in the **Action** column. Then select **Switch to this organization**.
# Managing Portfolios
Source: https://docs.lerian.studio/en/midaz/console/managing-portfolios
Manage Portfolios from the Lerian Console — create, edit, and delete the containers that group related Accounts under a customer or business unit.
Use **Midaz Module** to manage **Portfolios** that group your accounts. A Portfolio gives your accounts a higher-level structure. You manage a set of accounts as a single unit.
## What are Portfolios?
***
A **Portfolio** is a container that groups related accounts within a ledger. In Midaz Module, portfolios enable you to:
* **Group related accounts** - Organize accounts that belong together logically or operationally
* **Associate with external entities** - Link portfolios to external system identifiers via Entity ID
* **Simplify account management** - Manage multiple accounts as a cohesive unit
* **Add custom context** - Store additional information through metadata for integration and reporting
Portfolios are ledger-specific. Each ledger has its own set of portfolios for your needs.
For technical details about portfolios in Midaz, refer to the [Portfolios documentation](/en/midaz/portfolios).
## Accessing the Portfolios page
***
To open the **Portfolios** page, select the **Portfolios** option from the Accounts section of the left-side menu.
The **Portfolios** page displays a table of all Portfolios in your Ledger.
If you have no Portfolios, the list is empty and shows a prompt to create your first Portfolio.
### Available actions
From the **Portfolios** page, you can:
Add a new portfolio to group your accounts
Update portfolio name and metadata
Remove a portfolio from your ledger
## Common use cases
***
Create portfolios to group all accounts belonging to a single customer:
* Portfolio: `Customer-12345` with Entity ID linking to your CRM
* Contains: Checking account, savings account, investment accounts
* Use metadata for customer tier, relationship manager, onboarding date
Organize accounts by financial product:
* `Checking Accounts Portfolio` - All customer checking accounts
* `Savings Accounts Portfolio` - All customer savings accounts
* `Business Accounts Portfolio` - All commercial/business accounts
* `Investment Accounts Portfolio` - All investment-related accounts
Group accounts by institution or partner:
* `Partner Bank A` - Accounts related to Partner Bank A operations
* `Partner Bank B` - Accounts for Partner Bank B integration
* Use Entity ID to link to partner identifiers in your system
Create portfolios for operational purposes:
* `Treasury Operations` - Internal treasury and liquidity accounts
* `Settlement Accounts` - Accounts used for settlement operations
* `Suspense Accounts` - Temporary holding accounts for reconciliation
* `Fee Collection` - Accounts for collecting and managing fees
## Best practices
***
Design your portfolio structure before you create accounts. Decide how to group the accounts and which Entity IDs to use for external systems.
Set the Entity ID during creation when a portfolio links to an external system (CRM, core banking). External systems can then match the portfolio to their own records.
Use portfolio names that clearly indicate the purpose or contents of the portfolio. This helps team members quickly understand the account organization.
Use the Metadata tab to store extra details. Add business unit codes, cost centers, or integration references that do not fit in the standard fields.
Each portfolio should have a clear purpose. Do not create catch-all portfolios. Instead, create a specific portfolio for each use case.
## Portfolios vs Segments
***
Both Portfolios and Segments help organize accounts, but they serve different purposes:
| Feature | Portfolio | Segment |
| ---------------- | ----------------------------------------- | ----------------------------------------- |
| **Purpose** | Group related accounts as a unit | Classify accounts by category |
| **Entity ID** | Yes - links to external entities | No |
| **Use Case** | Customer accounts, product grouping | Departmental, regional classification |
| **Relationship** | One-to-many (portfolio contains accounts) | One-to-many (segment classifies accounts) |
You can use portfolios and segments together to organize accounts. For example, accounts in a customer portfolio can also belong to a segment (checking, savings, investment).
# Managing Segments
Source: https://docs.lerian.studio/en/midaz/console/managing-segments
Manage Segments from the Console — create, edit, and delete the classifications used to group Accounts by department, region, or business function.
Use **Midaz Module** to manage **Segments** that help you organize and categorize your accounts. Segments provide a way to group accounts by business purpose, department, region, or any other classification relevant to your organization.
## What are Segments?
***
A **Segment** is an organizational unit that helps you classify and group accounts within a ledger. In Midaz Module, segments enable you to:
* **Organize accounts by purpose** - Group accounts by business function, department, or product line
* **Create reporting hierarchies** - Build structures for financial reporting and analysis
* **Apply business classifications** - Use segments to filter and analyze accounts by classification
* **Maintain flexibility** - Add custom metadata for integration and business-specific needs
Segments are ledger-specific, meaning each ledger can have its own set of segments tailored to its organizational structure.
For technical details about segments in Midaz, refer to the [Segments documentation](/en/midaz/segments).
## Accessing the Segments page
***
To open the **Segments** page, select the **Segments** option on the Accounts section of the left-side menu.
The **Segments** page displays a table listing all Segments in your Ledger.
If you do not have any Segments, the list will appear empty with a prompt to create your first segment.
### Available actions
From the **Segments** page, you can:
Add a new segment to organize your accounts
Update segment name and metadata
Remove a segment from your ledger
## Common use cases
***
Create segments to represent different departments or cost centers:
* `Finance` - For treasury and financial operations accounts
* `Operations` - For operational and transactional accounts
* `HR` - For payroll and employee-related accounts
* `Sales` - For revenue and customer-related accounts
Organize accounts by product or service offering:
* `Checking Products` - All checking account-related segments
* `Savings Products` - Savings and investment account segments
* `Loans` - Credit and lending product segments
* `Cards` - Credit and debit card operation segments
Group accounts by geographic region:
* `North Region` - Accounts for northern operations
* `South Region` - Accounts for southern operations
* `International` - Cross-border and international accounts
Create segments for regulatory and reporting purposes:
* `Regulatory Reserve` - Accounts subject to reserve requirements
* `Audit Trail` - Accounts requiring special audit tracking
* `Tax Reporting` - Accounts with specific tax reporting needs
## Best practices
***
Design your segment hierarchy before creating accounts. A well-planned structure makes account management and reporting easier.
Choose segment names that clearly indicate their purpose. This helps team members understand account organization at a glance.
Use the Metadata tab to store additional information like cost center codes, department IDs, or integration references.
If you have multiple ledgers, consider using consistent segment naming conventions for easier cross-ledger reporting.
Periodically review your segment structure to ensure it still aligns with your organizational needs.
## Segments and transaction rules
***
Segments classify accounts for organization, filtering, and reporting — they do not control transaction flows. Route validation in [Accounting Routes](/en/midaz/console/managing-accounting-routes) supports only account types and aliases; there is no segment-based routing. Use segments to keep your account structure organized and to slice reports by department, region, or product line.
# Managing Transactions
Source: https://docs.lerian.studio/en/midaz/console/managing-transactions
Manage Transactions from the Midaz Module — create new Transactions between Accounts and inspect approved or canceled records in your Ledger.
Use the **Midaz Module** to view, create, and monitor **Transactions** in a visual interface. The Transactions page shows every financial movement in your Ledger. You can track balances, review operations, and create transactions without writing code.
See the [Transactions](/en/midaz/transactions) reference documentation for transaction structures, double-entry accounting, N:N transactions, and API integration.
## Accessing the Transactions page
***
Select the **Transactions** option in the left-side menu to open the **Transactions** page.
The **Transactions** page shows a table of all Transactions in your Ledger.
If your Ledger has no Transactions, the list is empty and shows a prompt to create your first transaction.
### Available actions
From the **Transactions** page, you can:
Record a new asset movement between accounts
See complete transaction information and operations
## Transaction status
***
Transactions in Midaz have the following statuses:
* **CREATED** - The initial state of a transaction
* **PENDING** - A two-step transaction holding funds, awaiting commit or cancel
* **APPROVED** - Midaz posted the transaction
* **CANCELED** - Midaz canceled the transaction
After Midaz approves a transaction, you cannot edit its operations or amounts, and you cannot delete it. To reverse it, create a reversal transaction. This protects the integrity of your financial records.
## Common use cases
***
A basic transaction moving funds from one account to another:
* **Source**: Customer checking account (debit \$100)
* **Destination**: Merchant account (credit \$100)
* Single source, single destination, equal amounts
A transaction that splits funds between multiple destinations:
* **Source**: Customer account (debit \$103)
* **Destinations**:
* Merchant account (credit \$100)
* Fee collection account (credit \$3)
* Single source, multiple destinations
Multiple sources funding a single destination:
* **Sources**:
* Primary account (debit \$500)
* Secondary account (debit \$300)
* **Destination**: Vendor account (credit \$800)
* Multiple sources, single destination
Multiple sources and destinations in a single transaction:
* **Sources**:
* Customer A (debit \$200)
* Customer B (debit \$300)
* **Destinations**:
* Vendor account (credit \$450)
* Tax account (credit \$50)
* Multiple sources, multiple destinations
## Best practices
***
Include a clear description of the purpose of each transaction. This helps auditing and troubleshooting.
Make sure the source account has enough balance before you create a transaction. This avoids rejections.
Store external reference IDs, order numbers, or other system identifiers in the transaction metadata for traceability.
When possible, combine related transfers into one transaction instead of several separate transactions.
Double-check the source accounts, destination accounts, and amounts before you submit. You cannot edit a transaction after you create it.
## Accounting Routes integration
***
Midaz can validate Transactions against [Accounting Routes](/en/midaz/console/managing-accounting-routes) before it processes them. Accounting Routes define which accounts can transact with each other. They add a layer of control over your financial operations.
# Accounting rules
Source: https://docs.lerian.studio/en/midaz/console/mc-accounting
Learn how Accounting Routes decide which Accounts join a transaction and how debit and credit entries are posted for each transaction type in Midaz.
The **Accounting** section is where you define optional rules for transactions.
You do this through **Accounting Routes**. An Accounting Route is a reusable rule for one kind of transaction, such as a *Pix transfer*, a *card purchase*, or a *fee charge*. Each route answers three questions:
* Which accounts can act on the source side?
* Which accounts can act on the destination side?
* Which debit and credit entries does the route post when the transaction runs?
**Example.** A `Pix transfer` route can require a `customer` account on the source side and a `merchant` account on the destination side. The route then defines direct debit-and-credit entries between them. When route validation is enabled, a Pix transaction that references the route validates the accounts and applies the configured rules.
Accounting Routes provide optional route validation. Midaz enforces them only when the Ledger setting `accounting.validateRoutes` is enabled (default `false`). When enabled, transactions must reference valid routes and operations must match configured rules. Routes are easier to model when you already know which accounts represent customers, treasury, fees, settlement, revenue, and expenses.
## How the pieces fit together
***
| Piece | What it controls | Example |
| ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- |
| Accounting Route | The transaction-level route that groups the operation rules. | `Pix transfer route` |
| Operation Route | The account side and validation rule. | `Source must be customer`, `Destination must be merchant` |
| Validation rule | How Midaz decides whether an account can be used. | Account Type `customer` or alias `@treasury_main` |
| Accounting scenario | Which debit and credit entries are posted for the transaction lifecycle. | Direct, Two-Step, Reversal, Overdraft, Block/Unblock |
## Choosing the operation type
***
Use **Source** for accounts where value originates.
Example: a customer account can send funds in a payment flow.
Use **Destination** for accounts where value lands.
Example: a merchant account can receive funds in a payment flow.
Use **Bidirectional** when the same account class can send and receive.
Example: checking accounts can transfer value to other checking accounts.
A route must include a Source and a Destination operation route, or at least one Bidirectional operation route.
## Choosing the validation rule
***
| Validation type | Use when | Example |
| --------------- | --------------------------------------- | -------------------------------- |
| Account Type | Any account in a class should be valid. | Any `customer` account can send. |
| Alias | Only one exact account should be valid. | Only `@treasury_main` can send. |
An Operation Route can optionally include an account rule—registered Account Types or an `@Alias`. Midaz enforces a present rule when route validation is enabled. Use Account Type validation for scalable flows. Use alias validation for fixed operational accounts, such as treasury, fee, settlement, or suspense accounts.
## Common route patterns
***
### Customer to merchant
Use separate Source and Destination routes when each side has a different role.
| Operation route | Validation |
| --------------- | ----------------------- |
| Source | Account Type `customer` |
| Destination | Account Type `merchant` |
### Peer-to-peer transfer
Use a Bidirectional route when the same account type can be both source and destination.
| Operation route | Validation |
| --------------- | ----------------------- |
| Bidirectional | Account Type `customer` |
### Fee collection
Use a Destination route with alias validation when fees must always land in one operational account.
| Operation route | Validation |
| --------------- | ----------------------- |
| Source | Account Type `customer` |
| Destination | Alias `@fee_revenue` |
## Accounting scenarios
***
| Scenario | Use when | What the user configures |
| -------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Direct Transaction | The movement is executed in one step. | Debit and credit entries for immediate posting. |
| Two-Step Transaction | The movement has hold, commit, and cancel phases. | Entries for reservation, confirmation, and cancellation. |
| Reversal | A completed transaction may need to be reversed. | Debit and credit entries for the reversal event. |
| Overdraft | A debit may exceed the account's available funds, drawing on an overdraft line. | Debit and credit entries for the overdraft draw and later repayment. |
| Block/Unblock | Funds need to be held and later released on the account's balance. Available for all route types (tab label "Block"). | Entries for the block (hold) and unblock (release) events. |
Do not enable route validation in Ledger settings until the required routes exist. If you enable it without matching routes, transactions fail validation.
## Available pages
***
A step-by-step, Console-first guide to building your accounting model — from chart of accounts to a working Pix payment, no API calls required.
Configure accounting routes with operation rules and accounting scenarios in a unified wizard.
# Account management
Source: https://docs.lerian.studio/en/midaz/console/mc-accounts
Explore how Accounts, Holders, Aliases, Segments, and Portfolios work together in the Midaz Module to organize your financial structure and ownership.
The **Accounts** section is where you open the accounts that hold balances, identify who owns them, and group them for reporting and control. Accounts can be linked to a Holder, Segment, and Portfolio. These persisted relationships support ownership, grouping, fee resolution, and scoped rules; they are not merely tags.
You will work with the following here:
* **Account** — The record that holds a balance of one asset and can send or receive that asset through transactions (for example, a customer's BRL account).
* **Holder** — The individual or entity that owns one or more accounts (for example, a customer or a merchant registered in CRM).
* **Alias Account** — An optional CRM Instrument linked to a Holder and an existing ledger Account. It stores banking details. This is different from the account's own **alias** field, a friendly identifier on the account itself that lets you refer to it by name instead of by ID (for example, `@treasury_main`).
* **Segment** — An optional ledger-scoped grouping for Accounts that can scope fee selection and Tracer limits.
* **Portfolio** — A ledger-scoped grouping of Accounts with an optional external `entityId`; it is not a direct Holder relationship.
## Available pages
***
Create and configure the accounts that hold and move assets within your ledgers.
Register the individuals or entities that own accounts in your system.
Link holders to ledger accounts with banking details.
Organize accounts into logical groupings for reporting and control.
Group accounts into portfolios for consolidated management.
# Foundation elements
Source: https://docs.lerian.studio/en/midaz/console/mc-foundation
Set up the Foundation of the Midaz Module — Organizations, Ledgers, Assets, and Account Types that every downstream Account and transaction depends on.
The **Foundation** section is the structure that the rest of Midaz builds on. Set up an Organization, Ledger, and Asset before opening an Account. Define Account Types when your configuration needs them; you can create and update Foundation resources as the environment evolves.
You will work with four elements here:
* **Organization** — The top-level entity that owns one or more ledgers. Usually represents your company or business unit (for example, *Acme Bank*).
* **Ledger** — The isolated book of records where accounts, balances, and transactions live. Use one or more ledgers according to your product and operational needs.
* **Asset** — A currency or instrument that accounts can hold and move, identified by a code (for example, `BRL`, `USD`, or `USDC`). Each account holds one asset.
* **Account Type** — An optional label that classifies accounts when your configuration needs it (for example, `customer`, `treasury`, `fee`, or `revenue`).
## Available pages
***
Create and configure the top-level entities that group your ledgers and assets.
Set up ledgers to organize and track your financial operations.
Register the currencies and instruments used across your ledgers.
Define the categories that classify your accounts within each ledger.
# Transaction basics
Source: https://docs.lerian.studio/en/midaz/console/mc-transactions
Use the Midaz Module to record Transactions that move Assets between Accounts with double-entry accounting, N:N support, and a full audit trail.
Use **Midaz Module** to manage **Transactions** that record the movement of assets between accounts. Transactions are the core of your ledger operations. They record every financial movement in your system.
## What are Transactions?
***
A **Transaction** represents the transfer of assets from one or more source accounts to one or more destination accounts. In Midaz Module, transactions:
* **Move assets between accounts** - Record value movements through one or more Operations
* **Support multiple sources and destinations** - Handle complex transactions with multiple parties
* **Track status** - Monitor the transaction lifecycle (`CREATED`, `APPROVED`, `PENDING`, `CANCELED`, or `REVERTED`)
* **Maintain audit trail** - Record complete history with timestamps and metadata
* **Support accounting controls** - When enabled, Accounting Routes validate configured rules for the Transaction
A transaction is immutable. Once Midaz records the movement, you cannot change or delete it. You can still update the transaction description and metadata, cancel a pending transaction, or view it at any time.
For technical details about transactions in Midaz, refer to the [Transactions documentation](/en/midaz/transactions).
## Available pages
***
Create Transactions and inspect their details after processing.
# Concepts map
Source: https://docs.lerian.studio/en/midaz/console/midaz-console-concepts-map
Understand how Organizations, Ledgers, Assets, Accounts, Routes, and Transactions depend on each other before you start configuring the Midaz Module.
The Midaz Module uses a small set of core objects with explicit dependencies. Organization, Ledger, Assets, and Accounts are the core setup; Account Types, CRM records, Portfolios, Segments, and Accounting Routes are configured when the workflow needs them.
## Core hierarchy
***
### What each object means
***
| Object | What it represents | Why it exists first or later |
| ------------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `Organization` | The legal or operational owner of the setup | It is the top-level boundary. Nothing below exists without it. |
| `Ledger` | The accounting environment where balances and movements are recorded | It groups the structure that a team will operate day to day. |
| `Asset` | The unit of value moved in the ledger, such as `BRL` or `USD` | Accounts and transactions depend on it. |
| `Account` | A balance container that can send or receive value | Transactions happen between accounts. |
| `Account Type` | An optional classification such as `checking`, `treasury`, or `expense` | Use it when configured account or route rules need it. |
| `Portfolio` | A grouping of accounts, often by customer or wallet structure | Useful for reporting and organization, but not a prerequisite for every account. |
| `Segment` | A ledger-scoped persisted grouping of accounts, such as by department, region, or tier | Useful when the business needs slicing or policy grouping. |
| `Holder` | The person or entity that owns accounts in CRM flows | Needed when account ownership matters operationally. |
| `Alias` | A human-readable identifier used to find or refer to an account | Especially important in transaction creation. |
| `Accounting Route` | An optional rule set for transaction participants and posting behavior | Midaz applies configured route rules when route validation is enabled. |
| `Transaction` | The actual balance movement recorded in the ledger | It is the output of the whole setup. |
## How a transaction actually depends on prior setup
***
The required prerequisites depend on the Transaction and Ledger settings. Assets and Accounts are required; Accounting Routes are required only when route validation is enabled.
## Practical reading of the UI
***
Use this interpretation when you guide users:
• `Foundation` defines where the system lives: Organization, Ledger, Asset.
• `Accounting` defines optional participation and posting rules: Account Types and Accounting Routes.
• `Accounts` defines who holds balances and how you classify them: Accounts, Holders, Aliases, Segments, Portfolios.
• `Transactions` exercises the configured structure.
• `Plugins` extend the model after the core behavior already works.
## When users usually get lost
***
The Account is the balance container. The Holder is the CRM identity. An Alias Account is an optional CRM Instrument linked to a Holder and an existing Ledger Account; the Account's own `alias` is its human-friendly identifier.
Account Type is an optional classification. Segment and Portfolio are ledger-scoped persisted groupings of Accounts; they are not merely labels or direct Holder relationships.
The Route is an optional reusable rule. The Transaction is the individual movement; it is checked against configured route rules only when route validation is enabled.
Almost every object below it is ledger-scoped. Users who treat the ledger as a cosmetic container usually misconfigure accounts and routes later.
Pair this page with [Setup path](/en/midaz/console/midaz-console-setup-path) when you onboard new users. One page explains the model. The other explains the order.
# Onboarding
Source: https://docs.lerian.studio/en/midaz/console/midaz-console-onboarding
Follow the Midaz Module guided onboarding to create your first Organization and Ledger in a few minutes, with fields explained at every step.
The Midaz Module guides you through a step-by-step onboarding flow. It helps you set up your Organization and create your first Ledger.
## Onboarding process
***
The onboarding starts automatically when you have no Organization. This happens when you never created one, or when you deleted all your Organizations.
This flow guides you through the initial setup. At each step, you see where you are and why the step matters.
In a few minutes, your Organization and your first Ledger are ready. You need both to access the full platform.
### Starting the process
Click the **Let's go** button on the first-steps window.
## Part 1: Creating your Organization
***
The first part of onboarding creates your Organization in three steps.
### Step 1 – Set up your Organization details
This step is the foundation. You enter the key details of your Organization.
**Required field:**
* **Legal Name** – Your company's official registered name.
You can also provide a **Trade Name** (DBA) and a **Document** such as a CNPJ, EIN, or other registration number.
The **Document** is permanent. After you submit it, you cannot change it. You can still edit the Legal Name and Trade Name later in the Organization settings.
Click **Next** at the bottom of the screen.
### Step 2 – Add your Organization's address
This step adds the registered address of your Organization.
**Required fields:**
* **Address** – Street address (line 1)
* **Complement** – Additional address information (optional)
* **Country** – Select from the dropdown
* **State** – Select from the dropdown (based on country)
* **City** – Your city name
* **ZIP Code** – Postal code
You can update your address at any time in the Organization settings.
Click **Next** at the bottom of the screen.
### Step 3 – Add a theme (optional)
Upload your Organization's **Icon** to personalize the Midaz Module.
**Icon format:**
* SVG or PNG file
* 256x256 px
When you submit this step, Midaz creates your Organization and returns a unique `organizationId`. Your icon becomes part of the Organization metadata.
You can skip this step and add the icon later. A confirmation dialog asks if you want to finish without a theme.
Click **Finish** to continue.
### Organization created
Your Organization is ready. You see a confirmation dialog. Click **Continue** to create your first Ledger.
## Part 2: Creating your first Ledger
***
After Midaz creates your Organization, the flow guides you to create your first Ledger.
The Ledger is a core part of Midaz. It is where you manage Accounts, balances, and Transactions. Create Assets separately for the Organization, then use them when you create Accounts.
**Required field:**
* **Ledger Name** – A unique name for your Ledger.
You can configure additional metadata later. An alert on the page reminds you of this option.
Enter a name for your Ledger. Click **Finish** to complete the setup.
## What's next?
***
With onboarding complete, you can explore everything the Midaz Module offers:
Edit your Organization settings and create additional Organizations.
Configure your Ledger and create new ones.
Define the currencies and tokens for your Organization.
Set up accounts to start managing balances.
# Setup path
Source: https://docs.lerian.studio/en/midaz/console/midaz-console-setup-path
Follow the recommended setup order for the Midaz Module so each Console screen has the Organization, Ledger, Assets, and Accounts it depends on.
The Midaz Module is easier to configure when you follow the dependency order the product expects. Most user confusion comes from trying to create Accounts, Routes, or Transactions before the structural pieces already exist.
## Recommended order
***
Start with the legal and operational owner of the environment.
Use [Midaz Module onboarding](/en/midaz/console/midaz-console-onboarding) if this is your first setup. If the organization already exists and you want to add another, go directly to [Creating an Organization](/en/midaz/console/creating-an-organization).
The Ledger is the workspace where balances, Accounts, and Transactions are scoped.
Go to [Creating a new Ledger](/en/midaz/console/creating-a-new-ledger).
Decide whether to enable Accounting Route validation before users start creating Transactions. This setting is optional and defaults to `false`.
You can configure these settings while creating the Ledger in [Creating a new Ledger](/en/midaz/console/creating-a-new-ledger#ledger-settings), or update them later from [Managing Ledgers](/en/midaz/console/managing-ledgers-via-console#ledger-settings).
Assets define a unit of value for the Organization, such as `BRL`, `USD`, or a custom token. Accounts reference an Asset when you create them.
Go to [Creating an Asset](/en/midaz/console/creating-an-asset).
Define Account Types when your configured Account or Route rules need those classifications.
Go to [Creating an Account Type](/en/midaz/console/creating-an-account-type).
Accounts hold balances and are the source or destination of transactions.
Go to [Creating an Account](/en/midaz/console/creating-an-account).
These are persisted support structures. Configure them when your workflow needs ownership context, grouping, fee resolution, or scoped rules.
Use:
* [Creating a Portfolio](/en/midaz/console/creating-a-portfolio)
* [Creating a Segment](/en/midaz/console/creating-a-segment)
* [Creating a Holder](/en/midaz/console/crm-creating-a-holder)
* [Creating an Alias Account](/en/midaz/console/crm-creating-alias-account)
Accounting Routes define optional participant and posting rules. Midaz applies them when route validation is enabled.
Go to [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route).
Create Transactions after the Organization has an Asset and the Ledger has the required Accounts. A matching Accounting Route is required only when route validation is enabled.
Go to [Creating a Transaction](/en/midaz/console/creating-a-transaction).
Capabilities such as Fees Engine extend behavior. Turn them on after the base ledger model is already clear.
Go to [Midaz module settings](/en/platform/console/plugin-settings).
## Fast path by goal
***
Use onboarding, then create at least one Asset and one Account.
Start with Ledger settings, Account Types, Accounts, and Segments.
Create the accounts first, then define Accounting Routes and test with a transaction.
Enable Fees Engine only after the core ledger structure is already in place.
## Common sequencing mistakes
***
You must link every Account to an Asset. If the Asset does not exist yet, account creation stalls immediately.
This makes the type field mandatory, but the dropdown remains empty. Create the Account Types first or disable validation until they are ready.
Users often try to model routes too early. In practice, route rules are easier to define after the treasury, customer, fee, and settlement accounts already exist.
Transaction creation depends on accounts being searchable and structurally valid. If operators cannot identify the source and destination accounts quickly, stop and fix the account model first.
When you document the setup or train a client team, teach this order first. The CRUD pages become much easier once the dependency chain is explicit.
# Troubleshooting
Source: https://docs.lerian.studio/en/midaz/console/midaz-console-troubleshooting
Fix common Midaz Module issues in Lerian Console — missing menus, empty dropdowns, blocked transactions, and Ledger validation setting mismatches.
Use this page when the Console does not show an option you expected, or when you cannot complete a form. Also use it when a transaction does not behave as you expect. Most issues come from missing prerequisites, or from Ledger settings you enabled before the supporting configuration existed.
## First checks
***
Before you troubleshoot a specific screen, confirm these basics:
* You selected the correct **Organization** and **Ledger**.
* The Ledger already has the required **Assets**.
* The Ledger settings match the workflow you want to use.
* The required **Account Types**, **Accounts**, and **Accounting Routes** exist before you create transactions.
* You use each account alias exactly as registered, with the `@` prefix.
Review the recommended setup order before you change individual screens.
## Common issues
***
The **Account Types** menu appears when you enable **Validate Account Type** in the current Ledger settings.
To fix it:
1. Open **Foundation > Ledgers**.
2. Open the target Ledger details.
3. Go to the **Settings** tab.
4. Enable **Validate Account Type**.
5. Return to the sidebar and open **Accounting > Account Types**.
After you enable this setting, new Accounts must use a valid Account Type. Create the required types before you ask operators to create accounts.
Each Account must link to an existing Asset. If no Asset exists in the selected Ledger, you cannot complete the Account form.
Create the Asset first in [Creating an Asset](/en/midaz/console/creating-an-asset), then return to [Creating an Account](/en/midaz/console/creating-an-account).
This usually means you enabled **Validate Account Type** but created no Account Types for the selected Ledger.
Create at least one Account Type in [Creating an Account Type](/en/midaz/console/creating-an-account-type). If your workflow does not need type validation, disable **Validate Account Type** in Ledger settings.
Transaction creation searches accounts by alias. Confirm that:
* The account exists in the selected Ledger.
* The alias includes the `@` prefix.
* The account allows the expected direction: source accounts can send, destination accounts can receive.
* The account Asset matches the Asset you selected for the Transaction.
If the alias is wrong or does not exist, open the account details from [Managing Accounts](/en/midaz/console/managing-accounts).
The **Validate Routes** setting controls route validation. When you enable it, transactions must match an existing Accounting Route.
Check that:
* The Accounting Route exists in the selected Ledger.
* The route has at least one valid source and one valid destination operation route, or one bidirectional route.
* Account Type or alias validation matches the accounts used in the transaction.
* The transaction type matches the accounting scenario configured in the route.
Start with [Managing Accounting Routes](/en/midaz/console/managing-accounting-routes) and then review [Creating an Accounting Route](/en/midaz/console/creating-an-accounting-route).
For complex transactions, the total you debit from source accounts must equal the total you credit to destination accounts.
Review every operation before you send the transaction. To model fees, include the fee account as an additional destination so the total credits match the total debits.
You cannot delete some entities after downstream records use them. For example, treat an Asset that Accounts use, or an Account that Transactions use, as part of the ledger history.
Instead, edit optional metadata, disable future use where the Console supports it, or create a replacement entity for new operations.
Start from the transaction details page and check:
* The source and destination accounts.
* The Asset used in the transaction.
* Each debit and credit operation.
* The transaction state: direct, pending, committed, cancelled, or reversed.
Then open the account balance and operation history for the accounts involved.
## API references
***
When a Console issue needs deeper inspection, use the corresponding API references:
Confirm whether the account exists in the selected Ledger.
Inspect account details such as type, asset, and metadata.
Review transaction history for a Ledger.
Inspect transaction status and operation details.
# About Midaz module
Source: https://docs.lerian.studio/en/midaz/console/midaz-module
Discover the Midaz Module in Lerian Console — manage Organizations, Ledgers, Accounts, Assets, and Transactions from a single visual interface.
**Midaz** is a module inside the [Lerian Console](/en/platform/console/about-lerian-console) that provides a visual interface for managing ledger infrastructure.
From a single screen, you create organizations and ledgers, register assets (such as BRL or USD), and open accounts. You define the rules that govern how money moves, and record transactions — all without writing API calls.
Use the Midaz Module for day-to-day operations: open an account, run a transaction, or check balances. You can also configure how the ledger validates and posts transactions.
**Important transition**: Midaz Console is now part of Lerian Console as the **Midaz Module**. All existing functionality remains available, along with new integrations and platform features.
## Start here first
***
New to Midaz? Start with these pages. They show what to create, in which order, and how the main components work together.
Follow the recommended setup order for Organizations, Ledgers, Assets, Accounts, Routes, and Transactions.
Learn how Midaz components connect and how they support financial operations.
Resolve common issues related to configuration, routes, permissions, and transactions.
## What you can do
***
From the Midaz Module, you can:
* **Manage organizations and ledgers** — Create the operational structure used by your financial environment
* **Configure assets** — Define currencies, tokens, and other asset types used across ledgers
* **Set up accounts** — Create accounts with account types, portfolios, and segments
* **Configure accounting routes** — Define how operations and transactions move assets between accounts
* **Monitor transactions** — Track financial movements across organizations and ledgers
* **Manage Holders and Alias Accounts** — Use the integrated CRM to register account owners and link them to ledger accounts
* **Access plugins** — Enable and configure capabilities such as Fees Engine from the [Settings](/en/platform/console/plugin-settings) page
## Recommended setup order
***
For a new environment, we recommend configuring Midaz in the following order:
Define the top-level structure that groups your ledgers and operations.
Create the ledger that will store balances, accounts, and transactions.
Adjust operational and accounting settings for the ledger.
Define the currencies or asset types that will circulate in the ledger.
Define how accounts behave within the financial structure.
Create the accounts that will hold balances and participate in transactions.
Organize accounts based on business or operational needs.
Define how transactions move assets between accounts.
Start recording and monitoring financial operations.
Activate optional platform capabilities such as Fees Engine.
For a more detailed explanation of each step, see [Setup path](/en/midaz/console/midaz-console-setup-path).
## Accessing Midaz
***
To open Midaz, select the **Midaz Module** from the Lerian Console product launcher (*Figure 1*).
## Available features
***
The sidebar follows the order in which most teams build their environment. You start with **Foundation**, then move to **Accounting**, **Accounts**, **Transactions**, and finally **Plugins**.
Configure Organizations, Ledgers, Assets, and Account Types that support your financial structure.
Configure Accounting Routes that control how assets move through the system.
Manage Accounts, Holders, Aliases, Segments, and Portfolios.
Create and monitor transactions that transfer assets between accounts.
Extend Midaz with integrated capabilities such as Fees Engine.
## Migrating from Midaz Console
***
If you're currently using the standalone **Midaz Console**, here's what changes when moving to Lerian Console.
The standalone Midaz Console is now in maintenance mode. It no longer receives feature updates. Only critical security fixes still apply.
We recommend migrating to Lerian Console to access the latest platform capabilities and integrations.
### Why migrate to Lerian Console?
* **Unified platform** — Access Midaz and other Lerian products from a single interface
* **New platform features** — Lerian releases new improvements and capabilities only in Lerian Console
* **Integrated workflows** — Connect Midaz with plugins and other platform modules
* **Simplified navigation** — Work with a consistent operational experience across products
### What stays the same?
* All core Midaz capabilities remain available
* Existing data and configurations continue to work
* Operational workflows remain familiar for current users
# Alias Accounts
Source: https://docs.lerian.studio/en/midaz/crm/alias-accounts
Link Holders to Midaz Ledger Accounts with Alias Accounts, adding banking, regulatory, and related-party context for CRM-driven features.
An **Alias Account** adds business context to a [Ledger Account](/en/midaz/accounts) in Midaz. It links a [Holder](/en/midaz/crm/holders) to a specific account in the ledger. It adds banking details, regulatory information, and related-party data to that account.
Without this link, CRM features that rely on account context do not work as expected.
For a step-by-step walkthrough of linking holders to accounts, see [Getting started with CRM](/en/midaz/crm/crm-getting-started).
## How it works
***
The Alias Account is a CRM-level representation of a Midaz Ledger Account. When you create an Alias Account, you provide the `ledgerId` and `accountId` that identify the target account in the ledger. The Alias Account inherits the `document` and `type` from its parent Holder automatically.
This design keeps customer-facing account details separate from the transactional ledger. Bank numbers, branch codes, and regulatory identifiers stay in the CRM, not in the ledger. The separation supports multi-bank setups and integration with external systems.
Always link an Alias Account to an existing Holder. Create the Holder first, then create the Alias Account. See [Using CRM](/en/midaz/crm/crm-using-overview) for the correct integration flow.
## Alias Account fields
***
### Core fields
| Field | Type | Required | Description |
| :------------ | :--------- | :--------------- | :---------------------------------------------------------------------------------------------------------------- |
| **id** | `uuid` | System-generated | Unique identifier of the Alias Account. |
| **holderId** | `uuid` | System-generated | The ID of the associated Holder (derived from the URL path). |
| **ledgerId** | `string` | Yes | The UUID of the Midaz Ledger. |
| **accountId** | `string` | Yes | The UUID of the Midaz Ledger Account. |
| **document** | `string` | System-generated | Inherited from the parent Holder. |
| **type** | `string` | System-generated | Inherited from the parent Holder (`NATURAL_PERSON` or `LEGAL_PERSON`). |
| **metadata** | `object` | No | Key-value pairs for custom, non-sensitive data. Keys are limited to 100 characters and values to 2000 characters. |
| **createdAt** | `datetime` | System-generated | Timestamp of creation (RFC 3339). |
| **updatedAt** | `datetime` | System-generated | Timestamp of last update (RFC 3339). |
| **deletedAt** | `datetime` | System-generated | Timestamp of soft deletion, if applicable (RFC 3339). |
### Banking details
The `bankingDetails` object stores information about the financial institution for the alias:
| Field | Type | Required | Description |
| :-------------- | :------- | :------- | :------------------------------------------------------------------ |
| **branch** | `string` | No | Bank branch code (e.g., `0001`). |
| **account** | `string` | No | Bank account number (e.g., `123450`). |
| **type** | `string` | No | Account type code (e.g., `CACC` for current account). |
| **openingDate** | `string` | No | Date the account was opened in `YYYY-MM-DD` format. |
| **closingDate** | `string` | No | Date the account was closed, if applicable, in `YYYY-MM-DD` format. |
| **iban** | `string` | No | International Bank Account Number. |
| **countryCode** | `string` | No | Country code of the financial institution (e.g., `US`, `BR`). |
| **bankId** | `string` | No | Identifier of the bank or financial institution. |
### Regulatory fields
The `regulatoryFields` object stores data that financial regulators require:
| Field | Type | Required | Description |
| :---------------------- | :------- | :------- | :--------------------------------------------------------------------------------- |
| **participantDocument** | `string` | No | Document number identifying the financial-group entity that owns the relationship. |
### Related parties
A related party is a person or entity linked to an Alias Account. Each related party has a defined role and a time-bounded relationship. Related parties represent the real-world people or organizations connected to the account, for ownership, legal authority, or operational accountability. Compliance, regulatory reporting, and CRM-driven workflows use them.
#### Roles
Each related party must have one of the following roles:
| Role | Description |
| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PRIMARY_HOLDER` | The main individual or entity who owns or holds the account. This is typically the customer themselves when account ownership differs from the Holder record. |
| `LEGAL_REPRESENTATIVE` | Someone with legal authority to act on behalf of the holder — for example, a legal guardian, attorney, or authorized representative. |
| `RESPONSIBLE_PARTY` | An entity responsible for the account in an operational or regulatory capacity, such as a compliance officer or a parent organization. |
#### Time-bounded relationships
Every related party relationship has a defined active period:
* **`startDate`** — Required. The date the relationship became active (`YYYY-MM-DD`).
* **`endDate`** — Optional. The date the relationship ended (`YYYY-MM-DD`). If you omit it, the relationship stays active. When you set it, `endDate` must be after `startDate`.
This design records who held a relationship with the account, and in what capacity. It keeps past relationships in the record.
#### Managing related parties
You manage related parties through the Alias Account endpoints. There are no standalone create or list endpoints:
* **Add on creation** — Include a `relatedParties` array in the [Create Alias Account](/en/reference/midaz/crm/create-alias-account) request body.
* **Add to existing** — Include a `relatedParties` array in the [Update Alias Account](/en/reference/midaz/crm/update-alias-account) request body. Midaz **appends** new entries to the existing list. It does not replace existing related parties.
* **Remove** — Use the [Delete Related Party](/en/reference/midaz/crm/delete-related-party) endpoint with the specific `related_party_id`.
* **List** — The Alias Account response returns related parties in the `relatedParties` array.
#### Fields
| Field | Type | Required | Description |
| :------------ | :------- | :--------------- | :----------------------------------------------------------------------------------------- |
| **id** | `uuid` | System-generated | Unique identifier of the related party. |
| **document** | `string` | Yes | Document number of the related party. Cannot be empty or whitespace. |
| **name** | `string` | Yes | Full name of the related party. Cannot be empty or whitespace. |
| **role** | `enum` | Yes | `PRIMARY_HOLDER`, `LEGAL_REPRESENTATIVE`, or `RESPONSIBLE_PARTY`. |
| **startDate** | `string` | Yes | Date the party relationship started in `YYYY-MM-DD` format. |
| **endDate** | `string` | No | Date the party relationship ended, if applicable. Must be after `startDate` when provided. |
Validation errors for related party fields return specific error codes: **CRM-0025** (invalid role), **CRM-0026** (document required), **CRM-0027** (name required), **CRM-0028** (start date required), **CRM-0029** (end date invalid — must be after start date). See the [CRM error reference](/en/reference/midaz/crm/crm-error-list) for details.
## Data security
***
Midaz **encrypts several Alias Account fields at rest**, including the inherited `document` field and banking details such as `account` and `iban`. Encryption protects sensitive financial data even if an attacker reaches the underlying storage.
Never store sensitive information in the `metadata` object. Midaz does **not** encrypt metadata, and stores it in plain text.
For the full list of protected fields and encryption strategies, see [CRM data security](/en/midaz/crm/crm-data-security).
## Managing Alias Accounts
***
### Via API
Use the CRM API to manage Alias Accounts programmatically:
* [Create an Alias Account](/en/reference/midaz/crm/create-alias-account) — Link a Holder to a Midaz Ledger Account.
* [List Alias Accounts](/en/reference/midaz/crm/list-alias-accounts) — View all Alias Accounts with pagination and filters.
* [Retrieve an Alias Account](/en/reference/midaz/crm/retrieve-alias-account) — Get the details of a specific Alias Account.
* [Update an Alias Account](/en/reference/midaz/crm/update-alias-account) — Edit the details of an existing Alias Account.
* [Delete an Alias Account](/en/reference/midaz/crm/delete-alias-account) — Soft-delete or permanently remove an Alias Account.
The organization ID is a URL path parameter for every Alias Account operation. The Holder ID is a path parameter for most of them. If [Access Manager](/en/platform/access-manager/access-manager) is enabled, add an `Authorization` header with a Bearer token.
### Via Lerian Console
You can manage Alias Accounts through the **Alias Accounts** page in the Midaz Module of [Lerian Console](/en/platform/console/about-lerian-console). The console gives you a visual interface to create, view, edit, and delete Alias Accounts without writing code.
[**Learn more in the Managing Alias Accounts guide.**](/en/midaz/console/managing-crm-alias-accounts)
## Next steps
***
Learn about the Holder entity that an Alias Account links to.
Review operational and data management best practices for CRM.
# Best practices
Source: https://docs.lerian.studio/en/midaz/crm/crm-best-practices
Follow CRM production best practices covering integration flow, encryption key management, metadata safeguards, and safe operational workflows.
CRM manages sensitive, identity-related data. These practices help you integrate CRM correctly, protect that data, and run it safely in production.
These recommendations complement the [CRM data security](/en/midaz/crm/crm-data-security) guide, which covers encryption, hashing, and key management in detail.
## 1. Follow the correct integration flow
***
CRM relies on a specific order to create entities. If you skip steps or create entities out of order, you can break integrations and lose data downstream.
The expected flow is:
1. **Create the holder** — the individual or the organization.
2. **Link the holder to a Midaz ledger account** — associate the holder with an account that already exists in the ledger.
3. **Make sure both exist** — before you start any downstream flow.
Without a holder linked to a ledger account, most CRM-driven features (fees, notifications, billing, identity verification) do not work as expected.
Make sure your identifiers are correct before you create or link records. The CRM API does **not** validate the accuracy of the data you submit. A mismatched `ledgerId` or `accountId` can cause integration failures with the ledger or other components.
## 2. Protect your encryption keys
***
CRM encrypts and hashes sensitive fields before it stores them. The security of this data depends entirely on how you manage your keys.
* **Generate unique keys** for `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` with `openssl rand -hex 32`.
* **Store keys in a secret manager** (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, or equivalent). Never hardcode them in configuration files, source code, or version control.
* **Plan for key rotation.** If a key is compromised, generate a new one and re-encrypt all affected data. This is a manual process, so design your operational runbooks for it.
* **Use Kubernetes Secrets** in production. Reference existing secrets via `useExistingSecret` and `existingSecretName` in your Helm values instead of storing keys inline.
For the full list of protected fields and encryption strategies, see [CRM data security](/en/midaz/crm/crm-data-security).
## 3. Never store sensitive data in metadata
***
The `metadata` object in CRM entities is **not encrypted**. CRM stores it in plain text for non-sensitive, auxiliary information only.
Do not use metadata for:
* Personal identification numbers (CPF, SSN, passport)
* Financial account details
* Contact information (email, phone)
* Any data subject to LGPD, GDPR, or similar regulations
If you need to store a sensitive attribute that is not in the [protected fields list](/en/midaz/crm/crm-data-security#protected-fields), contact your Lerian representative to discuss options.
## 4. Do not expose CRM directly on edge layers
***
CRM runs inside the Midaz ledger and exposes an internal API. If you expose it directly through API gateways, load balancers, or frontend applications, you increase your attack surface. You also bypass application-level access controls.
Instead:
* Route CRM traffic through your **backend services** or an internal API layer.
* Use [Access Manager](/en/platform/access-manager/access-manager) to enforce authentication and authorization if you need fine-grained control.
* Restrict network access to the ledger pods with Kubernetes NetworkPolicies or your cloud provider's security groups.
## 5. Use soft delete as the default
***
CRM supports both **soft delete** and **hard delete**:
* **Soft delete** (default): CRM marks the record with a `deletedAt` timestamp. The record drops out of standard queries but stays in the database for audit and recovery.
* **Hard delete**: CRM requests removal of the record, subject to your deployment's retention and compliance rules. Where legal, regulatory, audit, or record-keeping obligations require retention, CRM does not guarantee physical removal.
For most cases, soft delete is the safer choice. It preserves audit trails and lets you recover data you delete by mistake. Reserve hard delete for cases where regulations demand complete removal (for example, GDPR right-to-erasure requests).
Confirm your retention and legal-hold policy before you run a hard delete. The GDPR right to erasure (Article 17) is not absolute. Where legal, regulatory, audit, or record-keeping obligations require data retention, they can take precedence and restrict erasure.
## 6. Validate data before sending it to CRM
***
CRM acts as a **neutral, persistent data layer**. It does not enforce business rules, validate document formats, or check KYC compliance. Data integrity is your responsibility.
Before you create or update a record:
* Validate document formats (CPF, CNPJ, passport numbers) on your application layer.
* Make sure the `ledgerId` and `accountId` values point to real entities in Midaz.
* Sanitize inputs so you do not store malformed or inconsistent data.
## 7. Keep CRM and Midaz versions aligned
***
CRM ships inside the Midaz ledger binary, so it shares the Midaz version. Before you upgrade:
* Check the [version compatibility table](/en/platform/plugins/midaz-version-compatibility) to confirm your target version.
* Test the upgrade in a staging environment before you apply it to production.
* Back up your MongoDB data and Helm values before any major upgrade.
For upgrade procedures, see the [Helm upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide).
## 8. Monitor database health and performance
***
CRM uses MongoDB for data storage. In production:
* **Monitor connection pool usage.** The default `MONGO_CRM_MAX_POOL_SIZE` is 1000. Adjust it for your traffic patterns and replica count.
* **Set up alerts** for MongoDB disk usage, replication lag, and connection saturation.
* **Enable backups.** Whether you use the bundled Bitnami MongoDB or an external instance, run automated backups and test them regularly.
* **Enable OpenTelemetry** (`ENABLE_TELEMETRY: true`) to collect traces and metrics from CRM. Integrate with your observability stack for end-to-end visibility.
## 9. Review the security recommendations
***
CRM handles personal and sensitive data. Beyond CRM-specific practices, make sure your deployment follows the platform-wide [Security recommendations](/en/midaz/security-recommendations), which cover:
* Network segmentation and Zero Trust Architecture
* TLS 1.2+ enforcement for all communications
* IAM and RBAC configuration
* Incident response planning
* Patch management and vulnerability scanning
# Core Concepts
Source: https://docs.lerian.studio/en/midaz/crm/crm-core-concepts-overview
Meet the two CRM entities behind every Midaz account: Holders that carry customer identity and Alias Accounts that link them to Ledger accounts.
The CRM core concepts define the entities that connect real-world identities to your Midaz ledger accounts. They provide the business context that the ledger's transactional layer doesn't manage.
## Content
***
Here, you'll find:
* [**Holders**](/en/midaz/crm/holders): The core entity for an individual (`NATURAL_PERSON`) or an organization (`LEGAL_PERSON`) behind a ledger account. A Holder stores identity attributes such as name, document number, contact details, and addresses.
* [**Alias Accounts**](/en/midaz/crm/alias-accounts): The link between a Holder and a specific ledger account. Alias Accounts let one Holder own accounts in different ledgers. Each one keeps banking details and metadata in one place.
# CRM data security
Source: https://docs.lerian.studio/en/midaz/crm/crm-data-security
Secure CRM personal data with HTTPS/TLS in transit, field-level encryption and hashing at rest, and controls aligned with GDPR and LGPD requirements.
### Why does this matter?
Every regulation, from GDPR to LGPD, shares one principle: you use personal data only for the purpose the user agreed to. The **CRM** is part of the Midaz ledger. It handles transactional and non-transactional flows, so it must protect the sensitive data these flows carry.
Although Lerian doesn't offer cybersecurity services, we follow strict information security practices in all software we ship.
## Security responsibilities
***
As an on-premise technology provider, Lerian does not oversee or enforce our clients' cybersecurity policies. We respect the trust-based relationship each institution has with its end users. We assume that each client follows the LGPD and any other data protection regulation that applies to its region or industry.
Still, Lerian delivers technology that follows strong, market-aligned security practices. These practices protect sensitive CRM data as follows:
### In transit
**TLS over HTTPS** protects all data that you exchange with the CRM. The CRM encrypts sensitive fields *before* it writes them to the database. This applies to every request that creates or updates a holder or an instrument.
### At rest
The CRM runs on-premise, so disk and volume encryption is the client's responsibility. We strongly recommend it. It adds a robust layer of protection.
## How we protect data
***
The CRM uses different protection methods. The method depends on the type of data and its intended use.
* **Encryption** protects values that the system might need to read again, such as names or contact information.
* **Hashing** protects values that the system never shows again but must still match, such as identifiers for filtering or lookup.
The CRM protects sensitive data as soon as the data reaches it. The CRM never stores or processes sensitive data in raw form.
### Encryption and hashing strategies
The CRM combines several cryptographic techniques that follow industry best practices. These techniques protect sensitive data and still support the application.
#### Encryption
The system must read or show some fields again, such as personal names or emails. The CRM encrypts each of these values with strong, symmetric encryption. Even if someone reads the database directly, the original values stay unreadable without proper authorization.
Encryption also adds a random component, so identical values never produce the same encrypted output.
#### Hashing
The CRM hashes certain fields to allow secure filtering without exposure of the original value. It stores these hashes in a separate internal structure for fast, secure lookups.
In some cases, the CRM hashes a field only to meet an internal database constraint, even when the field is not searchable.
### Key management
The CRM uses encryption keys and hashing keys. The deployment team must generate, store, and manage these keys securely. Keys must follow strong cryptographic standards. Never hardcode a key or expose it in source code or version control.
Protect your keys with a dedicated secret manager or secure storage. If an attacker compromises a key, rotate it and re-encrypt or re-hash the affected data.
### Protected fields
The CRM protects the fields listed below. You do not need to know how this protection works. Do not enter sensitive data in any field that this list does not include.
Never store sensitive information in the `metadata` object.
#### Holder
* `name`
* `document`
* `contact.primaryEmail`
* `contact.secondaryEmail`
* `contact.mobilePhone`
* `contact.otherPhone`
* `naturalPerson.motherName`
* `naturalPerson.fatherName`
* `legalPerson.representative.name`
* `legalPerson.representative.document`
* `legalPerson.representative.email`
#### Instrument
* `document`
* `bankingDetails.account`
* `bankingDetails.iban`
* `regulatoryFields.participantDocument`
* `relatedParties.document`
## Best practices & recommendations
***
Data encryption in the **CRM** adds a strong security layer. Even if an attacker compromises the database, the sensitive data stays inaccessible in its original form.
The CRM decrypts data only with a valid key, or through authenticated and authorized service calls. Key security is therefore critical. If a key leaks, the application cannot protect your data on its own.
To keep your environment safe, we strongly recommend:
* **Do not expose the Midaz API** directly on edge layers such as gateways or frontend apps.
* **Never store sensitive data in metadata fields.** The CRM does not encrypt them.
* **Enforce governance** over access to the production environment and related tools.
* **Use secure key managers** with strict access controls to safeguard your secrets.
* **Encrypt disks or volumes** to protect data at rest.
If an attacker compromises a key, or if you must rotate it, **you must generate a new key and re-encrypt the affected data**. Use the CRM's available services for this.
Data security is a shared responsibility. The **CRM** gives you the building blocks. Make sure your deployment uses them well.
# Getting started with CRM
Source: https://docs.lerian.studio/en/midaz/crm/crm-getting-started
Follow this guide to register Holders in CRM and link them to ledger accounts with Instruments through the Midaz REST API.
This guide shows how to create and manage **Holders** — the customers or companies behind your accounts.
It also links each holder to a ledger account with an **Instrument**. By the end, you have a holder registered in CRM and linked to a ledger account.
## Prerequisites
***
Before you begin, make sure you meet the following requirements:
* You completed the [Midaz setup](/en/midaz/midaz-setup) guide and all services run.
* At least one **Organization**, **Ledger**, and **Account** already exist, as created in the [Midaz Getting Started](/en/midaz/midaz-getting-started) guide.
* The Midaz ledger serves the holder and instrument endpoints. In Midaz v4, CRM is part of the ledger binary, so it needs no separate service or port.
Replace the placeholder IDs in the examples below with the real IDs from your environment.
## CRM components
***
The CRM (Customer Relationship Management) component lets you register the people and companies behind your ledger accounts.
It manages two core entities:
* **Holders**: Individuals (`NATURAL_PERSON`) or companies (`LEGAL_PERSON`) that own accounts.
* **Instruments**: The link between a holder and a specific ledger account, with optional banking details.
This model lets one holder own many accounts across different ledgers. It keeps identity and contact information in one place.
## Step 1 — Create a holder
***
A **Holder** represents a person or company in your system. Create holders for individuals (`NATURAL_PERSON`) or companies (`LEGAL_PERSON`).
Send a `POST` request with the holder's type, name, document, contact, and address. For the full request and response schema, see [Create a holder](/en/reference/midaz/create-a-holder).
```bash theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/holders \
-H "Content-Type: application/json" \
-d '{
"type": "NATURAL_PERSON",
"name": "Jane Smith",
"document": "12345678900",
"contact": {
"primaryEmail": "jane.smith@example.com",
"mobilePhone": "+15551234567"
},
"addresses": {
"primary": {
"line1": "123 Main Street",
"line2": "Apt 4B",
"city": "New York",
"state": "NY",
"zipCode": "10001",
"country": "US",
"description": "Home address"
}
},
"naturalPerson": {
"favoriteName": "Jane",
"birthDate": "1990-05-15",
"nationality": "American"
},
"metadata": {
"segment": "premium",
"source": "onboarding"
}
}'
```
To register a company instead of an individual, set the type to `LEGAL_PERSON`:
```bash theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/holders \
-H "Content-Type: application/json" \
-d '{
"type": "LEGAL_PERSON",
"name": "Acme Corp Ltd",
"document": "12345678000199",
"contact": {
"primaryEmail": "finance@acmecorp.com",
"mobilePhone": "+15559876543"
},
"legalPerson": {
"tradeName": "Acme Corp",
"activity": "Financial services",
"type": "Limited Liability",
"foundingDate": "2015-03-20",
"size": "Medium",
"status": "Active",
"representative": {
"name": "Bob Johnson",
"document": "98765432100",
"email": "bob@acmecorp.com",
"role": "CFO"
}
}
}'
```
Save the `holderId` from the response. You use it when you create instruments.
## Step 2 — Link a holder to an account
***
Once the holder exists, link it to a ledger account with an **Instrument**. An instrument connects a holder to one account inside a ledger, with optional banking details.
For the full request and response schema, see [Create an instrument](/en/reference/midaz/create-an-instrument).
```bash theme={null}
curl -X POST http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id}/instruments \
-H "Content-Type: application/json" \
-d '{
"ledgerId": "{ledger_id}",
"accountId": "{account_id}",
"bankingDetails": {
"branch": "0001",
"account": "123450",
"type": "CACC",
"openingDate": "2025-01-15",
"countryCode": "US",
"bankId": "12345"
},
"metadata": {
"isPrimary": "true"
}
}'
```
## Step 3 — Query and update your data
***
Once holders and instruments exist, you can retrieve, list, and update them.
| Operation | Endpoint | API reference |
| ------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Retrieve a holder | `GET /v1/organizations/{organization_id}/holders/{holder_id}` | [Retrieve a holder](/en/reference/midaz/retrieve-a-holder) |
| List all holders | `GET /v1/organizations/{organization_id}/holders?limit=10&page=1` | [List holders](/en/reference/midaz/list-holders) |
| List instruments for a holder | `GET /v1/organizations/{organization_id}/instruments?holder_id={holder_id}` | [List instruments](/en/reference/midaz/list-instruments) |
| Retrieve a specific instrument | `GET /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}` | [Retrieve an instrument](/en/reference/midaz/retrieve-an-instrument) |
| Update a holder | `PATCH /v1/organizations/{organization_id}/holders/{holder_id}` | [Update a holder](/en/reference/midaz/update-a-holder) |
```bash theme={null}
curl -X PATCH http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id} \
-H "Content-Type: application/json" \
-d '{
"contact": {
"primaryEmail": "jane.new-email@example.com",
"mobilePhone": "+15559999999"
},
"metadata": {
"segment": "vip",
"source": "onboarding"
}
}'
```
The update changes only the fields you send. All other fields stay unchanged.
## Step 4 — Clean up
***
To remove resources, delete instruments first, then holders.
| Operation | Endpoint | API reference |
| -------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Delete an instrument | `DELETE /v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}` | [Delete an instrument](/en/reference/midaz/delete-an-instrument) |
| Delete a holder | `DELETE /v1/organizations/{organization_id}/holders/{holder_id}` | [Delete a holder](/en/reference/midaz/delete-a-holder) |
Delete an instrument:
```bash theme={null}
curl -X DELETE http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}
```
Delete a holder:
```bash theme={null}
curl -X DELETE http://localhost:3002/v1/organizations/{organization_id}/holders/{holder_id}
```
## Summary
***
In this guide, you:
1. Created a **Holder** to register an individual or company in CRM.
2. Created an **Instrument** to link the holder to a ledger account.
3. Queried and updated CRM data.
4. Removed instruments and holders when no longer needed.
## Next steps
***
Explore advanced filtering, metadata queries, and all available endpoints.
Manage holders through a graphical interface.
# Migrating from standalone CRM
Source: https://docs.lerian.studio/en/midaz/crm/crm-migration-guide
Migrate from the standalone plugin-crm chart to the CRM component embedded in Midaz v3.5.0+ and Helm v5.x, with architecture and configuration steps.
Starting with **Midaz v3.5.0** and **Helm Chart v5.x**, CRM is no longer deployed as a standalone plugin. It is now integrated directly into the Midaz monorepo and Helm chart as an embedded component.
This guide explains how to migrate from the standalone `plugin-crm` deployment to the integrated CRM.
If you are starting a **new Midaz deployment** (v5.x+), you do not need this guide. Simply enable CRM in your Helm values as described in [Deploy Midaz using Helm](/en/platform/helm/midaz/midaz-installation). The integrated CRM is now the only supported deployment model.
## What changed
***
The CRM plugin was originally maintained as a **separate codebase** with its own release cycle and deployed independently through a dedicated Helm chart (`plugin-crm`) in the `midaz-plugins` namespace.
Starting with **Midaz v3.5.0-beta.12** (December 2025), CRM was incorporated into the Midaz monorepo under `components/crm/`. Its deployment was then consolidated into the main Midaz Helm chart beginning in **v5.x**.
### Architecture comparison
| Aspect | Standalone (v4.x and earlier) | Integrated (v5.x+) |
| :--------------- | :---------------------------------- | :--------------------------------------- |
| **Source code** | Separate repository | `components/crm/` in the Midaz monorepo |
| **Helm chart** | `plugin-crm` (dedicated chart) | Part of the `midaz` chart |
| **Namespace** | `midaz-plugins` | `midaz` |
| **Versioning** | Independent release cycle | Matches Midaz core version |
| **MongoDB** | Own connection configuration | Shared MongoDB with other Midaz services |
| **Installation** | `helm install plugin-crm oci://...` | `crm.enabled: true` in Midaz values |
| **Port** | 4003 | 4003 (unchanged) |
### API changes
The CRM API remains **fully backward-compatible**. All endpoints available in the standalone version continue to work the same way in the integrated deployment.
| Resource | Endpoints | Status |
| :------------------- | :----------------------------------------------------- | :-------------------------- |
| **Holders** | POST, GET (list), GET (by ID), PATCH, DELETE | Unchanged |
| **Aliases** | POST, GET (list by holder), GET (by ID), PATCH, DELETE | Unchanged |
| **Aliases (global)** | GET `/v1/aliases` (list across all holders) | Unchanged |
| **Related Parties** | DELETE | Added in integrated version |
The `GET /v1/aliases` endpoint allows you to list aliases across all holders with advanced filtering. Filters include `holder_id`, `account_id`, `ledger_id`, `document`, banking details, regulatory fields, and related party attributes.
This endpoint complements the holder-scoped endpoints under `/v1/holders/{holder_id}/aliases`.
The endpoint `DELETE /v1/holders/{holder_id}/aliases/{alias_id}/related-parties/{related_party_id}` was introduced with the integrated CRM.
If you previously needed to remove related parties individually, this operation is now supported directly. In earlier versions, this required updating the alias payload without the related party.
### What stays the same
* **API contract**: All existing endpoints, request and response schemas, and behaviors remain unchanged.
* **Database**: MongoDB continues to be the storage backend.
* **Authentication**: Access Manager integration works the same way (`PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_HOST`).
* **Encryption keys**: `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` are still required.
* **Default port**: CRM continues to run on port 4003.
## Pre-migration checklist
***
Before starting the migration, confirm the following:
**Identify your current standalone version**
```bash Shell theme={null}
helm list -n midaz-plugins
```
Record the `plugin-crm` chart version and app version.
**Backup your current Helm values**
```bash Shell theme={null}
helm get values plugin-crm -n midaz-plugins > plugin-crm-values-backup.yaml
```
**Backup your MongoDB data**
```bash Shell theme={null}
# Find the MongoDB pod in the midaz-plugins namespace
kubectl get pods -n midaz-plugins -l app=mongodb
# Export the CRM database
kubectl exec -n midaz-plugins -- \
mongodump --db crm --archive=/tmp/crm-backup.archive
# Copy the backup locally
kubectl cp midaz-plugins/:/tmp/crm-backup.archive ./crm-backup.archive
```
**Verify your Midaz chart is v5.x or later**
```bash Shell theme={null}
helm list -n midaz
```
If you are running v4.x or earlier, upgrade Midaz first using the [Upgrading Helm](/en/platform/helm/midaz/midaz-upgrade-guide) guide.
**Schedule a maintenance window**
CRM will be temporarily unavailable during migration. Plan for a short downtime window.
## Migration steps
***
### Step 1 — Enable CRM in the Midaz chart
Add the CRM configuration to your Midaz Helm values:
```yaml values.yaml theme={null}
crm:
enabled: true
configmap:
MONGO_HOST: "midaz-mongodb.midaz.svc.cluster.local."
MONGO_NAME: "crm"
MONGO_USER: "midaz"
MONGO_PORT: "27017"
secrets:
MONGO_PASSWORD: ""
LCRYPTO_HASH_SECRET_KEY: ""
LCRYPTO_ENCRYPT_SECRET_KEY: ""
```
Use the **same encryption keys** (`LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY`) used in the standalone deployment. Different keys will make existing encrypted data unreadable.
If you use external secrets:
```yaml values.yaml theme={null}
crm:
enabled: true
useExistingSecret: true
existingSecretName: "crm-secrets"
```
### Step 2 — Migrate your MongoDB data
If your standalone CRM used its own MongoDB instance, restore the data into the Midaz-managed MongoDB.
```bash Shell theme={null}
# Copy the backup to the Midaz MongoDB pod
kubectl cp ./crm-backup.archive midaz/:/tmp/crm-backup.archive
# Restore the CRM database
kubectl exec -n midaz -- \
mongorestore --archive=/tmp/crm-backup.archive --db crm --drop
```
If the standalone and integrated CRM already use the **same MongoDB instance**, you can skip this step. Just confirm that `MONGO_HOST` and `MONGO_NAME` match.
### Step 3 — Deploy the integrated CRM
```bash Shell theme={null}
helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \
--version 5.x.x \
-n midaz \
-f your-values.yaml
```
### Step 4 — Verify the integrated CRM is running
```bash Shell theme={null}
# Check the pod status
kubectl get pods -n midaz -l app=crm
# Check the logs
kubectl logs -n midaz deployment/midaz-crm
# Test the health endpoint (run port-forward in a separate terminal)
kubectl port-forward -n midaz svc/midaz-crm 4003:4003 &
curl http://localhost:4003/health
```
### Step 5 — Validate your data
Run a quick validation to confirm that your data migrated correctly.
```bash Shell theme={null}
# List holders through the integrated CRM
curl -H "X-Organization-Id: " \
http://localhost:4003/v1/holders
# Verify a specific holder
curl -H "X-Organization-Id: " \
http://localhost:4003/v1/holders/
```
Compare the results with the standalone deployment.
### Step 6 — Update DNS and ingress
Update your DNS records or ingress rules to point to the CRM service in the `midaz` namespace.
```yaml ingress-example.yaml theme={null}
# The CRM service name changes from the standalone chart
# Old: plugin-crm.midaz-plugins.svc.cluster.local
# New: midaz-crm.midaz.svc.cluster.local
```
### Step 7 — Remove the standalone CRM
After confirming that everything works as expected, remove the standalone deployment.
```bash Shell theme={null}
helm uninstall plugin-crm -n midaz-plugins
```
Only uninstall the standalone CRM **after validating the integrated deployment**. This operation removes the standalone deployment and its resources.
If the `midaz-plugins` namespace is no longer needed, you can optionally remove it.
```bash Shell theme={null}
kubectl delete namespace midaz-plugins
```
## Access Manager permissions
***
Access Manager permissions remain unchanged after the migration.
The application name continues to be `plugin-crm`, and permissions apply to the `holders` and `aliases` resources.
| Permission | Description | Resources | Allowed Methods |
| :---------------------------------- | :------------- | :--------------- | :----------------------- |
| `plugin-crm-editor-permission` | Full access | holders, aliases | post, get, patch, delete |
| `plugin-crm-contributor-permission` | Read and write | holders, aliases | post, get, patch |
| `plugin-crm-viewer-permission` | Read only | holders, aliases | get |
No updates are required in your Access Manager configuration.
## Rollback procedure
***
If you need to revert to the standalone CRM:
Disable CRM in the Midaz chart:
```yaml values.yaml theme={null}
crm:
enabled: false
```
Re-deploy the Midaz chart:
```bash Shell theme={null}
helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \
--version 5.x.x -n midaz -f your-values.yaml
```
Re-install the standalone `plugin-crm`:
```bash Shell theme={null}
helm install plugin-crm oci://registry-1.docker.io/lerianstudio/plugin-crm \
--version \
-n midaz-plugins \
-f plugin-crm-values-backup.yaml
```
Restore DNS or ingress to point back to the standalone service.
## Troubleshooting
***
**CRM pod fails to start with encryption errors**
* Confirm that `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` exactly match the values used in the standalone deployment.
**Data appears empty after migration**
* Verify that `MONGO_HOST` and `MONGO_NAME` point to the correct MongoDB instance and database.
* If you ran `mongorestore`, confirm the restore completed successfully.
**Access Manager rejects requests**
* The application name in Access Manager should still be `plugin-crm`. No change is required.
**Port conflict on 4003**
* Running both the standalone and integrated CRM simultaneously will create a conflict on port 4003.
If you need to run both during testing, temporarily change the port in the Midaz values:
```yaml values.yaml theme={null}
crm:
configmap:
SERVER_PORT: "4013"
```
## Next steps
***
Learn about CRM features and design principles.
Start working with holders and alias accounts.
Complete Helm upgrade guide covering all migration paths.
Full deployment reference for Midaz v5.x.
# What is CRM?
Source: https://docs.lerian.studio/en/midaz/crm/crm-overview
Discover how the optional CRM layer enriches Midaz Ledger accounts with customer identity, contact details, and business context outside the ledger.
CRM is **not part of the ledger's transactional domain**. It adds business attributes to ledger accounts. It does not change the ledger's logic, consistency, or performance.
CRM is where user metadata lives. It does not hold balances, transfers, or fees. It records who is behind an account and what you need to run your business.
## Versioned with Midaz
***
CRM ships inside the Midaz ledger and needs **no separate license**. Midaz is source-available.
CRM shares the Midaz version. For example, Midaz `v3.5.0` uses CRM `v3.5.0`.
CRM runs in the same binary as the ledger. It is always available. You choose whether to store holder data and call its endpoints.
**Test CRM locally**
You can run CRM with the Midaz stack locally, without Kubernetes, using our [plugins-docker-compose repository](https://github.com/LerianStudio/plugins-docker-compose).
## Why use the CRM?
***
Midaz ledger accounts are generic by design. They are transaction-first and domain-agnostic. CRM adds meaning to those accounts and links each one to a user profile and a business context.
You do not have to use CRM. You can adopt it later, only when your use case needs customer or business metadata beyond the ledger.
### Example use cases
* **Banking (individuals and businesses)**: CRM stores identifiers like CPF or CNPJ, an address, contact details, or account numbers. Midaz runs the same transaction logic for both customer types.
* **Supply chains (supplier, factory, retailer)**: CRM tracks metadata like supplier IDs, warehouse locations, or contract details. This data does not belong in the transaction, but it drives business decisions.
CRM ships inside the Midaz ledger, so there is no separate service to deploy. Use it only if your product or regulatory context needs customer or business metadata.
## Entities
***
CRM has two core entities:
* **[Holder](/en/midaz/crm/holders)** – The main entity in CRM. It represents the persona for a Midaz ledger account. It stores identity attributes and individual traits.
* **[Alias Account](/en/midaz/crm/alias-accounts)** – The business context for a [Ledger Account](/en/midaz/accounts) in **Midaz Ledger**. It holds financial details such as bank account information.
*Figure 1* shows how the CRM entities connect to Midaz.
## Design principles
***
CRM focuses on:
* **Separation of concerns**: It keeps user profile data out of transaction logic.
* **System performance**: It adds no user-level orchestration inside Midaz core services.
* **Simpler deployment**: CRM ships inside the ledger binary, with no separate service to run.
* **Clear domain boundaries**: It evolves with Midaz without coupling to ledger internals.
CRM does not validate ledger-level rules or compliance processes such as KYC. It is a neutral, persistent data layer.
## API behavior
***
CRM runs inside the Midaz ledger binary. It exposes a registry API for non-transactional data linked to Midaz accounts.
* It does **not** enforce validations such as user status, risk profile, or document verification.
* It is **agnostic to the asset, ledger, or grouping logic** used in Midaz accounts.
* It gives consistent access to holder metadata without loading the transaction layer.
## Security & compliance
***
Lerian ships Midaz and its components for on-premise deployment. **You keep control** of your infrastructure, compliance policies, and user-data lifecycle.
CRM ships with Midaz and follows the same security standards as the rest of the platform:
* **Data in transit**: Midaz encrypts it with industry-standard protocols.
* **Data at rest**: Midaz protects it with encryption and fine-grained access control.
You own encryption key management. Use a secret-management service to store, rotate, and control access to keys.
You define and enforce your own policies for LGPD, GDPR, and other regional data-protection rules. CRM provides the structure.
Review the [Security recommendations](/en/midaz/security-recommendations) before you deploy any component into your environment.
Apply security best practices to every component — **Access Manager**, **Fees Engine**, **Pix**, and **CRM**. Secure network boundaries, manage secrets, apply patches, and enforce strict access control.
## Data deletion strategies
***
CRM gives you full control over how it removes data. It offers two options:
* **Soft delete** (default): CRM flags the record as deleted but keeps it in the database. Use it to keep an audit trail or to recover data later.
* **Hard delete**: Where your policy permits removal, you can request deletion of the data. Physical removal depends on your retention, compliance, and legal-hold configuration.
Your policies define the behavior. CRM adapts to your compliance requirements.
## Access control
***
To restrict who can access your CRM data, enable our [Access Manager](/en/platform/access-manager/access-manager).
### Enabling Access Manager
First, install **Access Manager**. Then open the Midaz ledger `.env` file and set these environment variables:
```text Text theme={null}
## AUTH
---
PLUGIN_AUTH_HOST=http://plugin-auth:4000
PLUGIN_AUTH_ENABLED=true
```
After you enable it, CRM enforces access permissions from your authentication setup.
Access Manager is available under the Enterprise model.
To evaluate it, [contact our team](https://lerian.studio/contact).
Ready to start? See [Getting started with CRM](/en/midaz/crm/crm-getting-started) for a step-by-step guide.
## Next steps
***
Understand the key entities managed by CRM: Holders and Alias Accounts.
Browse endpoints for holders, alias accounts, and related parties.
Learn how to register holders and link them to ledger accounts.
# Using CRM
Source: https://docs.lerian.studio/en/midaz/crm/crm-using-overview
Manage Holders and Instruments in CRM day to day through the REST API or the visual Midaz Console interface.
This section covers CRM configuration and day-to-day operations through the REST API or the Midaz Console.
## Content
***
Here, you'll find:
* [**Using CRM**](/en/midaz/crm/crm-getting-started): Manage Holders and Instruments through the REST API.
* [**Using CRM with Midaz Console**](/en/midaz/crm/using-crm-with-midaz-console): Manage Holders and Instruments through the Midaz Console graphical interface.
# Holders
Source: https://docs.lerian.studio/en/midaz/crm/holders
Model the individuals or organizations behind Midaz accounts as Holders, storing names, documents, contacts, and compliance details as Natural or Legal Person.
A **Holder** is the core entity in CRM. It represents a real-world individual or organization behind a Midaz ledger account. A Holder stores identity attributes such as the name, document number, contact details, and addresses.
[CRM](/en/midaz/crm/crm-overview) manages Holders. Holders do not belong to the ledger's transactional domain. They enrich ledger accounts with business data. They do not affect the ledger's logic, consistency, or performance.
Each Holder is one of two types: **Natural Person** (individual) or **Legal Person** (company/organization).
To create and manage holders step by step, see [Getting started with CRM](/en/midaz/crm/crm-getting-started).
## Holder types
***
The holder type controls which fields are available. It also sets how the platform treats the entity. You choose the type at creation. You **cannot change it later**.
### Natural Person
The Natural Person type represents an individual customer. It supports personal attributes such as the name, gender, birth date, civil status, nationality, and family information.
Use this type for:
* Individual bank account holders
* Personal wallet owners
* Freelancers or sole proprietors
### Legal Person
The Legal Person type represents a company or organization. It supports business attributes such as the trade name, activity type, founding date, company size, and legal representative details.
Use this type for:
* Corporate treasury accounts
* Business partners and suppliers
* Institutional clients
Choose the holder type carefully at creation. You cannot change it later. To use a different type, create a new Holder.
## Holder fields
***
### Core fields
| Field | Type | Required | Description |
| :------------- | :--------- | :--------------- | :---------------------------------------------------------------------------------------------------------------- |
| **id** | `uuid` | System-generated | Unique identifier of the Holder. |
| **type** | `enum` | Yes | `NATURAL_PERSON` or `LEGAL_PERSON`. |
| **name** | `string` | Yes | Full name of the individual or legal name of the company. |
| **document** | `string` | Yes | Identification document number (e.g., CPF, CNPJ, passport). |
| **externalId** | `string` | No | Optional identifier to map the Holder to an external system. |
| **metadata** | `object` | No | Key-value pairs for custom, non-sensitive data. Keys are limited to 100 characters and values to 2000 characters. |
| **createdAt** | `datetime` | System-generated | Timestamp of creation (RFC 3339). |
| **updatedAt** | `datetime` | System-generated | Timestamp of last update (RFC 3339). |
| **deletedAt** | `datetime` | System-generated | Timestamp of soft deletion, if applicable (RFC 3339). |
### Address fields
The `addresses` object supports up to three addresses: `primary`, `additional1`, and `additional2`. Each address contains:
| Field | Type | Required | Description |
| :-------------- | :------- | :------- | :----------------------------------------------------------------------------------------------- |
| **line1** | `string` | Yes | Primary address line (street, number). Max 256 characters. |
| **line2** | `string` | No | Secondary address line (apartment, suite). Max 256 characters. |
| **zipCode** | `string` | Yes | Postal or ZIP code. Max 20 characters. |
| **city** | `string` | Yes | City name. Max 100 characters. |
| **state** | `string` | Yes | State or province code. Max 100 characters. |
| **country** | `string` | Yes | ISO 3166-1 alpha-2 country code (e.g., `US`, `BR`). |
| **description** | `string` | No | A descriptive label for the address (for example, Home, Office, or Billing). Max 100 characters. |
### Contact fields
| Field | Type | Required | Description |
| :----------------- | :------- | :------- | :-------------------------------------------------------------- |
| **primaryEmail** | `string` | No | Primary email address. |
| **secondaryEmail** | `string` | No | Secondary email address. |
| **mobilePhone** | `string` | No | Mobile phone number with country code (e.g., `+5511999999999`). |
| **otherPhone** | `string` | No | Alternative phone number. |
### Natural Person fields
Available only when `type` is `NATURAL_PERSON`.
| Field | Type | Required | Description |
| :--------------- | :------- | :------- | :-------------------------------------------------------------------- |
| **favoriteName** | `string` | No | Preferred name or nickname. |
| **socialName** | `string` | No | Social name (name the person identifies with). |
| **gender** | `string` | No | Gender identity. |
| **birthDate** | `string` | No | Date of birth in `YYYY-MM-DD` format. |
| **civilStatus** | `string` | No | Marital status (e.g., Single, Married, Divorced). |
| **nationality** | `string` | No | Nationality (e.g., Brazilian, American). |
| **motherName** | `string` | No | Mother's full name. |
| **fatherName** | `string` | No | Father's full name. |
| **status** | `string` | No | Lifecycle status (e.g., `Active`, `Inactive`, `Suspended`, `Closed`). |
### Legal Person fields
Available only when `type` is `LEGAL_PERSON`.
| Field | Type | Required | Description |
| :--------------- | :------- | :------- | :-------------------------------------------------------------------- |
| **tradeName** | `string` | No | Trade or doing-business-as name. |
| **activity** | `string` | No | Primary business activity. |
| **type** | `string` | No | Legal structure (e.g., LLC, Corporation). |
| **foundingDate** | `string` | No | Date the company was founded in `YYYY-MM-DD` format. |
| **size** | `string` | No | Company size classification (e.g., Small, Medium, Large). |
| **status** | `string` | No | Lifecycle status (e.g., `Active`, `Inactive`, `Suspended`, `Closed`). |
#### Representative
The `representative` object within `legalPerson` stores the details of the company's legal representative:
| Field | Type | Required | Description |
| :----------- | :------- | :------- | :---------------------------------------- |
| **name** | `string` | No | Full name of the legal representative. |
| **document** | `string` | No | Document number of the representative. |
| **email** | `string` | No | Email address of the representative. |
| **role** | `string` | No | Role within the company (e.g., CEO, CFO). |
## Data security
***
Midaz encrypts several Holder fields at rest, including the `name`, the `document`, and the contact fields. This protects the data even if someone gains access to the storage.
Never store sensitive information in the `metadata` object. Midaz does not encrypt metadata and stores it in plain text.
For the full list of protected fields and encryption strategies, see [CRM data security](/en/midaz/crm/crm-data-security).
## Managing Holders
***
### Via API
Use the CRM API to manage Holders programmatically:
* [Create a Holder](/en/reference/midaz/crm/create-holder) — Register a new Holder in the system.
* [List Holders](/en/reference/midaz/crm/list-holders) — View all Holders with pagination and filters.
* [Retrieve a Holder](/en/reference/midaz/crm/retrieve-holder) — Get the details of a specific Holder.
* [Update a Holder](/en/reference/midaz/crm/update-holder) — Edit an existing Holder's data.
* [Delete a Holder](/en/reference/midaz/crm/delete-holder) — Soft-delete or permanently remove a Holder.
Every CRM API request includes the organization ID in the URL path, for example `/organizations/{organization_id}/holders`. If you enable [Access Manager](/en/platform/access-manager/access-manager), add an `Authorization` header with a Bearer token.
### Via Lerian Console
You can manage Holders through the **Holders** page in the Midaz Module of [Lerian Console](/en/platform/console/about-lerian-console). The console provides a visual interface to create, view, edit, and delete Holders without code.
[**Learn more in the Managing Holders guide.**](/en/midaz/console/managing-crm-holders)
## Next steps
***
Learn how Alias Accounts link Holders to Midaz ledger accounts.
Follow the step-by-step guide to register Holders and create Alias Accounts.
# CRM in Lerian Console
Source: https://docs.lerian.studio/en/midaz/crm/using-crm-with-midaz-console
Manage CRM Holders and Alias Accounts visually inside the Midaz Module of Lerian Console, without writing API calls or curl requests to the REST endpoints.
The **CRM** interface is part of the **Midaz Module** in [Lerian Console](/en/platform/console/about-lerian-console). Use it to manage core entities such as Holders and Alias Accounts. It gives you a clear view of your CRM data.
## Accessing CRM features
***
CRM features are in the **Midaz Module** of Lerian Console. To open them:
1. Open **Lerian Console** and go to the **Midaz Module**
2. In the left-side menu, find the CRM entities in the **Accounts** section:
* **Holders** - Manage individuals and companies
* **Alias Accounts** - Link holders to ledger accounts
### Available actions
From the Midaz Module, you can:
Create and manage individuals or companies associated with your accounts
Link holders to Midaz ledger accounts with business context
# Database migrations
Source: https://docs.lerian.studio/en/midaz/database-migrations
Run Midaz Ledger and Tracer PostgreSQL schema migrations with dedicated runner images so applications boot against an already-migrated database.
Midaz applies PostgreSQL schema migrations through **dedicated migration-runner images** — one for the Ledger, one for Tracer — decoupled from the application binaries. The apps no longer migrate at startup: they boot against a schema that has already been migrated by the runner.
This model is what runs under `make up` in local development and under the Helm chart in Kubernetes. If you use `make up`, no manual step is required — the compose stack gates the app on the migration runner.
## Why a dedicated runner
***
The old in-process migrator ran on every app boot from the working directory. Under a hardened pod security context (`distroless:nonroot`, `runAsUser: 1000`, `readOnlyRootFilesystem: true`, `capabilities: drop [ALL]`), that path failed with `permission denied` and crash-looped the pod.
The dedicated runner sidesteps that entirely:
* Migrations run **once, as a separate step**, before the app starts.
* The app image no longer bundles migration SQL and never writes to the schema.
* The runner image itself runs cleanly under the same hardened security context (see [Security posture](#security-posture)).
## Runner images
***
Two images are published per release:
| Image | Applies | Databases |
| ------------------------- | ------------------------------------------ | -------------------------------- |
| `midaz-ledger-migrations` | Ledger onboarding + transaction migrations | Two: `onboarding`, `transaction` |
| `midaz-tracer-migrations` | Tracer schema | One |
Both images share the same shape:
* Base: [`migrate/migrate`](https://github.com/golang-migrate/migrate) `v4.19.1` (pinned).
* Runs as `USER 65532:65532` — non-root, UID-agnostic.
* POSIX shell entrypoint that assembles a DSN from environment variables, runs `migrate ... up`, and exits.
* Writes nothing to disk — migration progress is tracked in the `schema_migrations` table in Postgres, so `readOnlyRootFilesystem: true` is safe.
The Ledger runner applies **onboarding first, then transaction**, in a single invocation. The Tracer runner applies its single database in one pass.
## Environment contract
***
Each entrypoint accepts either a prebuilt URL override or the individual `DB_*` variables. **The URL override takes precedence when set.**
### Ledger runner
| Database | URL override | Assembled from |
| ----------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| onboarding | `ONBOARDING_DATABASE_URL` | `DB_ONBOARDING_HOST`, `DB_ONBOARDING_PORT` (default `5432`), `DB_ONBOARDING_USER`, `DB_ONBOARDING_PASSWORD`, `DB_ONBOARDING_NAME`, `DB_ONBOARDING_SSLMODE` (default `disable`) |
| transaction | `TRANSACTION_DATABASE_URL` | `DB_TRANSACTION_*` (same shape) |
### Tracer runner
| URL override | Assembled from |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| `DATABASE_URL` | `DB_HOST`, `DB_PORT` (default `5432`), `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `DB_SSL_MODE` (default `disable`) |
### DSN shape
When the entrypoint assembles the DSN from `DB_*` variables, it produces:
```text theme={null}
postgres://:@:/?sslmode=
```
The password is percent-encoded so URI-reserved characters (`% @ : / ? # & + space [ ]`) do not break the DSN. `%` is encoded first so already-inserted escapes are not double-encoded.
The entrypoint logs only phase markers (`applying onboarding migrations`, `applying transaction migrations`, `applying tracer migrations`, `migrations complete`). It never prints credentials or the assembled DSN.
## Local development
***
### `make up`
From the repository root, `make up`:
1. Starts infra and waits until Postgres is healthy.
2. Starts the one-shot `ledger-migrate` and `tracer-migrate` compose services.
3. Starts each app service, which `depends_on` its migrate service with `condition: service_completed_successfully`.
The app never starts against an un-migrated database.
### Host `make` targets
If you want to run migrations directly against a local database — for example when developing a new migration — each component exposes host-side Make targets that use the pinned `golang-migrate` CLI and read connection settings from `.env`.
Ledger (`components/ledger`):
| Target | Effect |
| -------------------------- | ----------------------------------------------- |
| `make migrate` | Applies onboarding then transaction (aggregate) |
| `make migrate-onboarding` | Applies onboarding only |
| `make migrate-transaction` | Applies transaction only |
| `make migrate-down` | Rolls back both databases |
Tracer (`components/tracer`):
| Target | Effect |
| ---------------------- | ------------------------------------------------------- |
| `make migrate` | Applies the tracer schema |
| `make migrate-down` | Rolls back the tracer schema |
| `make migrate-version` | Prints the current schema version |
| `make migrate-force` | Forces the schema version (recovery from a dirty state) |
### Example: run the Ledger runner manually
You rarely need to invoke the runner image by hand — the compose gate does it for you. When you do, point the runner at your databases via `DB_*` (or `*_DATABASE_URL`) and run it once:
```bash theme={null}
docker run --rm \
--network=infra-network \
-e DB_ONBOARDING_HOST=postgres \
-e DB_ONBOARDING_PORT=5432 \
-e DB_ONBOARDING_USER=midaz \
-e DB_ONBOARDING_PASSWORD='s3cret!' \
-e DB_ONBOARDING_NAME=onboarding \
-e DB_ONBOARDING_SSLMODE=disable \
-e DB_TRANSACTION_HOST=postgres \
-e DB_TRANSACTION_PORT=5432 \
-e DB_TRANSACTION_USER=midaz \
-e DB_TRANSACTION_PASSWORD='s3cret!' \
-e DB_TRANSACTION_NAME=transaction \
-e DB_TRANSACTION_SSLMODE=disable \
lerianstudio/midaz-ledger-migrations:
```
The runner exits `0` on success (including the idempotent no-op case) and non-zero on failure.
## Kubernetes deployment
***
In production, run each migration runner as a Kubernetes **Job** (typically an Argo CD PreSync Job or a Helm hook) that completes **before** the app rollout. The Helm chart wires this up for you; the sections below cover the properties you should keep in mind if you author your own manifests.
For multi-tenant deployments, the runner is intentionally **tenant-agnostic**: it migrates the databases pointed at by its environment, once. Per-tenant fan-out is a deploy concern — run the Job once per tenant database, passing that tenant's `DB_*` (or URL override) values.
### Security posture
The runner is designed for a hardened pod security context:
* **Non-root** — the image sets `USER 65532:65532`.
* **UID-agnostic** — migration files are root-owned and world-readable, so any UID can read them.
* **`readOnlyRootFilesystem`-safe** — `migrate` writes no filesystem state.
The same image runs cleanly under `runAsNonRoot: true`, `readOnlyRootFilesystem: true`, and `capabilities: drop [ALL]`.
### TLS
The entrypoints honor `sslmode` from the environment (`DB_ONBOARDING_SSLMODE`, `DB_TRANSACTION_SSLMODE`, or `DB_SSL_MODE`), defaulting to `disable`. **Production deployments should set `require`** (or stricter). The shell runner does not reproduce the in-code TLS enforcement, error classification, or telemetry that the apps use for their own connections — TLS for migrations is controlled entirely through `sslmode`, in exchange for a minimal, dependency-free runner.
## Conventions and idempotency
***
* **Paired up/down** — every migration has a `.up.sql` and a `.down.sql` file.
* **Idempotent** — running `migrate up` when the schema is already at the latest version is a no-op. Re-running the runner after a partial or complete apply is safe.
* **Pinned CLI** — the runner images and host `make` targets all pin `golang-migrate` `v4.19.1`.
## Upgrading from earlier versions
***
If you were running a Midaz release where the app migrated on startup, no manual data migration is required — the on-disk schema is the same. To adopt the new model:
1. Pull the release that ships the runner images.
2. In Kubernetes, add the `midaz-ledger-migrations` and `midaz-tracer-migrations` Jobs to your deploy pipeline so they run before the app rollout. If you use the official Helm chart, this is done for you.
3. In local development, `make up` continues to work as before — the compose stack now gates the apps on the new one-shot migrate services automatically.
## Related pages
***
* [Installing Midaz](/en/midaz/midaz-setup) — full local setup with `make up`.
* [Updating Midaz](/en/midaz/updating-midaz) — upgrade flows for local and Helm.
* [Deployment strategies](/en/midaz/deployment) — BYOC deployment overview.
* [Tracer environment variables](/en/tracer/tracer-environment-variables) — full Tracer configuration reference.
# Deployment strategies
Source: https://docs.lerian.studio/en/midaz/deployment
Choose the right deployment strategy for Midaz under BYOC — Kubernetes with Helm, Terraform foundations, and infrastructure prerequisites.
This section covers deployment strategies for running Midaz in your own infrastructure under the **BYOC (Bring Your Own Cloud)** model. If you're using the **SaaS** model, Lerian manages deployment for you — skip ahead to [Getting started](/en/getting-started) to begin configuring your platform.
Not sure which model fits? See [Deployment models](/en/deployment-models) for a full comparison of SaaS and BYOC.
Set up your environment, meet the prerequisites, and follow our best practices to ensure a secure, scalable, and reliable setup.
Before you deploy, check out the [Midaz Architecture Overview](/en/midaz/architecture). Understanding how the components work together will help you make informed decisions during setup.
### Content
In this section, you will find:
* [Midaz Terraform Foundation](/en/midaz/midaz-terraform-foundation): Ready-made Terraform examples for provisioning the cloud infrastructure Midaz runs on — AWS, GCP, or Azure.
For Kubernetes deployment, configuration, and lifecycle management via Helm, see the dedicated [Helm documentation](/en/platform/helm/helm-overview) in the Platform section.
## Deployment strategies
***
You manage the infrastructure in both Midaz modules, Community and Enterprise. We strongly recommend using Kubernetes with our Helm Charts. This approach simplifies deployment and scaling, especially if using the Enterprise edition, which includes support and modular microservices.
That said, you can choose the deployment strategy that fits your needs.
### Deploying with Kubernetes
We recommend our official Helm Charts. They offer a fast and flexible way to install Midaz in a Kubernetes environment.
You can either plug Midaz into your existing databases and services or use the default components provided in the Helm chart. This setup adapts to different infrastructure needs and supports production-grade deployments.
For a step-by-step guide, check the [Deploying using Helm](/en/platform/helm/midaz/midaz-installation) documentation.
### Self-managed deployment
If you prefer full control, you can deploy Midaz manually. Midaz includes a Docker configuration, which makes local or small-scale setups straightforward.
You can also adapt it to your own infrastructure, whether you're using a different orchestrator or running directly on virtual machines or bare metal.
For instructions, check the [Installing Midaz](/en/midaz/midaz-setup) page.
## Platform requirements
***
The default Helm configuration meets these minimums, except for RabbitMQ. Its bundled subchart installs server 3.13.6. Midaz targets RabbitMQ 4.x for development and tests, so that subchart runs below the target. If you bring your own infrastructure components, make sure you meet the minimum supported versions:
| Service | Minimum Supported Version | Notes |
| :------------- | :------------------------ | :-------------------------------------------------------------------- |
| Kubernetes | 1.20+ | 1.30+ recommended |
| Helm CLI | 3.8+ | Required for OCI registry |
| PostgreSQL | 16.3 | Primary database |
| MongoDB | 8.x | Document storage (Helm chart: bitnami/mongodb 16.4.0+) |
| RabbitMQ | 4.x | Message broker (Helm chart: rabbitmq subchart 2.1.11 → server 3.13.6) |
| Valkey | 8.x | In-memory cache (Redis fork) |
| OTEL Collector | 2.2.1 | Optional, for observability |
Midaz needs these minimum versions for a stable, performant platform. For the complete compatibility matrix, see [Version compatibility](/en/platform/helm/helm-version-compatibility).
## Best practices
***
### Access to the infrastructure
* Use **least privilege** policies when connecting to databases.
* Block direct access to databases by default. Only allow it in exceptional cases with approval.
* Store secrets securely using tools like **HashiCorp Vault**.
* Use dedicated tools for safe access to production data.
### Redundancy and high availability
* Enable **replication and backups** for both Midaz and your databases.
* Use **load balancers** to route traffic across services evenly.
### Network security
* Run your services inside **VPCs** to isolate critical components.
* Use **NAT gateways or proxies** for outbound connections.
* Apply **security groups or firewall rules** to restrict access to required ports and IPs.
### Data persistence and storage
* Use **replicated databases** to support high availability and recovery.
* Choose **scalable storage solutions** that match your data growth.
* Use **Valkey** (Redis fork) for caching and background message management.
### Observability
* Integrate tools that monitor logs, traces, and metrics.
* Midaz supports observability through **OpenTelemetry**, making detecting and resolving issues easier.
## Shared responsibilities
***
Under the BYOC model, Lerian and your team share responsibility for running Midaz. For a detailed breakdown of who secures what — including how responsibilities differ between BYOC and SaaS — see the [Security](/en/midaz/security) page.
For actionable guidance on securing your environment before deployment, check the [Security Recommendations](/en/midaz/security-recommendations) documentation.
# FAQ
Source: https://docs.lerian.studio/en/midaz/faq
Answers to common Lerian and Midaz questions on API pagination limits, SaaS data isolation, tenant JWT scoping, and multi-organization platform configuration.
## Lerian APIs
***
This section answers common questions about Lerian's APIs. It covers general behavior, configuration, and best practices across all services.
Yes. The default maximum is **100** records per page. This limit keeps performance consistent and controls the data volume in each request. To raise it, set the `MAX_PAGINATION_LIMIT` environment variable in your deployment configuration. The API accepts larger page sizes after you restart the application.
**Important**: A larger page size can slow response times, especially with large datasets. Test in staging before you change production.
## Multi-tenancy and SaaS
***
These questions cover data isolation, tenant scoping, and how multi-tenancy works in Lerian deployments.
Yes. Every tenant operates on a separate database. The platform resolves your tenant from the JWT on every request and routes it to your isolated database. There is no way to access another tenant's data through the API. Learn more about [multi-tenancy](/en/multi-tenancy).
No. The JWT access token you receive during authentication carries your tenant context. The platform resolves it automatically. You do not need to include a tenant identifier in headers or request bodies.
Yes. A tenant can contain multiple Organizations. Each Organization has its own Ledgers, accounts, and transactions. The platform scopes all of them to your tenant automatically.
No. The API surface is identical: same endpoints, same payloads, same responses. Only one thing differs. SaaS requires authentication on every request, and your token scopes all operations to your tenant.
## Midaz
***
These questions cover Organizations, Ledgers, Accounts, Transactions, and more in Midaz.
### Organizations
No. Each Organization operates independently and does not communicate with others.
No. Each license binds to one Organization. To support multiple Organizations, acquire a separate license for each. The same rule applies to Plugins.
Yes. An Organization can have more than one Plugin.
Yes. An Organization can manage multiple Ledgers.
You can create a **Parent Organization** and a **Child Organization**. Each **Organization** keeps its own Ledger and operates independently. Transactions cannot move value directly between ledgers. You orchestrate the transfer with these steps:
In the source ledger, create a transaction from the original account (**source**) to the asset's **external account** (distribute). This removes the value from the source ledger.
In the destination ledger, **create a second transaction**. The **source** is now the asset's **external account**, and the destination is the receiving account (**distribute**).
This pattern moves value between ledgers in different Organizations in a controlled way.
### Ledgers
No. Ledgers do not communicate directly. Transfers between Ledgers require orchestration.
You must orchestrate the process and move the amount through an External Account. This involves two steps:
Ledger A -> External Account.
External Account -> Ledger B.
No. A single Ledger can support multiple Plugins. For example, one Ledger can handle both Exchange and Pix Plugins.
### Assets
No. Each Asset links to a single Account. Each Asset also links to an External Account. Midaz creates that External Account automatically when you create the Asset.
Midaz supports several Asset types:
* *currency*: Traditional fiat currencies like BRL, USD, and EUR.
* *fiat*: An alternative type for fiat currencies; like `currency`, the Asset code must follow ISO 4217.
* *crypto*: Digital assets such as BTC, ETH, and other cryptocurrencies.
* *commodities*: Tangible goods like gold, soybean, and oil.
* *others*: Custom Assets, including loyalty points and tokenized securities.
### Portfolios
A **Portfolio** groups accounts that belong to the same entity (**CPF/CNPJ**). For example, one CPF with two different `segment_id` values has two matching `account_id` values. You create a Portfolio for that CPF to link both accounts under one structure. This makes related accounts easier to find and manage.
### Accounts
No. Each Account links to a single Asset. You cannot change this link.
An External Account receives funds from outside the Ledger. It brings money into the system.
Midaz creates an **External Account** automatically when you create an Asset. This External Account backs all transactions that move in and out of the Ledger.
No. Each account (`account_id`) links to only one Segment (`segment_id`).
No. You can create as many Accounts as you need. Midaz sets no limit on the number of Accounts.
The balance top-up process works as follows:
1. When you create an Asset (for example, BRL) in the Midaz Ledger, Midaz also creates an External Account for that Asset.
2. This External Account mirrors the balances the institution holds outside Midaz. Those balances can sit in a PI account, a settlement account, a reserve account, or a traditional bank or payment account.
3. To deposit funds from outside the Midaz Ledger into a user account, follow these steps:
* Create a transaction with the External Account as the source and the target accounts as the destination.
* Midaz debits the External Account by the amount (so it becomes negative) and credits the destination accounts by the values in the transaction payload.
### Transactions
A Transaction must have at least two Operations. For example, a transfer of R\$ 100 from Account A to Account B has two operations:
* **Operation 1:** Debit R\$ 100 from Account A.
* **Operation 2:** Credit R\$ 100 to Account B.
Lerian gives clients several ways to access transaction receipts:
1. **Via APIs** — Retrieve transaction data through the APIs, then generate a visual receipt in any format you choose.
2. **With the Reporter** — Extract transaction data and create custom visual receipts.
3. **Through the Console** — Access transaction data directly in the Lerian Console.
### Entities
The Entity (`entity_id`) accepts external IDs. Midaz enforces no validation on this field. You can use the IDs that already exist in your database and integrate them into your system.
### Idempotency
Midaz treats the request as new every time. Retries can then create duplicate operations.
No. Scope each key to a single operation and endpoint.
Midaz uses only the TTL from the first request. A later change has no effect.
Yes. For a completed request, Midaz returns the same result it stored from the first request. It also sets the `X-Idempotency-Replayed` header to `true`.
The default TTL is **300 seconds** (5 minutes). Send the `X-TTL` header to set a custom value in seconds.
### Accounting in Midaz
Midaz lets you mirror your organization's official **Chart of Accounts** in the platform. You configure two core features:
* [Account Types](/en/midaz/accounts) — Create the logical categories from your chart, such as Assets, Liabilities, Revenue, and Expenses. Assign them to accounts in your ledger. When you enable the Account Types feature, the `type` field in the Accounts API becomes mandatory and must match a registered value.
* [Accounting Routes](/en/midaz/transaction-routing-entities) — Use Operation Routes to validate each leg of a transaction. For example, a debit must come from an account of type `user_wallet`. Use Accounting Routes (the `transactionRoute` resource in the API) to define complete transaction patterns that match your accounting logic.
Account Types and Accounting Routes together enforce your accounting rules at the ledger level. Midaz validates and categorizes every transaction against your Chart of Accounts. You do not hardcode rules in your business logic.
## Plugins
***
Plugins extend Midaz with integration and process orchestration. They provide abstractions so you can focus on your business model instead of system logic outside your domain.
The questions below cover how plugins work, how you deploy them, and the options available.
Plugins are technologies that integrate into the Midaz ledger. They simplify process integration and orchestration. They provide abstractions so clients can focus on their business model. Clients do not build or manage system logic outside their domain.
No. Plugins operate only with Midaz. They provide specific abstractions and orchestrate transactions based on the ledger structure.
After you contract a plugin, Lerian provides and installs it in your infrastructure (on-premise model), next to your Midaz instance. Applications connect to each plugin according to its function.
Lerian provides two types of plugins, grouped by origin:
* **Native Plugins:** Lerian develops and integrates these plugins into the Midaz ledger. Lerian provides full support for them.
* **Marketplace Plugins:** Lerian's partners create these plugins for specific market niches. Lerian helps integrate them into Midaz. The partners provide and support them directly.
## Fees Engine
***
These questions cover Fees Engine. Fees Engine is a licensed Midaz capability that runs inside the unified ledger process.
### General Concepts
Fees Engine is part of **Midaz**. It runs in the Midaz ledger process to calculate fees for financial transactions. Configure and deploy it with Midaz. Learn more in the [Fees Engine overview](/en/midaz/fees/fees-engine-overview). It works across three main domains:
* **Fee Packages (`/v1/packages`):** defines charging rules per transaction (flat fee, percentage, or whichever is greater).
* **Billing Packages (`/v1/billing-packages`):** defines periodic charges based on transaction volume or account maintenance.
* **Calculation and Estimation (`/v1/fees` and `/v1/estimates`):** endpoints for calculating fees in real time or simulating before confirming.
Fees Engine runs inside the Midaz ledger process. When a configured fee package applies, Midaz incorporates its fee calculations into the transaction. It uses ledger query use cases rather than an external HTTP connection to Midaz.
Every request requires the `X-Organization-Id` header with your Organization ID in Midaz. This header scopes the request to an Organization. It is specific to Fees Engine, not a tenant identifier. The platform still resolves your tenant context automatically from the JWT. When the authentication plugin is active, you also send a Bearer token in the `Authorization` header.
```
X-Organization-Id:
Authorization: Bearer
```
Fees Engine uses **MongoDB** for storage. Deletions follow the **soft-delete** pattern. Fees Engine does not remove records physically. It marks them with `deletedAt` instead. A deleted record does not appear in listings, but you can still audit it.
You need **Midaz v3.6.0** or higher. Fees Engine depends on Transaction module APIs that Midaz added in v3.6.0. Earlier Midaz versions do not work with Fees Engine.
### Fee Packages
A Fee Package (`Package`) is a set of charging rules under one `feeGroupLabel`. Each package links to an **Organization + Ledger**, and optionally to a **Segment**. A package can contain several fees (`Fee` objects), each with its own calculation logic. Learn more about [Fee Packages](/en/midaz/fees/using-fee-engine).
Send a `POST /v1/packages` with the following body. See the [Create Package API reference](/en/reference/midaz/plugins/fees-engine/create-package) for full details.
```json theme={null}
{
"feeGroupLabel": "Digital Account Fees",
"ledgerId": "ldg_abc123",
"segmentId": "seg_xyz456",
"minimumAmount": "100.00",
"maximumAmount": "50000.00",
"transactionRoute": "PIX",
"enable": true,
"waivedAccounts": ["exempt-account-1", "exempt-account-2"],
"fees": {
"admin_fee": {
"feeLabel": "Administrative Fee",
"calculationModel": {
"applicationRule": "percentual",
"calculations": [
{ "type": "percentage", "value": "1.50" }
]
},
"referenceAmount": "originalAmount",
"priority": 1,
"isDeductibleFrom": true,
"creditAccount": "fee-revenue-account"
}
}
}
```
Yes. Set the `enable` field to `false` when you create or update the package. Fees Engine skips a disabled package during fee calculation, even when the transaction context matches its scope.
Fees Engine applies the package only to transactions whose value falls within the `[minimumAmount, maximumAmount]` range. If the transaction value falls outside this range, Fees Engine ignores the package.
**Example:** A package with `minimumAmount: 100` and `maximumAmount: 5000` charges fees only on transactions between 100 and 5,000.
If you do not set `maximumAmount`, the package can apply without an upper limit. Check the validation rules for your version.
Yes. Set the `transactionRoute` field in the package. Fees Engine then considers the package only for transactions with that route, such as `"PIX"`, `"TED"`, or `"BOLETO"`.
These are account aliases that the package **exempts** from fees. If the sender or recipient of a transaction is an account in `waivedAccounts`, Fees Engine does not apply the package fees to it.
```json theme={null}
"waivedAccounts": ["vip-account", "employee-account"]
```
This package does not charge any transaction that comes from or goes to these accounts.
Yes. The listing endpoints (`GET /v1/packages`, `GET /v1/billing-packages`) support the `limit` and `page` query parameters for pagination.
```
GET /v1/packages?limit=20&page=2
```
### Calculation Models
The `applicationRule` field inside `calculationModel` defines how Fees Engine calculates the fee. See [Calculation Models](/en/midaz/fees/fee-engine-calculation) for full details. There are three options:
| Rule | Description |
| ----------------- | ------------------------------------------------------------------- |
| `flatFee` | Fixed-amount fee |
| `percentual` | Percentage fee based on the reference amount |
| `maxBetweenTypes` | Calculates both flat and percentage; applies the **greater** result |
Use exactly **1 calculation** of type `flat`:
```json theme={null}
"calculationModel": {
"applicationRule": "flatFee",
"calculations": [
{ "type": "flat", "value": "5.00" }
]
}
```
This charges a fixed 5.00 regardless of the transaction amount.
Use exactly **1 calculation** of type `percentage`:
```json theme={null}
"calculationModel": {
"applicationRule": "percentual",
"calculations": [
{ "type": "percentage", "value": "2.50" }
]
}
```
This charges 2.5% of the transaction's reference amount.
`maxBetweenTypes` requires **2 or more calculations** that combine `flat` and `percentage`. Fees Engine calculates both and applies the **greater result**.
**Example:** Minimum fee of 3.00 or 1% of the value — whichever is greater:
```json theme={null}
"calculationModel": {
"applicationRule": "maxBetweenTypes",
"calculations": [
{ "type": "flat", "value": "3.00" },
{ "type": "percentage", "value": "1.00" }
]
}
```
For a 200 transaction: 1% = 2.00 vs. 3.00 flat → charges **3.00**.
For a 500 transaction: 1% = 5.00 vs. 3.00 flat → charges **5.00**.
Yes. You can include any combination of `flat` and `percentage`. Fees Engine evaluates all of them and applies the greatest. Note that `flatFee` and `percentual` require exactly 1 calculation. Only `maxBetweenTypes` accepts 2 or more.
### Important Fields
The `referenceAmount` defines **which value** Fees Engine calculates the fee on:
* `originalAmount`: the original transaction value, **before** any fees.
* `afterFeesAmount`: the transaction value **after** higher-priority fees apply.
The fee with `priority: 1` runs first, so it **must** use `originalAmount`. No prior fees exist to consider.
When `isDeductibleFrom: true`, Fees Engine deducts the fee from the amount the sender sends. The recipient receives the discounted amount, and the sender pays extra to cover the charge.
When `false`, Fees Engine charges the fee **separately**. The sender sends the full amount, and Fees Engine debits the fee apart.
**Constraints:**
* `isDeductibleFrom: true` requires `referenceAmount: originalAmount`
* If the type is `percentage`: the value cannot exceed 100
* If the type is `flat`: the value cannot exceed the package's `minimumAmount`
The `priority` defines the **execution order** of fees within a package. Fees Engine runs lower values first.
* `priority: 1` → executed first (must use `originalAmount`)
* `priority: 2` → executed after, can use `afterFeesAmount`
Use priorities to chain fees. For example, run an administrative fee on the original amount. Then run an IOF fee on the amount after the administrative fee.
It is the alias of the ledger account that receives the fee revenue. Each fee can have a different `creditAccount`. This helps when different fees belong to different cost centers.
```json theme={null}
"creditAccount": "admin-fee-revenue-account"
```
These fields define the routes of the **accounting legs** that the fee charge generates. They are optional. They let you track the origin and destination of fee movements in the ledger.
### Billing Packages
Billing Packages are **periodic** charge packages, independent of per-transaction fee calculation. See [Billing Package examples](/en/midaz/fees/billing-package-examples) for use cases. There are two types:
* **`volume`:** charges based on the **number of transactions** in a period, with tiered pricing.
* **`maintenance`:** charges a **fixed fee per account** in a given scope.
Use volume billing to charge clients by the **number of processed transactions**. This is a common model for payment platforms with volume-based pricing. You define price tiers that apply as volume grows.
```json theme={null}
{
"type": "volume",
"eventFilter": {
"transactionRoute": "PIX",
"status": "approved"
},
"pricingModel": "tiered",
"tiers": [
{ "minQuantity": 0, "maxQuantity": 1000, "unitPrice": "0.50" },
{ "minQuantity": 1001, "unitPrice": "0.30" }
],
"assetCode": "BRL",
"debitAccountAlias": "client-account",
"creditAccountAlias": "volume-revenue-account"
}
```
The last tier must be **unbounded** (no `maxQuantity`). There must be no gaps or overlaps between tiers.
Use maintenance billing to charge a **fixed periodic fee per account**. For example, charge a monthly fee per active account. You specify the scope (`segmentId`, `portfolioId`, or `aliases`) and the fee amount.
```json theme={null}
{
"type": "maintenance",
"feeAmount": "15.00",
"assetCode": "BRL",
"maintenanceCreditAccount": "maintenance-revenue-account",
"accountTarget": {
"segmentId": "seg_premium_clients"
}
}
```
`accountTarget` must have exactly **one** of the three fields: `segmentId`, `portfolioId`, or `aliases` (maximum 100 aliases).
Tiers define the **unit price per tier** as volume increases. The rules are:
1. Must be **contiguous** — no gaps between tiers (`minQuantity` of the next = `maxQuantity` of the previous + 1).
2. Must not **overlap**.
3. The **last tier must be unbounded** (no `maxQuantity`).
**Example of correct tiers:**
```json theme={null}
"tiers": [
{ "minQuantity": 0, "maxQuantity": 500, "unitPrice": "0.80" },
{ "minQuantity": 501, "maxQuantity": 2000, "unitPrice": "0.60" },
{ "minQuantity": 2001, "unitPrice": "0.40" }
]
```
It is a **free allowance**. Fees Engine does not charge a set number of transactions before tiers apply. This helps pricing models with a minimum included volume.
**Example:** `freeQuota: 100` means Fees Engine does not charge the first 100 transactions in the period.
These are discount tiers for volume billing. They reduce the charged amount based on extra criteria. They complement the logic of the main `tiers`.
It defines **how Fees Engine counts transactions**:
* `perRoute`: counts transactions by route (e.g., total approved Pix).
* `perAccount`: counts transactions per individual account.
### Fee Calculation and Estimation
| Endpoint | When to use |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/fees` | Calculate the **actual** fee of an ongoing transaction. See the [Calculate Fees API reference](/en/reference/midaz/plugins/fees-engine/calculate-fees). The system automatically finds applicable packages based on ledger, segment, route, and amount. |
| `POST /v1/estimates` | **Simulate** the fee of a specific package before confirming the transaction. Useful for showing end users the cost before execution. See the [Simulate Fees API reference](/en/reference/midaz/plugins/fees-engine/simulate-fees). |
The endpoint receives the transaction data. The system **automatically finds** the applicable packages. It considers:
* `ledgerId` — required
* `segmentId` — optional
* `transactionRoute` — optional
* Transaction value — compared with the package's `minimumAmount`/`maximumAmount`
Fees Engine calculates and returns fees from all matching packages.
The `/v1/estimates` endpoint simulates the fee of a **specific package** (`packageId`). You do not need an actual transaction. It works well for:
* Show the estimated cost to the user before confirmation.
* Test configurations of newly created packages.
* Build fee simulators in your product.
Yes. `/v1/estimates` is a read-only endpoint. It does not alter state or register transactions. It is safe to use in UX flows to show the cost before confirmation.
After you configure Billing Packages, call this endpoint:
```
POST /v1/billing/calculate
```
This endpoint processes the configured rules and generates the charges for the period. See the [Calculate Billing API reference](/en/reference/midaz/plugins/fees-engine/calculate-billing).
### Common Errors
The fee with `priority: 1` must have `referenceAmount: "originalAmount"`. It is the first fee to run, so no prior fees exist to base the calculation on.
**Fix:**
```json theme={null}
{
"priority": 1,
"referenceAmount": "originalAmount"
}
```
Fees with `isDeductibleFrom: true` can only use `referenceAmount: "originalAmount"`. Update the field:
```json theme={null}
{
"isDeductibleFrom": true,
"referenceAmount": "originalAmount"
}
```
When `isDeductibleFrom: true` and the type is `flat`, the fee value cannot exceed the package's `minimumAmount`. This prevents a fee that is larger than the minimum transaction value.
**Example:** If `minimumAmount: 100`, the flat fee cannot exceed 100.
This error occurs when `isDeductibleFrom: true`, the type is `percentage`, and the value is above 100. A deductible percentage fee of 100% would zero out the transaction. Values above 100 are invalid.
Volume billing tiers must cover all ranges without gaps. Check that the `minQuantity` of each tier is exactly `maxQuantity + 1` of the previous tier.
```json theme={null}
// ❌ Wrong — gap between 500 and 600
{ "minQuantity": 0, "maxQuantity": 500, "unitPrice": "0.80" },
{ "minQuantity": 600, "unitPrice": "0.40" }
// ✅ Correct
{ "minQuantity": 0, "maxQuantity": 500, "unitPrice": "0.80" },
{ "minQuantity": 501, "unitPrice": "0.40" }
```
The last tier in volume billing must have **no upper limit** (no `maxQuantity`). This keeps a price on transactions above the highest defined range.
In maintenance billing, the `accountTarget` field accepts only **one** of the three options. Do not combine fields:
```json theme={null}
// ❌ Wrong
"accountTarget": {
"segmentId": "seg_abc",
"portfolioId": "port_xyz"
}
// ✅ Correct
"accountTarget": {
"segmentId": "seg_abc"
}
```
Yes. The `aliases` field accepts a maximum of **100 aliases** per maintenance Billing Package.
The `applicationRule: "flatFee"` accepts exactly 1 calculation, and it must be of type `flat`. Do not use `percentage` with `flatFee`.
```json theme={null}
// ✅ Correct
"calculationModel": {
"applicationRule": "flatFee",
"calculations": [{ "type": "flat", "value": "10.00" }]
}
```
Like `flatFee`, the `applicationRule: "percentual"` accepts exactly 1 calculation of type `percentage`.
`maxBetweenTypes` requires at least 2 calculations to work — it needs values to compare. Provide at least one `flat` and one `percentage`.
Diagnostic checklist — see also [Best Practices](/en/midaz/fees/fees-engine-best-practices):
* **`enable`:** is the package active (`enable: true`)?
* **`ledgerId`:** is the package linked to the correct ledger?
* **`minimumAmount` / `maximumAmount`:** is the transaction value within the range?
* **`transactionRoute`:** if the package has `transactionRoute`, does the transaction use the same route?
* **`segmentId`:** if the package is scoped to a segment, does the account belong to it?
* **`waivedAccounts`:** is the account listed as exempt?
Fees Engine soft-deletes records. It marks them with `deletedAt` and does not remove them from the database. The API does not expose restoration endpoints by default. Contact the Lerian team if you need to recover a deleted record. For a full list of error codes, see the [Error Codes reference](/en/reference/midaz/plugins/fees-engine/fee-engine-error-list).
# Fees Engine billing package examples
Source: https://docs.lerian.studio/en/midaz/fees/billing-package-examples
Walk through end-to-end billing package examples for boleto tiered pricing, volume discounts, and account maintenance charges with full JSON configuration.
This page is business-oriented — it focuses on *what* each billing model solves and *how* to configure it. For field-level details, see the [Billing Packages overview](/en/midaz/fees/fees-engine-overview#billing-packages) and the [API reference](/en/reference/midaz/plugins/fees-engine/create-billing-package).
## Volume billing: boleto issuance with tiered pricing
***
### The business need
A fintech offers boleto issuance to its business clients. Pricing is volume-based: the more boletos a client issues each month, the lower the unit cost. The first 50 boletos each month are free. Clients that issue 1,000 or more boletos receive an extra 5% discount. At 3,000 boletos or more, the discount increases to 10%.
### Pricing structure
| Range | Unit price |
| --------- | ---------- |
| 1–500 | R\$ 1.20 |
| 501–2,000 | R\$ 0.80 |
| 2,001+ | R\$ 0.45 |
### Package configuration
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "Boleto Issuance — Tiered",
"description": "Monthly volume billing for boleto issuance with progressive tiers",
"ledgerId": "ldg_01HZ...",
"type": "volume",
"enable": true,
"eventFilter": {
"transactionRoute": "boleto-issuance",
"status": "APPROVED"
},
"pricingModel": "tiered",
"tiers": [
{ "minQuantity": 1, "maxQuantity": 500, "unitPrice": "1.20" },
{ "minQuantity": 501, "maxQuantity": 2000, "unitPrice": "0.80" },
{ "minQuantity": 2001, "maxQuantity": null, "unitPrice": "0.45" }
],
"freeQuota": 50,
"discountTiers": [
{ "minQuantity": 1000, "discountPercentage": "5.00" },
{ "minQuantity": 3000, "discountPercentage": "10.00" }
],
"countMode": "perRoute",
"assetCode": "BRL",
"debitAccountAlias": "client-operating",
"creditAccountAlias": "fees-boleto-revenue"
}
```
### How the calculation works
At month-end, the orchestrator calls `POST /v1/billing/calculate` with the billing period. The engine counts approved boleto transactions on the route, subtracts the free quota, applies tiered pricing, and applies the matching discount.
Tiered pricing is **volume pricing**: the engine picks the single tier the billable count lands in and charges every billable unit at that tier's price. It does not bill each tier's slice separately.
**Example result** — a client that issued 1,800 boletos in March:
| Step | Detail | Amount |
| ------------- | ------------------------ | ---------------- |
| Total issued | 1,800 boletos | — |
| Free quota | 50 exempt | — |
| Billable | 1,750 boletos | — |
| Matching tier | 501–2,000 → R\$ 0.80 | — |
| Gross | 1,750 × R\$ 0.80 | R\$ 1,400.00 |
| Discount | 5% (total count ≥ 1,000) | −R\$ 70.00 |
| **Net total** | — | **R\$ 1,330.00** |
The engine returns a transaction payload that debits `client-operating` by R\$ 1,330.00 and credits `fees-boleto-revenue`.
## Maintenance billing: monthly account fee (*PF*)
***
### The business need
A digital bank charges a fixed monthly maintenance fee for active personal (PF) accounts. The fee is R\$ 9.90 per account. The engine keeps only accounts whose status code is `active` and excludes every other status. You do not filter them by hand.
Midaz organizes accounts by segment. The segment `seg_pf` groups all personal accounts.
### Package configuration
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "PF Account Maintenance",
"description": "Monthly maintenance fee for active personal accounts",
"ledgerId": "ldg_01HZ...",
"type": "maintenance",
"enable": true,
"feeAmount": "9.90",
"assetCode": "BRL",
"maintenanceCreditAccount": "fees-maintenance-pf",
"accountTarget": {
"segmentId": "seg_pf_01HZ..."
}
}
```
### How the calculation works
The engine resolves all accounts in the PF segment and filters for active status. It generates one N:1 transaction: it debits R\$ 9.90 from each active account and sends the full total to `fees-maintenance-pf`.
**Example result** — 12,000 active PF accounts:
| Detail | Value |
| :-------------------------------- | :------------------- |
| Active accounts | 12,000 |
| Fee per account | R\$ 9.90 |
| Transaction entries (source) | 12,000 debit entries |
| Transaction entries (destination) | 1 credit entry |
| **Total revenue** | **R\$ 118,800.00** |
## Volume billing: fixed-price Pix with segment exemption
***
### The business need
A fintech charges R\$ 0.10 per Pix sent — flat rate, no tiers. Premium-tier customers pay no fee. Instead of listing each premium account, you configure the exemption at the segment level.
### Package configuration
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "Pix Send — Standard",
"description": "Flat-rate billing per Pix sent",
"ledgerId": "ldg_01HZ...",
"type": "volume",
"enable": true,
"eventFilter": {
"transactionRoute": "pix-send",
"status": "APPROVED"
},
"pricingModel": "fixed",
"tiers": [
{ "minQuantity": 1, "maxQuantity": null, "unitPrice": "0.10" }
],
"freeQuota": 0,
"discountTiers": [],
"countMode": "perRoute",
"assetCode": "BRL",
"debitAccountAlias": "client-wallet",
"creditAccountAlias": "fees-pix-revenue"
}
```
### Segment exemption
To exempt premium accounts, configure the fee package for the `pix-send` route. Add a segment reference to `waivedAccounts`:
```json theme={null}
{
"waivedAccounts": [
"segment:seg_premium_01HZ..."
]
}
```
All accounts in the premium segment are exempt automatically. When accounts join or leave the segment in Midaz, the change takes effect on the next calculation. You do not update the package.
### How the calculation works
**Example result** — 5,000 Pix transactions from standard accounts:
| Detail | Value |
| :------------- | :------------- |
| Total Pix sent | 5,000 |
| Unit price | R\$ 0.10 |
| **Total** | **R\$ 500.00** |
Premium accounts show zero charges in the billing results.
## Maintenance billing: PJ portfolios with different rates
***
### The business need
A financial institution manages multiple business (PJ) account portfolios: PME (small and medium enterprises) and Corporate. Each portfolio has a different monthly maintenance fee. The institution wants to calculate both in a single billing run.
### Package configuration
**PME portfolio — R\$ 29.90/month:**
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "PJ Maintenance — PME",
"description": "Monthly maintenance for PME business accounts",
"ledgerId": "ldg_01HZ...",
"type": "maintenance",
"enable": true,
"feeAmount": "29.90",
"assetCode": "BRL",
"maintenanceCreditAccount": "fees-maintenance-pj",
"accountTarget": {
"portfolioId": "port_pme_01HZ..."
}
}
```
**Corporate portfolio — R\$ 89.90/month:**
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "PJ Maintenance — Corporate",
"description": "Monthly maintenance for Corporate business accounts",
"ledgerId": "ldg_01HZ...",
"type": "maintenance",
"enable": true,
"feeAmount": "89.90",
"assetCode": "BRL",
"maintenanceCreditAccount": "fees-maintenance-pj",
"accountTarget": {
"portfolioId": "port_corp_01HZ..."
}
}
```
### How the calculation works
A single call to `POST /v1/billing/calculate` with `"type": "maintenance"` processes both packages. The response includes one result per package and a consolidated summary.
**Example result** — 500 PME accounts + 50 Corporate accounts:
| Portfolio | Active accounts | Fee | Total |
| ---------------- | --------------- | --------- | ----------------- |
| PME | 500 | R\$ 29.90 | R\$ 14,950.00 |
| Corporate | 50 | R\$ 89.90 | R\$ 4,495.00 |
| **Consolidated** | **550** | — | **R\$ 19,445.00** |
Both results credit the same `fees-maintenance-pj` account, keeping revenue consolidated. The orchestrator sends each transaction payload to Midaz independently.
## Next steps
***
API reference for creating volume and maintenance billing packages.
Trigger billing calculations for a period and retrieve transaction payloads.
Detailed mechanics of tiered pricing, free quotas, and maintenance billing.
Operational guidance for billing packages in production.
# Connect the Fees Engine to Midaz
Source: https://docs.lerian.studio/en/midaz/fees/connecting-fees-to-midaz
Configure the embedded Fees Engine with the Midaz ledger.
Fees runs inside the unified Midaz ledger process. Configure it in the ledger deployment; it is neither a separate service nor a plugin.
## Configuration
***
Configure its MongoDB storage with `MONGO_FEES_*` settings in the Midaz deployment. `DEFAULT_CURRENCY` is optional and defaults to `USD` when unset. See [Midaz environment variables](/en/midaz/midaz-environment-variables) for ledger configuration.
## See it in Lerian
***
* The full integration journey across the pieces: [Building a complete core banking](/en/building-a-complete-core-banking)
* What the Fees Engine does: [What is Fees Engine?](/en/midaz/fees/fees-engine-overview)
* Deploy the stack: [Helm charts](/en/platform/helm/helm-overview)
# Billing Runs
Source: https://docs.lerian.studio/en/midaz/fees/console/billing-runs
Use Billing Runs in the Midaz Console to preview volume and maintenance fees for a daily, weekly, or monthly period before any charges are posted to accounts.
A **Billing Run** previews the fees for a given period, based on your configured packages. It does not charge anything — it only calculates a preview. Use it to check amounts, reconcile expected revenue, and review the results before you apply any charge.
Enable the Fees Engine in [Midaz module settings](/en/platform/console/plugin-settings) before you use this page.
## Key concepts
***
* **Preview, not a charge** — A Billing Run shows an extract of what it *would* bill for the selected period. It posts nothing until you act on the result.
* **Period type** — The length of the period: **daily**, **weekly**, or **monthly**. With a reference date, it sets the date range that the run evaluates.
* **Reference date** — The date that sets the period range. A monthly run covers the full month, a weekly run the ISO week, a daily run that day. You can also choose a **custom range**.
* **Package type** — Which packages the run includes:
* **Volume** — fees on the number of transactions.
* **Maintenance** — a fixed fee per account.
* **All** — both Volume and Maintenance packages.
* **Scope to specific packages** — Limit the run to selected packages instead of every package of the chosen type.
* **Extract** — The result of the run. It has a summary and one preview line per package. The summary shows the total results, the Volume and Maintenance totals, and a net total.
## Accessing Billing Runs
***
In the Midaz Module sidebar, expand **Fees Engine**.
Choose **Billing Runs** to open the run page.
Pick the **period type** and **reference date**, or a custom range. Choose the **package type**. To narrow the run, select specific packages.
Run the calculation to see the extract. Review the summary and the per-package results. If the period has no fees to bill, the preview shows an empty state.
## Where to go next
***
Create, edit, and delete the packages that Billing Runs evaluate.
Understand how the Fees Engine, packages, and rules fit together.
# Creating a Fee Package
Source: https://docs.lerian.studio/en/midaz/fees/console/fe-creating-a-package
Build a new Fee Package in the Midaz Module by configuring basic info, amount ranges, flat or percentage fee rules, waivers, and priorities from the Console UI.
A Fee Package groups fee rules and defines when they apply to Transactions.
## Before you start
***
* Enable the **Fees Engine** in [Midaz module settings](/en/platform/console/plugin-settings).
* You need an existing Ledger and at least one Account that can receive fee amounts.
* If the package should apply only to a specific route or segment, create those records first.
## Create the Fee Package
***
Go to the **Fee Packages** page and click the **New Package** button.
The **New Fee Package** form will open with multiple sections to configure.
Check the field guide for more information about the fields in the form.
Fill in the **Basic Information** section
Configure the **Amount Range** section
Transactions outside this range will not have fees applied from this package.
Click the **Next** button.
Select one of the options in the **Add Fee** section:
* `Flat Fee` - Fixed amount regardless of transaction value.
* `Percentage` - Percentage of the transaction amount.
* `Max Between Types` - Higher value between flat and percentage.
Add Fee information.
(Optional) You can add another fee rule if needed. To do so, select a new fee type and fill the information.
Click the **Next** button.
(Optional) Configure **Account Waivers** section:
* Add account aliases that should be exempt from fees in this package
* Click **Add** to add each waived account
Click the **Next** button.
Review the information and confirm that everything is correct.
Click the **Create Package** button.
If successful, the Console shows a confirmation message and opens the Fee Packages list.
## Field guide
***
### Basic Information
| Field | Description | Example |
| ----------------- | ------------------------------------------------------------------ | ---------------------------------- |
| Fee Package Name | Required package name. Use a name that describes the charge model. | `Standard Transfer Fee` |
| Description | An explanation of when and why the package applies. | `Fixed fee for standard transfers` |
| Transaction Route | Optional route filter for the package. | `Pix Transfer Route` |
| Segment | Optional segment filter for targeted fees. | `Retail Customers` |
| Minimum Amount | Lowest transaction amount eligible for the package. | `10.00` |
| Maximum Amount | Highest transaction amount eligible for the package. | `500.00` |
### Fee Rules
Every fee inside a package has a **Priority** (1 = applied first). The fee with priority `1` must use `Original Amount` as Reference Amount.
#### Flat Fee
A fixed amount applied to the Transaction regardless of its value.
| Field | Description | Example |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| Amount | The fixed amount charged for this fee. Must be a positive number. | `5.00` |
| Fee Name | Unique identifier for this fee, used as the JSON object key. **Must start with a letter or underscore** and contain only letters, numbers, or `_`. | `taxaAdm` |
| Reference Amount | Base used by the calculation: `Original Amount` (transaction value as sent) or `After Fees Amount` (value after previous fees in the package). | `Original Amount` |
| Credit Account ID | Alias of the account that receives the fee amount. | `@fees_transfers` |
| Route From | Optional. Name or UUID of the source route for this fee operation. | `payments_in` |
| Route To | Optional. Name or UUID of the destination route for this fee operation. | `fees_revenue` |
| Deductible from transaction? | When enabled, the fee is deducted from the transaction amount (recipient pays); when disabled, the fee is added on top (sender pays). | Off |
When you enable **Deductible from transaction?**, the form forces **Reference Amount** to `Original Amount` and disables the `After Fees Amount` option.
#### Percentage Fee
A percentage of the Transaction amount.
| Field | Description | Example |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| Percentage | Percentage rate applied to the reference amount. Must be greater than `0` and less than or equal to `100`. | `2.5` |
| Fee Name | Unique identifier for this fee, used as the JSON object key. Must start with a letter or underscore and contain only letters, numbers, or `_`. | `processingFee` |
| Reference Amount | `Original Amount` to charge on the transaction value as sent, or `After Fees Amount` to charge on the value after previous fees in the package. | `Original Amount` |
| Credit Account ID | Alias of the account that receives the fee amount. | `@fees_revenue` |
| Route From | Optional. Name or UUID of the source route for this fee operation. | `payments_in` |
| Route To | Optional. Name or UUID of the destination route for this fee operation. | `fees_revenue` |
| Deductible from transaction? | Enable so the fee is deducted from the transaction amount; disable to add the fee on top of the transaction. | `Disabled` |
#### Max Between Types
Calculates a flat amount **and** a percentage, then applies the highest result. The form requires both calculations.
| Field | Description | Example |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| Flat Fee (Calculations) | First calculation row. Fixed amount candidate. Must be a positive number. | `1.00` |
| Percentage Fee (Calculations) | Second calculation row. Percentage candidate; must be greater than `0` and less than or equal to `100`. | `2.0` |
| Fee Name | Unique identifier for this fee, used as the JSON object key. Must start with a letter or underscore and contain only letters, numbers, or `_`. | `guaranteeFee` |
| Reference Amount | Base used to calculate the percentage candidate before the comparison. | `Original Amount` |
| Credit Account | Alias of the account that receives the resulting (highest) fee amount. | `@fees_guarantee` |
| Route From | Optional. Name or UUID of the source route for this fee operation. | `payments_in` |
| Route To | Optional. Name or UUID of the destination route for this fee operation. | `fees_revenue` |
| Deductible from transaction? | When enabled, the higher of the two results is deducted from the transaction amount; when disabled, it is added on top. | `Disabled` |
The **Max Between Types** form always requires exactly two calculations: one of type `Flat Fee` and one of type `Percentage Fee`. The Console computes both and applies the highest.
### Account Waivers
| Field | Description | Example |
| ------------- | --------------------------------------------- | ------------------------- |
| Account Alias | The alias of the account that will be waived. | `@customer_john_checking` |
## Important notes
***
### Fee priority ordering
When a package contains multiple fees, the Console applies them in priority order (lowest number first). This matters when using `After Fees Amount` as a reference, since earlier fees affect the calculation base for later fees.
**Example**:
* Fee A (Priority 1): 1% on original amount
* Fee B (Priority 2): 0.5% on after fees amount
On a \$100 transaction:
* Fee A = \$1.00 (1% of \$100)
* Fee B = \$0.495 (0.5% of \$99)
### Amount range considerations
* Ranges should not overlap between packages for the same transaction route
* Use `0.01` as minimum for packages that should apply to all transactions
* Use a high maximum (e.g., `999999999.99`) for no upper limit
* The range includes Transactions exactly at the min or max values
### Waived accounts behavior
* Waived accounts bypass all fees in the package, not just specific fees
* Use account aliases (not IDs) when adding waived accounts
* Waivers apply when the waived account is the **source** of the transaction
## What to do next
***
After creating the Fee Package, run a test calculation in [Running a Fee Estimation](/en/midaz/fees/console/fe-fee-simulation) before using it with live Transactions.
[Create Package endpoint](/en/reference/midaz/plugins/fees-engine/create-package)
# Deleting a Fee Package
Source: https://docs.lerian.studio/en/midaz/fees/console/fe-deleting-a-package
Delete a Fee Package from the Midaz Module when it is no longer needed, keeping in mind data retention, audit, and compliance rules for regulated deployments.
If you only need to stop a package temporarily, disable it instead of deleting it. A deleted Fee Package no longer appears in active listings. Your deployment's data retention and compliance policies still apply to the record. In regulated environments, your deployment can keep the record for audit, regulatory, or reporting purposes.
## Delete a package
On the **Fee Packages** page, click the three dots () in the **Actions** column for the package you want to delete.
Select **Delete**.
Review the confirmation dialog.
Click **Confirm** to finalize the deletion.
# Editing a Fee Package
Source: https://docs.lerian.studio/en/midaz/fees/console/fe-editing-a-package
Edit a saved Fee Package in the Midaz Console to adjust its name, amount range, fee rules, or waived accounts without recalculating fees already applied to past Transactions.
Edit a Fee Package to change its name, amount range, fee rules, or waived accounts. Your changes apply to new fee calculations. Midaz does not recalculate fees already applied to past Transactions.
## Edit a package
On the **Fee Packages** page, find the package you want to edit.
Click the three dots () in the **Actions** column and select **Edit**.
The creation wizard opens with the current values.
Make your changes and click **Save**.
A confirmation toast appears and the updated package returns to the list.
To test the changes before you save, use the [Fee Calculator](/en/midaz/fees/console/managing-fee-calculations) on sample amounts first.
# Running a Fee Estimation
Source: https://docs.lerian.studio/en/midaz/fees/console/fe-fee-simulation
Run a fee estimation with the Console Fee Calculator to preview charges on sample amounts, accounts, and currencies before creating any real Ledger Transaction.
Use the **Fee Calculator** in the Midaz Module to estimate fees before you process a real Transaction. The Fee Calculator is read-only. It does not create a Transaction or change any Ledger balance.
## Before you start
***
* Enable the **Fees Engine** in [Midaz module settings](/en/platform/console/plugin-settings).
* You need at least one [Fee Package](/en/midaz/fees/console/fe-creating-a-package) saved.
* You need the source and destination Account aliases you want to test against.
## Running an estimation
***
In the Midaz Module sidebar, expand **Fees Engine** and click **Calculator**.
Fill in the estimated transaction. See the [Field guide](#field-guide) below for each input.
Click **Calculate Fees** to run the estimation.
Review the results displayed below the form.
## Field guide
***
| Field | What to enter | Example |
| ------------------ | ----------------------------------------- | ----------------------- |
| Transaction Amount | Total value to estimate fees against. | `100.00` |
| Currency | Asset code for the estimated transaction. | `BRL` |
| Fee Package | Fee Package to test. | `Standard Transfer Fee` |
| From Account | Account alias that would be debited. | `@customer_123` |
| To Account | Account alias that would be credited. | `@merchant_456` |
| Description | Optional description for the estimation. | `Pix transfer test` |
The Fee Calculator is a read-only tool. It does not affect the ledger or create any transactions. You can run as many estimations as needed safely.
## Understanding the results
***
After you run an estimation, the Fee Calculator shows a detailed breakdown of the calculation:
### Summary
* **Total Fee** - The sum of all fees for the transaction
* **Net Amount** - The amount the destination would receive after deductible fees
### Fee breakdown
For each fee rule applied, you'll see:
| Field | Description |
| --------------------- | ------------------------------------------------ |
| **Fee Label** | Name identifying the fee |
| **Priority** | Order in which the fee was applied |
| **Calculation Type** | `flat` or `percentage` |
| **Reference Amount** | The base amount used for calculation |
| **Calculated Amount** | The actual fee value |
| **Credit Account** | Account that would receive the fee |
| **Is Deductible** | Whether the fee is deducted from the transaction |
## Estimation examples
***
**Scenario**: Test a \$2.00 flat fee on a \$100 transfer
**Input**:
* Fee Package: `Standard Transfer Fee`
* Asset: `USD`
* Amount: `100.00`
* Source: `@customer/checking`
* Destination: `@merchant/revenue`
**Expected Result**:
* Total Fee: \$2.00
* Net Amount: \$98.00 (if deductible) or \$100.00 (if not deductible)
**Scenario**: Test a 1.5% fee on a \$500 transfer
**Input**:
* Fee Package: `Percentage Transaction Fee`
* Asset: `USD`
* Amount: `500.00`
* Source: `@customer/savings`
* Destination: `@vendor/payments`
**Expected Result**:
* Total Fee: \$7.50 (1.5% of \$500)
* Calculation: 500 × 0.015 = 7.50
**Scenario**: Test max(flat, percentage) fee on different amounts
**Test 1** - Small transaction (\$30):
* Flat: \$1.00
* Percentage: 2% = \$0.60
* Result: \$1.00 (flat is higher)
**Test 2** - Large transaction (\$200):
* Flat: \$1.00
* Percentage: 2% = \$4.00
* Result: \$4.00 (percentage is higher)
**Scenario**: Package with two fees applied in priority order
**Fee A** (Priority 1): 1% on original amount
**Fee B** (Priority 2): 0.5% on after fees amount
**Input**: \$1000 transaction
**Expected Result**:
* Fee A: \$10.00 (1% of \$1000)
* Fee B: \$4.95 (0.5% of \$990)
* Total Fee: \$14.95
## Use cases for fee estimation
***
### Validate new fee packages
Before you enable a new fee package, run estimations at various transaction amounts. Make sure the fees are correct across the full amount range.
### Test edge cases
Estimate transactions at minimum and maximum amount boundaries to verify fee behavior at the limits of your configured ranges.
### Compare fee structures
Run the same transaction through different fee packages to compare outcomes and choose the best structure for your use case.
### Document expected fees
Use estimation results to create documentation for your finance team or customers about expected fee amounts.
### Debug fee issues
If a transaction shows unexpected fees, use the Fee Calculator to reproduce the scenario and understand the calculation breakdown.
## Best practices
***
* **Test full amount ranges** - Do not test only one amount. Estimate transactions at the minimum, the maximum, and several points in between.
* **Verify priority ordering** - When packages have multiple fees, confirm they apply in the expected order.
* **Check waived accounts** - Test with accounts that the fee package waives. Confirm the results exclude them from fees.
* **Document your tests** - Keep a record of estimation results for audit and compliance purposes.
* **Share with stakeholders** - Use estimation breakdowns to align with finance teams on expected fee outcomes before going live.
## What to do next
***
After validating the fee result, adjust the Fee Package if needed in [Editing a Fee Package](/en/midaz/fees/console/fe-editing-a-package), or create a real Transaction in [Creating a Transaction](/en/midaz/console/creating-a-transaction).
[Estimate Fees endpoint](/en/reference/midaz/plugins/fees-engine/simulate-fees)
# Fees Engine in Lerian Console
Source: https://docs.lerian.studio/en/midaz/fees/console/fees-engine-overview
Explore the Fees Engine in the Midaz Module: enable it, manage Fee Packages, and estimate charges from the Console using Ledgers, segments, and routes.
The **Fees Engine** is an optional capability of the Midaz Module that lets operators model transaction fees from the Console. Once enabled, it adds two pages to the Midaz Module sidebar:
* **Fee Packages**: create and edit packages and their rules.
* **Calculator**: estimate what a package would charge before you apply it to a real Transaction.
The Fees Engine builds on the same Ledgers, Segments, Transaction Routes, and Accounts already defined in Midaz Core.
Packages use that existing structure to set their scope. Midaz applies a package automatically to each Transaction that matches its conditions.
## Key concepts
***
* **Fee Package** — Container that groups one or more fee rules and the conditions under which they apply (transaction route, segment, ledger, amount range).
* **Fee Rule** — An individual fee inside a package, with its own calculation method.
* **Application Rule** — How Midaz calculates a single fee: **flat**, **percentage**, or **maximum between both**.
* **Reference Amount** — Whether a percentage fee applies to the original Transaction amount or to the amount after other fees.
* **Priority** — The order in which Midaz applies multiple fees inside the same package.
* **Waiver** — A source account exempted from a given package.
## Enabling the Fees Engine
***
The Fees Engine appears in the sidebar only after you enable it. The [Plugin settings](/en/platform/console/plugin-settings) page shows different options for each organization license. If you do not see the Fees Engine there, contact the Lerian team.
From the Midaz Module sidebar, go to **Settings** > **Plugins**.
Toggle **Fees Engine** on. The **Fees Engine** menu item appears in the sidebar with **Fee Packages** and **Calculator** sub-items.
If you disable the Fees Engine later, Midaz hides its menu but keeps every Fee Package you created.
## Where to go next
***
Create, edit, and delete packages — define rules, scope by ledger or segment, and set waivers.
Estimate what a package would charge for a sample Transaction without running a real one.
Preview the fees for a period before you apply them.
# Fee Calculator
Source: https://docs.lerian.studio/en/midaz/fees/console/managing-fee-calculations
Preview how Fee Packages apply to a sample Transaction using the Console Fee Calculator, with a detailed breakdown of flat, percentage, and deductible fees.
The **Fee Calculator** previews how fees apply before you run a real Transaction. It uses an existing [Fee Package](/en/midaz/fees/console/managing-fee-packages) to estimate the total fees. It also shows a detailed breakdown of each rule — flat fees, percentages, and deductibles.
Use it to test changes, validate logic with business teams, or build fee previews in your product UI.
## Accessing the Fee Calculator
***
In the Midaz Module sidebar, expand **Fees Engine**.
Click **Calculator** to open the Fee Calculator page.
You need at least one [Fee Package](/en/midaz/fees/console/fe-creating-a-package) before using the Calculator — the estimator must run against a saved package.
## Why use the Fee Calculator
***
The Fee Calculator is a read-only tool that does not affect the ledger or create any transactions. Use it to:
* **Validate new fee packages** before enabling them in production
* **Test edge cases** at minimum and maximum amount boundaries
* **Compare fee structures** by running the same transaction through different packages
* **Document expected fees** for finance teams or customers
* **Debug fee issues** by reproducing scenarios and analyzing calculation breakdowns
# Managing Fee Packages
Source: https://docs.lerian.studio/en/midaz/fees/console/managing-fee-packages
Browse, create, edit, and delete Fee Packages from the Midaz Console to control how flat, percentage, and combined charges apply to matching Transactions.
**Fee Packages** are the base unit of the [Fees Engine](/en/midaz/fees/console/fees-engine-overview). Each package defines a set of fee rules and the conditions under which they apply to Transactions.
Enable the Fees Engine in [Midaz module settings](/en/platform/console/plugin-settings) before you use this page.
## What a Fee Package contains
***
A Fee Package lets you:
* **Define fee calculation rules** — flat fees, percentage-based fees, or combined calculations.
* **Set amount ranges** — apply fees only to transactions within a min/max range.
* **Link to transaction routes** — target specific transaction flows.
* **Waive fees for specific accounts** — exempt selected source accounts.
* **Prioritize fee application** — control the order in which Fees Engine applies multiple fees.
Fee Packages combine with [Accounting Routes](/en/midaz/console/managing-accounting-routes) and [Segments](/en/midaz/console/managing-segments) for granular control.
## Accessing the Fee Packages page
***
In the Midaz Module sidebar, expand **Fees Engine**. Select **Fee Packages**. The page lists every configured package.
If you have no Fee Packages, the list is empty and shows a prompt to create your first package.
### Available actions
From the Fee Packages page, you can:
Define new fee calculation rules for transactions
Update fee configurations and rules
Delete a fee package from your configuration
Select a package from the list to open its **details view**. There you can review the package's configuration — type, scope, and rules — before you edit it. To preview the fees a period generates across packages, use [Billing Runs](/en/midaz/fees/console/billing-runs).
## Understanding Fee Package structure
***
When you create or edit a fee package in Midaz Module, you work with these fields:
### Basic information
* **Fee Group Label** (required) - A descriptive name for the fee package
* **Description** (optional) - Explanation of the package purpose and when it applies
* **Transaction Route** (optional) - Link to a specific transaction route
* **Segment** (optional) - Associate with a specific segment
* **Ledger** (required) - The ledger this package applies to
### Amount range
* **Minimum Amount** (required) - Lowest transaction amount for fee application
* **Maximum Amount** (required) - Highest transaction amount for fee application
* **Waived Accounts** (optional) - List of account aliases exempt from these fees
### Fee rules
Each fee package can contain multiple fee rules with:
* **Fee Label** - Name that identifies this fee
* **Application Rule** - How Fees Engine calculates the fee:
* `flatFee` - Fixed amount regardless of transaction value
* `percentual` - Percentage of the transaction amount
* `maxBetweenTypes` - Higher value between flat and percentage calculations
* **Calculation Type** - `flat` or `percentage`
* **Calculation Value** - The fee amount or percentage
* **Reference Amount** - Base for percentage calculation:
* `originalAmount` - Calculate on the original transaction value
* `afterFeesAmount` - Calculate after Fees Engine applies other fees
* **Priority** - Order of fee application (lower numbers apply first)
* **Is Deductible From** - Whether Fees Engine deducts this fee from the transaction
* **Credit Account** - Account that receives the fee amount
## Common use cases
***
Apply a fixed fee to all transactions within an amount range:
* **Application Rule**: `flatFee`
* **Calculation**: `flat` with value `2.50`
* **Example**: $2.50 fee on every transaction between $10 and \$1000
Charge a percentage of the transaction amount:
* **Application Rule**: `percentual`
* **Calculation**: `percentage` with value `1.5`
* **Example**: 1.5% fee on transfers, where $100 transfer = $1.50 fee
Create multiple packages with different amount ranges:
* Package 1: $0-$100 → 2% fee
* Package 2: $100-$1000 → 1.5% fee
* Package 3: \$1000+ → 1% fee
* Lower fees for higher-value transactions
Apply whichever is higher between flat and percentage:
* **Application Rule**: `maxBetweenTypes`
* **Calculations**: `flat: 1.00` and `percentage: 2.0`
* **Example**: On $30 transaction: max($1.00, $0.60) = $1.00 fee
* **Example**: On $100 transaction: max($1.00, $2.00) = $2.00 fee
Exempt specific accounts from fees:
* Create package with standard fee rules
* Add premium account aliases to **Waived Accounts** list
* These accounts transact without the configured fees
## Best practices
***
Design your fee packages before implementation. Consider transaction types, amount ranges, and customer segments that require different fee treatments.
Choose clear fee group labels and descriptions that explain when and why fees apply. This helps with auditing and customer communication.
Define minimum and maximum amounts carefully to avoid gaps or overlaps in your fee structure.
Use the [Fee Estimation](/en/midaz/fees/console/fe-fee-simulation) feature to preview fee calculations before you enable packages in production.
When you use multiple fees in a package, set priorities so the fees apply in the correct order.
Record why you waive fees for certain accounts. This supports audit and compliance.
## Fee calculation flow
***
When Fees Engine processes a transaction, it:
1. Identifies applicable fee packages based on transaction route and segment
2. Filters packages by amount range (transaction must fall within min/max)
3. Checks whether the account is in the waived accounts list
4. Applies fee rules in priority order
5. Calculates each fee based on application rule and reference amount
6. Credits fee amounts to designated accounts
Enable a fee package so it applies to transactions. To pause a package, disable it. A disabled package stays in your configuration.
# Fees Engine calculations
Source: https://docs.lerian.studio/en/midaz/fees/fee-engine-calculation
Learn how the Fees Engine calculates fees, handles decimal precision, splits amounts across accounts, and applies exemptions over daily or monthly periods.
## Numeric values (string)
***
Express all financial values in **Fees Engine** as a `string` with the `numeric` type. This gives high-precision decimal handling for assets like BRL or BTC. It also prevents rounding errors during calculations, splits, or exemptions.
**Important**
* **Required**: Midaz v3.x.x (uses `numeric`).
* **Incompatible**: Midaz v2.x.x (deprecated `amount` + `scale` format).
Clients using Midaz v2.x.x must upgrade to v3.x.x to ensure proper integration and functionality with Fees Engine.
**Example:**
```
"value": "12.50"
```
## Billing periods
***
When triggering a billing calculation, you specify the time window through the `period` field. Fees Engine supports three formats:
| Format | Example | Window |
| ------- | ------------ | ------------------------------------------------------------------ |
| Daily | `2026-03-15` | Start of that day → start of the next day (UTC) |
| Weekly | `2026-W13` | Monday 00:00 UTC of the ISO week → following Monday 00:00 UTC |
| Monthly | `2026-03` | First instant of the month → first instant of the next month (UTC) |
The engine uses the period to count qualifying transactions (for volume packages) or active accounts (for maintenance packages) within that exact window.
Weekly periods follow [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Week_dates). Week numbering ranges from `W01` to `W52` (or `W53` in years that have 53 ISO weeks). The week always starts on Monday.
Choose the granularity that matches your billing cycle. A prepaid card product billed daily would use `2026-03-15`. A SaaS platform billed monthly would use `2026-03`. A marketplace that settles weekly would use `2026-W13`.
## Fee calculation rules
***
Each fee uses an `applicationRule` to define how it's calculated. You can choose from three rule types:
* [maxBetweenTypes](#maxbetweentypes)
* [flatFee](#flatfee)
* [percentual](#percentual)
You can combine different rules in a single package to match your use case.
Other key fields:
* `isDeductibleFrom`: defines if the fee is deducted from the sender or the receiver.
* `referenceAmount`: either `originalAmount` or `afterFeesAmount`.
* `priority`: defines the order of application. Priority 1 must always use `originalAmount`.
### maxBetweenTypes
Applies whichever is greater: a flat or percentage-based fee.
**Example**
* Flat fee value: R\$5.
* Percentual fee: 2%.
* Reference amount: R\$1,000.
```
rate = 1000 * 0.02 = R$ 20.00
```
Since **R\$ 20 > R\$ 5**, the engine applies the percentage-based fee.
### flatFee
Applies a fixed fee amount. Behavior depends on `isDeductibleFrom`.
**Example**
* Flat fee: R\$15.
* Reference amount: R\$115.
| `isDeductibleFrom` | Formula | Total Fee |
| :----------------- | :---------------------- | :--------- |
| `false` | `referenceAmount + fee` | R\$ 130.00 |
| `true` | `referenceAmount - fee` | R\$ 100.00 |
### percentual
Applies a fee as a percentage of the reference amount.
**Example**
* Value: 30%.
* Reference amount: R\$ 389.50.
| `isDeductibleFrom` | Formula | Total Fee |
| :----------------- | :-------------------------------------------- | :--------- |
| `false` | `referenceAmount * value` | R\$ 116.85 |
| `true` | `referenceAmount - (referenceAmount * value)` | R\$ 272.65 |
## Fee splitting
***
When a transaction has multiple source accounts, Fees Engine splits fees proportionally.
### Example
* Total amount: R\$4,000.00
* Fixed fee: R\$15.00
* Tax: 4%
* `isDeductibleFrom: false`
### Participation %
> **Formula**: (Account Amount ÷ Total Amount) × 100
| Account | Share | Amount |
| :-------- | :---- | :-------- |
| @account1 | 25% | R\$ 1,000 |
| @account2 | 25% | R\$ 1,000 |
| @account3 | 40% | R\$ 1,600 |
| @account4 | 10% | R\$ 400 |
### Fixed fee distribution
> **Formula**: `fixed Fee × participation %`
| Account | Fee Share | Total |
| :-------- | :-------- | :----------- |
| @account1 | R\$ 3.75 | R\$ 1,003.75 |
| @account2 | R\$ 3.75 | R\$ 1,003.75 |
| @account3 | R\$ 6.00 | R\$ 1,606.00 |
| @account4 | R\$ 1.50 | R\$ 401.50 |
### Proportional tax
> **Formula**: `account amount × tax %`
| Account | Tax | Total w/ Tax |
| :-------- | :-------- | :----------- |
| @account1 | R\$ 40.00 | R\$ 1,040.00 |
| @account2 | R\$ 40.00 | R\$ 1,040.00 |
| @account3 | R\$ 64.00 | R\$ 1,664.00 |
| @account4 | R\$ 16.00 | R\$ 416.00 |
### Final amount per account
> **Formula**: `principal + fee + tax`
| Account | Fee | Tax | Final Total |
| :-------- | :------- | :-------- | :----------- |
| @account1 | R\$ 3.75 | R\$ 40.00 | R\$ 1,043.75 |
| @account2 | R\$ 3.75 | R\$ 40.00 | R\$ 1,043.75 |
| @account3 | R\$ 6.00 | R\$ 64.00 | R\$ 1,670.00 |
| @account4 | R\$ 1.50 | R\$ 16.00 | R\$ 417.50 |
#### Validations
* Total shares = 100%
* Fee split matches flat fee
* Tax split = 4%
* Total sent = R\$ 4,175.00
## Fee exemptions: rules & hierarchy
***
### By transaction amount
Use `minimumAmount` and `maximumAmount` to define when **fees should apply**.
> For example: If the range is R$ 0–300, a transaction of R$ 301 won’t trigger fees.
### By account
The system checks `waivedAccounts`. A source in that list is exempt from fees.
**Hierarchy:** Value range check > then account exemption
## Mixed example: fee exemptions and proportional fee splitting
***
Let’s look at an example of a package that includes accounts with fee exemptions and requires splitting fees proportionally.
### Scenario
We’re processing a transaction of **R\$ 4,000**, which includes:
* A **fixed fee** of **R\$ 16**.
* Only some accounts are subject to the fixed fee
* An **IOF tax** of **6%** to be deducted.
#### Split on the source side
| Source Account | % | Proportional Value |
| :------------- | --- | :----------------- |
| @account1 | 15% | R\$ 600 |
| @account2 | 35% | R\$ 1,400 |
| @account3 | 40% | R\$ 1,600 |
| @account4 | 10% | R\$ 400 |
The engine applies the fixed fee only to `@account3` and `@account4`.
#### Result after Admin Fee (proportional)
| Account | Admin Fee | Total |
| :-------- | :-------- | :----------- |
| @account1 | Exempt | R\$ 600 |
| @account2 | Exempt | R\$ 1,400 |
| @account3 | R\$ 12.80 | R\$ 1,612.80 |
| @account4 | R\$ 3.20 | R\$ 403.20 |
Total send value increases to **R\$4,016**.
#### IOF deduction (recipient)
| Recipient | % | Gross | IOF (6%) | Net |
| :--------- | --- | :-------- | :------- | :------ |
| @donation1 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 |
| @donation2 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 |
| @donation3 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 |
| @donation4 | 25% | R\$ 1,000 | R\$ 60 | R\$ 940 |
The engine credits fees to the accounts defined in each fee’s `creditAccount`.
## Repeating decimals
***
When a fee split produces a repeating decimal (for example, 0.3333...), Fees Engine keeps every fee leg at full precision. It reconciles the small remainder onto the fee for the account with the largest value. This keeps the total exact, avoids rounding drift, and keeps your ledger consistent.
## Billing calculations
***
Billing packages use a different calculation model than fee packages. Instead of evaluating individual transactions, they aggregate data over a billing period and return charge payloads for your orchestrator to execute.
Billing supports three period formats: monthly (`YYYY-MM`), weekly (`YYYY-Www`, e.g., `2026-W13`), and daily (`YYYY-MM-DD`).
### Volume billing calculation
Volume billing counts transactions matching an `eventFilter` (transaction route + status) within the billing period, then applies pricing based on the configured model.
The calculation follows this order:
1. **Count** qualifying transactions in the period.
2. **Subtract** the `freeQuota` from the total count to get the billable count.
3. **Apply pricing** based on the `pricingModel` (`tiered` or `fixed`).
4. **Apply a discount** from `discountTiers`, evaluated against the **total** count (before the free quota was subtracted).
Amounts are kept at full decimal precision throughout — the engine applies no asset-scale rounding.
#### Tiered pricing
Tiered pricing is **volume pricing, not graduated pricing**. The engine finds the single tier whose quantity range contains the billable count, then charges **every** billable unit at that tier's unit price. It does not price each unit inside its own range.
A tier's range is inclusive on both ends. If you omit the upper bound, the tier is unbounded.
The engine looks up a tier only when the billable count is positive. If the billable count is positive and no tier covers it, the calculation fails for that package. A billable count of zero skips the tier lookup and produces a zero amount, so your tiers do not need to cover zero.
**Example**: A billing package for boleto issuance with three tiers and a free quota of 50:
| Range | Unit price |
| --------- | ---------- |
| 1–500 | R\$ 1.20 |
| 501–2,000 | R\$ 0.80 |
| 2,001+ | R\$ 0.45 |
For a client that issued 1,800 boletos in the month:
* 50 exempt (free quota) → 1,750 billable
* 1,750 falls in the 501–2,000 tier, so **all** 1,750 units are priced at R\$ 0.80: R\$ 1,400.00 gross
* Discount tier applies on the total count of 1,800 (≥ 1,000 → 5%): −R\$ 70.00
* **Net total: R\$ 1,330.00**
#### Fixed pricing
A single unit price applies to all billable transactions regardless of volume. The engine still subtracts the free quota before calculation. Fixed pricing takes its unit price from the **first tier** in the package, so a fixed package must still declare at least one tier — otherwise the calculation fails.
**Example**: R\$ 0.10 per Pix sent, no free quota:
* 5,000 Pix transactions × R\$ 0.10 = R\$ 500.00
#### Discount tiers
At most **one** discount tier applies: the one with the largest `minQuantity` that the total count meets or exceeds. Its percentage is applied to the gross amount. Discounts are evaluated against the total transaction count, not the billable count.
#### Count scope
Volume calculation counts transactions **per transaction route across the whole ledger**. The free quota, the tiers, and the discount tiers all apply to that route-level total. To count two flows separately, create one package per transaction route.
### Maintenance billing calculation
Maintenance billing charges a fixed amount per active account in the billing period. The engine resolves the target accounts, keeps only the active ones, and generates a single transaction payload.
The result is a N:1 transaction:
* Each active account appears as a debit entry (`source.from`) for the configured `feeAmount`.
* The `maintenanceCreditAccount` receives the full total as a single credit entry (`distribute.to`).
Only accounts whose status code is `active` are included; every other status is excluded. If no account resolves, the package returns an empty payload (`{}`) instead of a transaction.
**Example**: Monthly maintenance of R\$ 9.90 for a segment with 12,000 active PF accounts:
* 12,000 entries in `source.from`, each debited R\$ 9.90
* 1 entry in `distribute.to` credited R\$ 118,800.00
### Zero-amount results
When a package's net amount comes out zero — for example the free quota covered every transaction — the engine still returns a result for that package, but with an empty transaction payload (`{}`). Treat that as "processed, nothing to submit".
Fully exempt usage reaches this result without a tier lookup. The free quota brings the billable count to zero, the engine skips tier matching, and the amount is zero. A package whose tiers start at 1 is correct for this case.
### All-or-nothing failure policy
If any billing package fails during a `/billing/calculate` call, the entire operation fails. The engine returns no partial results. The response includes which package and resource caused the failure, so you can fix and re-execute.
### Audit metadata
Each billing calculation result carries structured metadata for traceability. Volume results include the billing type, package id and label, period, total and billable event counts, free quota used, pricing model, gross and net amounts, and the discount detail (percentage, amount, and `minQuantity`) when one applied. Maintenance results include the billing type, package id and label, period, total account count, and the per-account fee amount.
# Fees Engine best practices
Source: https://docs.lerian.studio/en/midaz/fees/fees-engine-best-practices
Follow Fees Engine best practices for naming packages, setting fee priorities, segmenting customers, and running production setups with full auditability.
Fees Engine controls how you calculate, apply, and track fees. Production use demands careful configuration and operational discipline, because accuracy and auditability are non-negotiable.
These recommendations complement the [Fees Engine overview](/en/midaz/fees/fees-engine-overview) and the [calculation mechanics](/en/midaz/fees/fee-engine-calculation) guide.
## 1. Design fee packages with clear naming and segmentation
***
Fee packages are the foundation of your fee logic. A well-organized package structure makes it easier to maintain, debug, and audit your fee configuration over time.
* **Use descriptive names** that reflect the business context (e.g., "pix-transfer-standard", "wire-premium-segment").
* **Segment by product and customer group** using `segmentId`. This allows you to apply different fee rules to different customer tiers without creating conflicting packages.
* **Keep packages focused**. A package that tries to cover too many scenarios becomes hard to test and maintain. Prefer multiple focused packages over one that does everything.
* **Document your package structure** internally. As your package count grows, a clear reference of which package applies where prevents misconfiguration.
## 2. Set fee priorities carefully
***
When a package contains multiple fees, the `priority` field determines the order of execution. Getting this wrong can produce incorrect calculations.
* **Priority 1 must always use `referenceAmount: originalAmount`**. The engine enforces this.
* **Fees with `isDeductibleFrom: true` must also use `referenceAmount: originalAmount`**. Deductible fees then always apply to the full transaction value.
* **Priority must be unique within a package**. The engine rejects duplicate priorities.
* **Think about fee dependencies**. If one fee adjusts the transaction value and another fee should be calculated on the adjusted value, use `referenceAmount: afterFeesAmount` with a higher priority number. If the second fee should reference the original value, use `originalAmount`.
When in doubt, start with a simple configuration (one or two fees per package) and validate the results using the estimate endpoint before adding complexity.
## 3. Always estimate before applying fees in production
***
Fees Engine provides an [estimate endpoint](/en/reference/midaz/plugins/fees-engine/simulate-fees) that lets you preview fee calculations without writing anything to the ledger.
Use estimation to:
* **Validate new packages** before activating them. Confirm that the calculated values match your expected results across different transaction amounts.
* **Test edge cases**: zero-amount transactions, boundary values at `minimumAmount` and `maximumAmount` thresholds, and exempted accounts.
* **Preview fees for users**. If your product shows fees before confirmation, use the estimate endpoint to provide accurate previews.
* **Debug unexpected results**. If a calculated fee doesn't match expectations, estimate the same transaction with a specific `packageId` to isolate the issue.
The [calculate endpoint](/en/reference/midaz/plugins/fees-engine/calculate-fees) automatically selects the best matching package. The estimate endpoint requires a specific `packageId`, giving you full control over which package to test.
## 4. Manage exemptions explicitly
***
Fees Engine supports two types of exemptions: by **transaction amount range** and by **account**.
* **Amount ranges** (`minimumAmount`, `maximumAmount`): Define the transaction value window in which fees apply. Transactions outside this range are exempt. Use this for promotional thresholds or tiered pricing.
* **Waived accounts** (`waivedAccounts`): Specific accounts exempt from fees within a package. Use this for internal accounts, employee accounts, or partnership arrangements.
Best practices for exemptions:
* **Keep waived account lists short and reviewed**. Large lists become hard to audit. Periodically review which accounts are exempted and why.
* **Document the business reason** for each exemption in your internal records.
* **Test exemption boundaries**. If your range is R$ 0–300, make sure transactions at exactly R$ 300 and R\$ 301 behave as expected.
## 5. Enable and tune caching for performance
***
Fees Engine caches fee packages in memory to reduce database queries during high traffic.
Configure caching through environment variables:
```yaml theme={null}
fees:
configmap:
PACKAGE_CACHE_ENABLED: "true"
PACKAGE_CACHE_TTL_SECONDS: "600"
```
* **`PACKAGE_CACHE_ENABLED`** (default: `true`): Enables or disables the package cache.
* **`PACKAGE_CACHE_TTL_SECONDS`** (default: `180`): Time-to-live in seconds before the engine refreshes cached packages from the database.
Recommendations:
* **Keep caching enabled in production**. It significantly reduces latency for high-volume transaction processing.
* **Adjust TTL based on your change frequency**. If you update packages frequently, use a shorter TTL (e.g., 60–120 seconds). If packages are stable, the default 180 seconds is appropriate.
* **Be aware of cache delay**. After you update a package, the change may take up to the configured TTL to propagate. If you need immediate effect, restart the service or temporarily reduce the TTL.
## 6. Use correct numeric values
***
Express all financial values in Fees Engine as **strings** using the `numeric` type. This prevents floating-point precision errors that are common with decimal arithmetic.
```json theme={null}
"value": "12.50"
```
* Always send values as strings, even whole numbers (e.g., `"100"` not `100`).
* Never use floating-point types for monetary calculations in your integration layer.
* Note that the engine automatically adjusts fee splits with repeating decimals (e.g., R\$ 10 divided by 3 accounts) to keep ledger totals exact.
Fees Engine requires **Midaz v3.x.x** or later. The v2.x.x `amount` + `scale` format is not compatible. Upgrade Midaz before deploying Fees Engine.
## 7. Use soft delete for auditability
***
When you delete a fee package, Fees Engine marks it with a `deletedAt` timestamp rather than removing it from the database. This preserves the audit trail for historical transactions that referenced that package.
* **Don't rely on hard deletion** for fee packages in production. Historical transactions may reference deleted packages for reconciliation.
* **Periodically review** deleted packages if your database grows significantly. Archiving strategies can help manage storage without losing audit capability.
## 8. Monitor the Fees Engine in production
***
Fees Engine supports OpenTelemetry for traces and metrics. Enable it to gain visibility into fee calculation performance and behavior.
```yaml theme={null}
fees:
configmap:
ENABLE_TELEMETRY: "true"
OTEL_RESOURCE_SERVICE_NAME: "plugin-fees"
```
In production:
* **Monitor health endpoints**. Fees Engine exposes `/health` for readiness and liveness checks (default port: 4002).
* **Set up alerts** for sustained high latency on fee calculations, which may indicate database contention or cache misconfiguration.
* **Monitor MongoDB** connection pool usage, disk space, and replication health. The default `MONGO_MAX_POOL_SIZE` is 100.
* **Review pod resource usage** against your traffic patterns and autoscaling behavior.
## 9. Keep versions compatible
***
Before upgrading Fees Engine:
* Check the [version compatibility table](/en/platform/plugins/midaz-version-compatibility) to confirm compatibility with your Midaz Core version.
* Always upgrade **Midaz Core first**, then Fees Engine.
* Back up your MongoDB data and Helm values before any major upgrade.
* Test the upgrade in a staging environment before applying it to production.
For upgrade procedures, see the [Helm upgrade guide](/en/platform/helm/midaz/midaz-upgrade-guide).
## 10. Review the security recommendations
***
Fees Engine processes financial data and integrates with Midaz ledger operations. Make sure your deployment follows the platform-wide [Security recommendations](/en/midaz/security-recommendations), which cover:
* Network segmentation and Zero Trust Architecture
* Secret management and rotation (including `LICENSE_KEY` and database credentials)
* TLS 1.2+ enforcement for all communications
* RBAC configuration via [Access Manager](/en/platform/access-manager/access-manager)
* Patch management and vulnerability scanning
## 11. Design billing packages with clear scope
***
Each billing package should represent a single, well-defined charge. Avoid packing unrelated pricing into one package.
* **Separate packages per transaction route**. A package for Pix billing and a package for boleto billing are clearer than one package that tries to handle both.
* **Use descriptive labels** that include the billing type and target: "Pix Send Monthly Billing — Standard Tier" is better than "Billing Package 1".
* **One `accountTarget` type per maintenance package**. You cannot combine `segmentId`, `portfolioId`, and `aliases` in the same package. If you need different targets, create separate packages — a single `/billing/calculate` call evaluates all active packages.
## 12. Write commercial terms against route volume
***
Volume calculation counts transactions per transaction route across the whole ledger. The free quota, the tiers, and the discount tiers all apply to that route-level total.
* **State thresholds as route volume.** "The first 100 transactions on the `pix-send` route are free" maps directly onto a package. Write the contract in the same terms the engine bills in.
* **Split routes to split counts.** One package per transaction route keeps each flow on its own quota, tiers, and discounts.
## 13. Plan free quotas and discount tiers carefully
***
Fees Engine evaluates free quotas and discounts in a specific order:
1. The engine subtracts the free quota from the total count to get the billable count.
2. The engine prices the billable count — for tiered packages it charges every billable unit at the single matching tier's rate (volume pricing, not graduated).
3. The engine applies one discount tier to the gross amount, chosen against the **total** count (before the free quota was subtracted).
Design considerations:
* **Free quotas reset each billing period**. Set the value based on your commercial agreement per period (monthly, weekly, or daily), not lifetime.
* **Tiers are volume brackets, not slices**. A billable count of 1,750 against a 501–2,000 tier prices all 1,750 units at that tier's rate. Crossing a bracket boundary changes the rate for the whole volume, so tier boundaries can move the bill sharply.
* **Cover every positive billable count.** If the billable count is positive and no tier's range contains it, the calculation fails for that package. Leave the top tier's upper bound open. Start the first tier at 1 — a billable count of zero produces a zero amount and an empty payload without a tier lookup.
* **Discount tiers are cumulative thresholds**, not ranges, and only one applies: the highest `minQuantity` the total count reaches. If you define discounts at 200 and 400 transactions, a client with 500 transactions gets the 400+ discount — not both.
* **Watch the two different counts.** Pricing uses the billable count (after free quota); the discount uses the total count (before it).
Use test calculations with known transaction counts to validate your tier and discount configuration before enabling the package in production.
## 14. Size maintenance account targets appropriately
***
Maintenance packages support three target types with different scale profiles:
| Target type | Scale | Use case |
| ------------- | --------------------- | ------------------------------------------------ |
| `segmentId` | 100,000+ accounts | Standard tiers (PF, PJ, premium) |
| `portfolioId` | Thousands of accounts | Business portfolios (PME, Corporate, Enterprise) |
| `aliases` | Up to 100 accounts | Specific named accounts |
Choose the target type that matches your operational scale. If you find yourself listing hundreds of aliases, migrate to a segment or portfolio in Midaz instead.
## 15. Handle billing failures with re-execution
***
Billing calculation follows an all-or-nothing policy. If any package fails, the engine returns no results.
* **Build retry logic** into your orchestrator. The calculation is stateless — re-executing for the same period produces the same results.
* **Check error responses** for the specific package and resource that failed. Common causes: invalid `ledgerId`, unreachable Midaz API, or disabled billing packages.
* **Separate volume and maintenance calculations** if one type consistently succeeds while the other fails. Call `/billing/calculate` with `"type": "volume"` and `"type": "maintenance"` independently to isolate failures.
# What is Fees Engine?
Source: https://docs.lerian.studio/en/midaz/fees/fees-engine-overview
Fees Engine controls how fees and billing are configured, calculated, and tracked across per-transaction and per-period scenarios.
Fees is an embedded component of the unified Midaz ledger process. It provides fee and billing configuration and calculations alongside the ledger. Deploy and configure it with Midaz; it is not a separate service or plugin.
## Why use Fees Engine?
***
The **Fees Engine** helps you manage complex fee logic. It applies flat rates, distributes fees proportionally, and estimates transactions before you run them.
Here's what it unlocks:
* **Flexible fee configuration** via fee packages—tailored to account groups or specific ledgers.
* **Multiple calculation methods**: flat fees, percentual rates, and "max between types" logic.
* **Proportional fee distribution** for marketplace flows and multi-account operations.
* **Support for accounting routes** via `transactionRoute`, `routeFrom`, and `routeTo`.
* **Estimation tools** to preview calculations before executing transactions.
* **Fee exemption logic** by account and transaction value ranges.
* **Priority-based application** to control the order of multiple fees.
* **Accurate deduction mechanics** with `isDeductibleFrom` support.
* **Volume-based billing** via billing packages — charge based on accumulated transaction counts per period (daily or monthly).
* **Maintenance billing** for recurring per-account charges, targeting accounts by segment, portfolio, or explicit list.
* **Free quotas and progressive discounts** to model tiered pricing and volume incentives.
* **Segment-based exemptions** to exempt entire account groups from fee packages without listing individual accounts.
Fees Engine is a licensed Midaz capability that runs inside the unified ledger process. Deploy and configure it with Midaz. If you'd like to learn more or evaluate it for your use case, [get in touch with our team](https://lerian.studio/contact).
## What are fees?
***
Fees are monetary values charged in exchange for services, products, or access to resources. Their purpose depends on the industry, but the need for clarity and consistency is universal. Below are just a few examples:
### Finance
In the financial sector, fees cover operational costs and support legal compliance.
* **Account maintenance fee**: Keeps accounts operational and covers administrative costs.
* **Transfer fee:** Applies to transactions like TEDs or international transfers.
### Logistics and transportation
In the logistics sector, fees cover transportation and storage services.
* **Handling fee**: Applies during storage and physical movement of goods.
* **Unloading fee**: Covers unloading operations at delivery points.
### Pharmaceutical and healthcare
In the pharmaceutical sector, fees ensure the quality and regulation of services.
* **Medication registration fee**: Related to regulatory approvals and market entry.
* **Laboratory analysis fee**: Covers testing and quality control costs.
### Agricultural
In the agricultural sector, fees cover commercialization and regulatory processes.
* **Sanitary inspection fee**: Ensures health compliance for agricultural exports.
* **Agricultural export fee**: Covers admin and regulatory export costs.
## Scope
***
Fees and Billing support organization scope and ledger scope in parallel. On the ledger-scoped v2 surface, the ledger named by the request is authoritative: a `ledgerId` query parameter is not accepted, and a body `ledgerId` must match the ledger in the request. A package owned by another ledger is returned as not found.
## Fee Packages
***
A **Fee Package** defines how the engine applies fees to a transaction. It groups one or more fee rules. You customize it by segment, ledger, and accounting routes.
You can create different packages for different products, transaction types, or customer segments. Each package has its own calculation logic, route configuration, and priority rules.
A package includes required fields and can add optional matching or exemption fields:
* **ledgerId** – The ledger that records the transaction and its fees.
* **transactionRoute** – The main accounting route for the transaction, for route-level matching.
* **segmentId** – The product or segment the package applies to, for segment-level matching.
* **waivedAccounts** – Accounts to exempt from fees, when you configure exemptions.
* **fees** – A map of individual fee rules, each including:
* **priority** – Defines execution order.
* **routeFrom** and **routeTo** – Custom accounting routes for the fee.
* **isDeductibleFrom** – Whether the engine deducts the fee from the original amount.
* **referenceAmount** – The base amount for calculations.
**Fees Engine** requires explicit route configuration for each fee and direction (e.g., debit or credit).
### Validation rules
To ensure consistency and prevent configuration errors, Fees Engine enforces the following rules:
* **Fee priority must be unique** within a package.
* Fees with `isDeductibleFrom: true` must use `referenceAmount: originalAmount`.
* Fees with `priority` 1 must also use `referenceAmount: originalAmount`.
* Fields like `organizationId`, `ledgerId`, and `creditAccount` must exist in Midaz. Fees Engine validates them with the [Retrieve an Account by Alias](/en/reference/midaz/retrieve-an-account-by-alias) endpoint.
Make sure your configuration meets the latest Midaz standards. Fees Engine validates every package and transaction against them. Check the rules for `ledgerId`, `creditAccount`, and `referenceAmount`, plus optional matching fields like `segmentId` when you configure them.
### Choosing the right endpoint: calculate vs. estimate
Fees Engine provides two endpoints to apply fees. They behave differently based on the control you need:
#### [Calculate fees for a package](/en/reference/midaz/plugins/fees-engine/calculate-fees)
* Automatically fetches all available packages for the given organization and ledger.
* Picks the best match based on transaction context.
* Applies the corresponding fee rules.
* If no package matches, the engine applies no fees.
#### [Estimate transaction fees](/en/reference/midaz/plugins/fees-engine/simulate-fees)
* Estimates fees for a **specific** package by its `packageId`.
* Returns calculated fees **only if the transaction matches** the package's conditions.
* Useful for testing, debugging, or a preview of fees, without writing to the ledger.
Use `calculate` when you want the engine to decide which package to apply. Use `estimate` when you want full control over which package to test.
### Segment-based exemptions
Fee packages support exempting individual accounts by listing their aliases in `waivedAccounts`. To exempt a whole group at once, add a segment reference to that same list using the `segment:` form — for example `"segment:seg_premium_01HZ..."`.
The package's own `segmentId` field is **not** an exemption. It scopes which package applies to a transaction (segment-level package matching). Exemptions always live in `waivedAccounts`.
Use segment-based exemptions when:
* A customer tier (such as premium accounts) is universally exempt from a fee.
* Internal or partner accounts belong to an existing segment in Midaz.
* Maintaining a list of individual account aliases is impractical at scale.
Fees Engine resolves segment-based exemptions at calculation time. When accounts join or leave the segment, the change takes effect on the next calculation. You do not update the package.
## Billing Packages
***
**Billing Packages** calculate charges from accumulated transaction volume over a period — daily or monthly. Fee packages charge per individual transaction. Billing packages count the qualifying transactions and return payloads for your orchestrator to execute.
Two types are available:
* **Volume** — charges based on the number of transactions matching an event filter in the period.
* **Maintenance** — charges a fixed recurring fee per active account, once per billing period.
Billing packages are a calculation engine, not a billing platform. The engine calculates the charges and returns the payloads. Your orchestrator — Flowker, a cron job, or any other caller — runs the actual charges against Midaz.
### Volume packages
A volume package counts transactions that match a given `eventFilter` (transaction route + status) within the billing period. It then applies a charge from the configured pricing model.
Key fields:
* **eventFilter** — Specifies which transactions to count: `transactionRoute` and `status`.
* **pricingModel** — Either `tiered` (unit price varies by quantity range) or `fixed` (single unit price regardless of volume).
* **tiers** — Quantity ranges (`minQuantity`, `maxQuantity`) and `unitPrice` per unit within each range.
* **freeQuota** — Number of transactions exempt per period. The engine subtracts this count before it applies pricing.
* **discountTiers** — Progressive discounts: when total volume meets a threshold, the engine applies the configured discount percentage to the final amount.
* **countMode** — Accepts `perRoute` or `perAccount`. Volume calculation counts all matching transactions on the route as a single total.
* **debitAccountAlias** / **creditAccountAlias** — Accounting routes for the charge.
### Maintenance packages
A maintenance package applies a fixed fee per active account in the billing period, regardless of transaction activity.
Key fields:
* **feeAmount** — Fixed charge per active account.
* **assetCode** — Currency for the charge.
* **maintenanceCreditAccount** — Account that receives the fee revenue.
* **accountTarget** — Defines which accounts to charge. Use exactly one per package:
* `segmentId` — All accounts in the segment.
* `portfolioId` — All accounts in the portfolio.
* `aliases` — Explicit list of account aliases (maximum 100 accounts).
Each maintenance package supports only one `accountTarget` type. You cannot combine `segmentId`, `portfolioId`, and `aliases` in the same package.
### Managing billing packages
The following endpoints manage billing packages:
* `POST /v1/billing-packages` — Create a billing package.
* `GET /v1/billing-packages` — List all billing packages.
* `GET /v1/billing-packages/:id` — Retrieve a specific billing package.
* `PATCH /v1/billing-packages/:id` — Update a billing package (`label`, `description`, `enable`).
* `DELETE /v1/billing-packages/:id` — Soft-delete a billing package.
* `POST /v1/billing/calculate` — Calculate billing for a period.
## Fee Packages vs. Billing Packages
***
| | Fee Packages | Billing Packages |
| --------------------- | ---------------------------------------------- | ---------------------------------------------------------- |
| **Trigger** | Per transaction (synchronous) | Per period (monthly, weekly, or daily) |
| **Pricing model** | Flat, percentual, maxBetweenTypes | Tiered or fixed by volume |
| **Account exemption** | `waivedAccounts` (aliases or `segment:`) | `accountTarget`: segment, portfolio, or alias list |
| **Volume discounts** | No | Yes (`discountTiers`) |
| **Free quotas** | No | Yes (`freeQuota` per period) |
| **Recurring charges** | No | Yes (maintenance type) |
| **Execution** | Automatic — engine evaluates each transaction | Caller-triggered — orchestrator calls `/billing/calculate` |
| **Output** | Transaction with fees applied | Calculation payloads for the caller to execute |
Fee packages and billing packages operate independently. A transaction can trigger a fee package calculation and also count toward a billing package in the same period. These are separate, non-conflicting events.
## Routing fees
***
Every fee can have:
* A `routeFrom`, which represents the accounting route for the debit (or source).
* A `routeTo`, which represents the accounting route for the credit (or destination).
* A `transactionRoute`, which represents the overall nature of the transaction.
This allows granular tracking of each fee entry in the ledger.
## Deductible fees
***
If you flag a fee as deductible ( `isDeductibleFrom: true`), the following logic applies:
* The **source account sends the full value**.
* The **engine subtracts the fee from the amount the destination account receives**.
* The `referenceAmount` for calculations must be `originalAmount`.
This way, the sender sends the full amount, and the destination account absorbs the deduction.
## Soft delete for safe recordkeeping
***
Fees Engine loses no data. When you delete a resource:
* Fees Engine marks it with a `deletedAt` timestamp. Active records return `deletedAt: null`.
* Standard queries exclude it, but the database still stores it for audit and history.
This keeps full traceability when you need it.
## Integrations
***
Use **Fees Engine** in a Midaz deployment alongside other components in your stack. You can call its capabilities from Lerian plugins or your own implementation to apply fees from your business logic.
Popular use cases include:
* Exchange engines
* Lending platforms
* Bill payment systems
* Smart contracts
* Pix (Brazil's instant payment platform)
## Security recommendations
***
**Security is foundational when you work with Lerian products and plugins.**\
Before you deploy any component, review our [**Security Recommendations**](/en/midaz/security-recommendations). Implement each product and its plugins in line with security best practices, such as:
* Securing network boundaries
* Managing and rotating secrets
* Applying timely patch management
* Enforcing strict role-based access controls (RBAC)
These practices keep Lerian's products and plugins secure and compliant across your stack.
## Next steps
***
Browse endpoints for fee packages, calculations, and estimations.
Learn how to create fee packages and apply them to transactions.
# Using Fees Engine
Source: https://docs.lerian.studio/en/midaz/fees/using-fee-engine
Use the Fees Engine to apply per-transaction fee packages and per-period billing packages that match your ledgers, segments, and transaction routes.
Fees Engine applies per-transaction fees and calculates periodic billing charges. This guide covers both workflows: fee packages (per-transaction) and billing packages (per-period).
Not sure which to use? Fee packages apply charges at transaction time. Billing packages calculate charges over a period (daily or monthly) for your orchestrator to execute. You can use both simultaneously.
## Fee packages: per-transaction workflow
***
Fee packages apply charges synchronously when you create a transaction. This section shows the process, from fee setup to results.
### Step 1 - Create your fee packages
First, set up **fee packages** that define how the engine applies fees. Use the [Create a Package](/en/reference/midaz/plugins/fees-engine/create-package) endpoint. Each package contains a set of fee rules and matching criteria.
You can tailor packages to different ledgers, segments, and transaction routes with these fields:
* **transactionRoute** - Identifies the nature of the transaction, when you need route-level matching.
* **segmentId** - Groups customers or product types, when you need segment-level matching.
* **ledgerId** - Defines which ledger records the transaction.
* **Minimum** and **maximum** amount - Optional thresholds for fee application.
* **routeFrom** / **routeTo** - Define how each fee moves across accounting flows.
This flexibility lets you apply distinct fees per scenario, from account-based setups to value-based.
**Managing Packages**
The following endpoints are also available for you to manage the packages:
* [List Packages](/en/reference/midaz/plugins/fees-engine/list-packages) - List all packages created.
* [Retrieve a Package](/en/reference/midaz/plugins/fees-engine/retrieve-package) - Retrieve information of a specific package.
* [Update a Package](/en/reference/midaz/plugins/fees-engine/update-package) - Update the information of a specific package.
* [Delete a Package](/en/reference/midaz/plugins/fees-engine/delete-package) - Soft-delete a package.
### (optional) Step 2 - Run an estimation
To preview how a fee package behaves before you commit a real transaction, use the [Estimate Transaction Fees](/en/reference/midaz/plugins/fees-engine/simulate-fees) endpoint.
This estimation helps validate:
* Which fee rules apply.
* How the package will behave with the given values.
* Whether any exemptions apply.
### Step 3 - Create a transaction
Once you set the packages, create the transaction with the [create a transaction](/en/reference/midaz/create-a-transaction-using-json) endpoint. Include `ledgerId` and any configured matching fields, such as `transactionRoute` or `segmentId`, so the engine can evaluate the right package.
### Step 4 - The Fees Engine kicks in
Fees Engine automatically calls the [Calculate Fees for a Package](/en/reference/midaz/plugins/fees-engine/calculate-fees) endpoint. It evaluates whether a package applies, based on:
* **transactionRoute**, when configured
* **segmentId**, when configured
* **ledgerId**
* **Minimum** and **maximum** amount
* **waivedAccounts**, when configured for fee exemptions
Fees Engine selects only one package per transaction.
### Step 5 - Check for exemptions
The system checks:
* If the transaction amount is outside the allowed range.
* If the source account is exempt.
If either condition is true, Fees Engine applies no fees and the transaction proceeds normally.
### Step 6 - Fee calculation and application
If a package applies, Fees Engine:
* Calculates fee values based on the selected `applicationRule`.
* Applies fees proportionally across accounts if needed.
* Uses `isDeductibleFrom` to set whether it adds or deducts the fee.
* Routes fees to the correct `creditAccount` with the configured `routeFrom` and `routeTo`.
* Returns the full transaction result along with the `packageAppliedID` in the metadata.
### Step 7 - Ledger updates
Once Fees Engine calculates the fees, the **Transactions** component takes over. It processes:
* Debits from source accounts
* Credits to fee destinations
* Fee breakdown per route and account
The ledger stores every movement for full traceability and auditability.
### Step 8 - Review and confirm
After execution, you can:
* Inspect the final transaction and amounts per account.
* Confirm which fee package applied.
* Verify all fee movements via metadata and ledger records.
## Why estimate a transaction?
***
Estimations let you preview how a specific fee package behaves, without running a real transaction or writing to the ledger.
Use estimations when:
* You want to test a specific package.
* You debug fee rules or thresholds.
* You want to validate exemptions, value ranges, or proportional splits.
* You need a preview before you create a real transaction.
* You build an interface and want to show estimated fees.
Fees Engine provides the [Estimate Transaction Fees](/en/reference/midaz/plugins/fees-engine/simulate-fees) endpoint for this purpose. You pass a `packageId`, and the endpoint returns what would happen if it applied **that exact package**.
### What do you get with an estimation?
* A full estimation of the fee rules.
* Which accounts the engine would charge.
* How the engine would split the fee.
* No impact on the ledger.
Use estimations when you're not ready to commit the transaction, or want to give your users a clear fee preview.
## Common errors
***
Fees Engine validates every request for consistency and correct fee logic. Below are the most frequent issues you might see when you create packages or process transactions.
| Code | Title | Message |
| :------- | :---------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| FEE-0002 | Missing fields in request | Your request is missing one or more required fields. Please refer to the documentation to ensure all necessary fields are included in your request. |
| FEE-0012 | Entity not found | No entity was found for the given ID. Please make sure to use the correct ID for the entity you are trying to manage. |
| FEE-0013 | Invalid fee priority | The priority field in fees is invalid. Field can not be repeated. |
| FEE-0015 | minimumAmount greater than maximumAmount | minimumAmount value is greater than maximumAmount. |
| FEE-0022 | Failed to calculate fee | Error to make the calculation of a fee about a transaction. |
| FEE-0024 | originalAmount is required when priority is one | For Priority equals to one, referenceAmount must be 'originalAmount' for fee. |
| FEE-0025 | Failed to apply rule: flatFee or percentual | applicationRule flatFee or percentual must have exactly 1 calculation for Fee. |
| FEE-0035 | Package amount range overlap | The maximumAmount and minimumAmount of the new package overlap with the amount range of an existing package. |
Want the full list of error codes? You'll find it in the [Fees Engine error list](/en/reference/midaz/plugins/fees-engine/fee-engine-error-list) page in the [API reference](/en/reference/introduction).
## Billing packages: per-period workflow
***
Billing packages calculate charges based on accumulated transaction volume or per-account maintenance over a billing period. Unlike fee packages, your orchestrator triggers billing. It decides when to calculate and executes the resulting charges.
### Step 1 — Create billing packages
Set up billing packages that define your periodic charge rules. Each package is either **volume** or **maintenance** type.
**Volume package example** — charge per Pix sent with tiered pricing:
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "Pix Send Monthly Billing",
"description": "Monthly volume billing for Pix transactions",
"ledgerId": "ldg_01HZ...",
"type": "volume",
"enable": true,
"eventFilter": {
"transactionRoute": "pix-send",
"status": "APPROVED"
},
"pricingModel": "tiered",
"tiers": [
{ "minQuantity": 1, "maxQuantity": 100, "unitPrice": "0.50" },
{ "minQuantity": 101, "maxQuantity": 500, "unitPrice": "0.35" },
{ "minQuantity": 501, "maxQuantity": null, "unitPrice": "0.20" }
],
"freeQuota": 10,
"discountTiers": [
{ "minQuantity": 200, "discountPercentage": "5.00" },
{ "minQuantity": 400, "discountPercentage": "10.00" }
],
"countMode": "perRoute",
"assetCode": "BRL",
"debitAccountAlias": "client-wallet",
"creditAccountAlias": "fees-revenue"
}
```
**Maintenance package example** — monthly fee per active PF account:
```json theme={null}
POST /v1/billing-packages
Headers:
X-Organization-Id: org_01HZ...
Body:
{
"label": "PF Account Maintenance",
"description": "Monthly maintenance fee for active PF accounts",
"ledgerId": "ldg_01HZ...",
"type": "maintenance",
"enable": true,
"feeAmount": "9.90",
"assetCode": "BRL",
"maintenanceCreditAccount": "fees-maintenance-pf",
"accountTarget": {
"segmentId": "seg_pf_01HZ..."
}
}
```
### Step 2 — Trigger billing calculation
Call `POST /v1/billing/calculate` with the ledger ID and billing period. The engine evaluates all active billing packages matching the criteria.
```json theme={null}
POST /v1/billing/calculate
Body:
{
"ledgerId": "ldg_01HZ...",
"period": "2026-03",
"type": "volume"
}
```
The `period` field supports three formats: `YYYY-MM` (monthly), `YYYY-Www` (weekly, e.g., `2026-W13`), and `YYYY-MM-DD` (daily).
The `type` field is optional. Use `"volume"` or `"maintenance"` to restrict the calculation to one type. Omit it to calculate both types in a single call.
### Step 3 — Receive calculation results
The engine returns an array of results. Each result contains a `transactionPayload` ready to send to Midaz.
Each result includes:
* The billing package that generated it.
* The calculated amounts with full breakdown (tiers applied, discounts, free quota subtracted).
* A transaction payload with `source.from` (debit entries) and `distribute.to` (credit entries).
* Structured audit metadata for traceability.
### Step 4 — Execute charges
Send each `transactionPayload` to Midaz via `POST /transactions/json` to create the actual billing transactions. This step is your orchestrator's responsibility — Flowker, a cron job, or any other system.
The billing engine calculates and returns results. It does not create transactions in Midaz. Your orchestrator controls when and how it executes charges.
### Step 5 — Review and reconcile
After executing the charges:
* Verify the created transactions in Midaz match the billing calculation results.
* Use the audit metadata in each result for reconciliation.
* The billing calculation is stateless — you can re-run it for the same period to verify results.
### Managing billing packages
Use these endpoints to manage existing billing packages:
* `GET /v1/billing-packages` — List all billing packages.
* `GET /v1/billing-packages/:id` — Retrieve a specific billing package.
* `PATCH /v1/billing-packages/:id` — Update a billing package (`label`, `description`, `enable` only).
* `DELETE /v1/billing-packages/:id` — Soft-delete a billing package.
If any package fails during calculation, the entire operation fails and returns no partial results. Billing calculation follows an all-or-nothing policy. Fix the failing package and re-execute.
# Fees Engine in Lerian Console
Source: https://docs.lerian.studio/en/midaz/fees/using-fee-engine-with-midaz-console
Define, test, and apply fees visually with the Fees Engine in the Midaz Module — no code required for fee packages and estimations.
The **Fees Engine** is available through the Midaz Module in [Lerian Console](/en/platform/console/about-lerian-console). It gives you a visual interface to define, test, and update how fees apply to transactions without writing code.
## What you can do
***
Through the Lerian Console, you can:
* **Create and manage Fee Packages**: Define fee rules with different calculation types (flat fee, percentage, or maximum between both)
* **Run fee estimations**: Test how the engine calculates fees for specific amounts before you apply them to real transactions
* **Associate fees with your data**: Link fee packages to specific ledgers, segments, or transaction routes
## Accessing the Fees Engine
***
To access the Fees Engine in Lerian Console:
1. From the home page, select **Midaz** to enter the Midaz Module
2. In the left sidebar, go to **Plugins**
3. Select **Fees Engine**
## Learn more
***
For complete documentation on using the Fees Engine through Lerian Console, see:
Complete guide to managing fees through the Lerian Console interface.
# Getting help
Source: https://docs.lerian.studio/en/midaz/getting-help
Find the right support channel for Midaz — community support for Community users and the Lerian Support Portal for Enterprise clients.
When you get stuck, this section points you to the right support channel. It serves both first-time users and teams that run Midaz in production.
Explore the resources below to get help, report issues, or contact the team.
## Content
***
Here, you will find the following pages:
* [**Community support**](/en/midaz/community-support): If you use the **Community** version of Midaz, start here. Join GitHub Discussions or Discord to ask questions, report bugs, and collaborate with others.
* [**Lerian Support Portal**](/en/midaz/lerian-support-portal): For **Enterprise** clients with a support agreement, this is the official channel. Raise tickets, request services, and get help with production issues. Your SLA sets the guaranteed response times.
# Prerequisites
Source: https://docs.lerian.studio/en/midaz/installing-and-deploying-overview
Prerequisites hub for installing and deploying Midaz — security recommendations, setup, updates, deployment strategies, Terraform, Helm, and Lerian MCP guides.
This section shows you how to install, update, and deploy Midaz. You can run it locally or provision a full cloud-native setup.
## Content
***
Here, you'll find:
* [**Security recommendations**](/en/midaz/security-recommendations): Understand shared responsibilities, best practices, and the configurations you need to deploy Midaz and its plugins securely.
* [**Installing Midaz**](/en/midaz/midaz-setup): Instructions to install and configure Midaz.
* [**Updating Midaz**](/en/midaz/updating-midaz): Instructions to update your instance when you need to.
* [**Deployment strategies**](/en/midaz/deployment): Learn about Midaz deployment strategies and platform requirements.
* [**Midaz Terraform Foundation**](/en/midaz/midaz-terraform-foundation): Set up the Midaz cloud foundation with Terraform best practices.
* [**Helm**](/en/platform/helm/helm-overview): Deploy, configure, and manage Midaz on Kubernetes with Helm (see the Platform section for full Helm documentation).
* [**Version compatibility**](/en/platform/plugins/midaz-version-compatibility): Check which plugin versions work with each Midaz release to plan safe deployments and avoid integration issues.
* [**Lerian MCP**](/en/midaz/lerian-mcp): Connect your AI assistant directly to the Midaz ecosystem. This gives your Large Language Model (LLM) secure, real-time access to Midaz documentation, APIs, and local services.
By default, Midaz does not require authentication, so you can call the Midaz APIs directly. To require authentication, use the [Access Manager](/en/platform/access-manager/access-manager) plugin. A production deployment or multi-tenant mode always requires authentication, and Midaz does not start without it. For details, [contact us](https://lerian.studio/contact).
# Lerian MCP
Source: https://docs.lerian.studio/en/midaz/lerian-mcp
Connect AI assistants like Claude, Cursor, or Windsurf to the Midaz ecosystem with secure, real-time access to docs, APIs, and local services.
Lerian MCP connects your AI assistant directly to the Midaz ecosystem. Whether you use ChatGPT, Claude, Cursor, Windsurf, or another compatible client, this server gives your large language model (LLM) secure, real-time access to Midaz documentation, APIs, and local services.
The assistant reads the docs, generates code, and runs actions for you, so you move faster.
## What is MCP?
***
MCP stands for Model Context Protocol. Think of it as Bluetooth for LLMs. It is how your AI learns what tools it can use and how your system works. It also learns what questions to ask to finish the job.
**Lerian MCP** teaches your assistant to:
* Understand documentation, architecture, and SDKs.
* Use APIs through built-in tools.
* Ask follow-up questions to guide itself.
Once connected, your assistant becomes context-aware and capable of action.
## Why use Lerian MCP?
***
**Lerian MCP** turns your AI assistant into a development companion. It helps you:
* Search Midaz documentation with natural language.
* Call local Midaz APIs through tools, not just static text.
* Understand Midaz's architecture, endpoints, and SDKs.
* Generate code, troubleshoot issues, and automate setup.
* Keep everything local and explicitly permissioned.
Whether you build new integrations or support production systems, Lerian MCP gives your LLM the context it needs, safely and instantly.
## Built for security
***
**Lerian MCP** keeps security first. It:
* Runs entirely on **your machine**.
* Has **read-only** access by default.
* Requires your explicit approval before it writes data.
* Needs no API keys for local setup.
* Is source available and auditable.
Your data stays where it belongs, under your control.
## What can your assistant do?
***
Once connected, your assistant can interact with your Midaz environment as if it has already read the documentation.
It can:
* Explain how Midaz concepts work.
* Generate code for real-world tasks (e.g., create an organization).
* Search and summarize API endpoints.
* Help debug integration issues.
* Explore the architecture and available SDKs.
* Run pre-configured tools to call your local API services.
### Examples of what you can ask
* “How do I create a transaction in Midaz?”
* “Show me the Go code to onboard an organization.”
* "What's the difference between onboarding and transaction APIs?”
* “Help me troubleshoot this 400 error.”
* “List all Midaz account types.”
## Available tools and prompts
***
**Lerian MCP** gives your assistant a small set of tools to explore documentation, call APIs, and learn through guided steps. Everything runs locally and read-first.
### Core tools
* `lerian`: the read-oriented entry point for documentation, learning, SDK examples, product discovery, and search.
* `portfolio-workflow`: runs cross-product workflows across Lerian products.
### The `lerian` tool
The `lerian` tool takes an `operation` parameter. Each operation covers one need:
* `discover`: summarize a product and the tools it exposes.
* `docs`: look up product documentation.
* `learn`: return guided learning for a topic.
* `sdk`: return SDK code examples in Go or TypeScript.
* `search`: search across product knowledge.
### Live Midaz API tools
Two tools give your assistant live access to a running Midaz:
* `midaz-discover`: returns the available resources, actions, parameters, and request schemas. It is read-only.
* `midaz-execute`: calls the Midaz API with a contract from `midaz-discover`.
Call `midaz-discover` before `midaz-execute`. A write action needs explicit confirmation and an audit reason.
### Built-in prompts
**Lerian MCP** also ships built-in prompts that guide common tasks. For example, a prompt can help you onboard a first organization, learn a concept, or debug an API call. Each prompt adapts to your experience level and role.
## How does it work?
***
**Lerian MCP** teaches your assistant using three key inputs:
#### 1. Documentation
Your LLM can immediately use anything you add to llms.txt, including product guides, examples, and concepts.
#### 2. API tools
The assistant learns how to call your APIs, including endpoints, required fields, and response formats, through structured tools.
#### 3. Prompts and workflows
It learns how to interact: what to ask, when to ask, and how to validate the next step.
### Tool invocation flow
Most assistant interactions use this core flow. When you ask your assistant to “create an organization” or “get ledger details,” it follows this sequence:
* The LLM calls the tool.
* MCP validates and enriches the input.
* The MCP server triggers the tool handler.
* The tool sends the API request.
* The tool returns the response to the assistant in real time.
The following diagram shows how a tool invocation request flows through the MCP server.
### Tool discovery and registration
Every time the MCP server starts, it registers its tools, such as `lerian`, `midaz-discover`, and `midaz-execute`. The server adapts these tools to the capabilities of each MCP client, such as Claude Desktop, Cursor, or ChatGPT.
This keeps things up to date. After the server registers a tool, your assistant knows how to use it.
The following diagram shows how Lerian MCP announces tools to your assistant and makes them usable within the client.
### Protocol-Level Error Handling
If something goes wrong, such as a malformed tool call, an unexpected backend response, or a missing configuration, the MCP server returns a standardized error. In many cases, the assistant can recover and retry with better input or fallback logic.
The following diagram shows how Lerian MCP detects, handles, and communicates protocol-level errors.
## From resources to tools
***
Lerian MCP moved from static resources to interactive tools. Your assistant can now explore, generate, test, and troubleshoot directly.
| Aspect | Resources | Documentation tools |
| :-------------- | :------------- | :---------------------------------------- |
| Client support | Limited | Full support across all MCP clients. |
| Functionality | Static content | Contextual, interactive, dynamic. |
| Examples | Basic text | Production-ready code. |
| Search | None | Fuzzy search with filters. |
| Troubleshooting | Not available | Diagnostic tools and prevention tips. |
| Interactivity | Read-only | Guided tours, demos, and code generation. |
### What the tools unlock
**Lerian MCP** tools help your assistant help you. They cover:
* **API reference**: Get detailed endpoint docs with payloads, methods, and examples.
* **Tutorials and guides**: Learn setup, onboarding, and best practices.
* **Architecture**: Explore how components connect, including optional diagrams.
* **SDK docs**: Access Go and TypeScript SDK documentation, with code examples.
* **Code generation**: Generate working snippets for tasks like account creation or fund transfers.
* **Workflow patterns**: Understand common flows like onboarding, reporting, and asset tracking.
* **Troubleshooting**: Get real-time help resolving integration issues.
* **Search and navigation**: Quickly locate relevant topics with advanced filters.
* **Exploration tools**: Run health checks, try guided tours, and explore capabilities.
Want the full reference with all tools and parameters? See the [lerian-mcp-server README on GitHub](https://github.com/LerianStudio/lerian-mcp-server).
## Getting started
***
**Lerian MCP** works locally and integrates with multiple AI assistants. All you need is [Node.js](https://nodejs.org/en/download) installed and one of the supported tools below.
Before you set it up, check which tools are available for your operating system:
| Tool | Linux | macOS | Windows | Notes |
| :--------------------- | :---------------------------------------------- | :------------------------------- | :------------------------------- | :---------------------------------------------------------------------- |
| **ChatGPT Desktop** | Unofficial | Yes | Yes | Linux builds available via Flatpak and AppImage (community maintained). |
| **Claude Desktop** | No | Yes | Yes | Not supported on Linux. |
| **Claude Code (CLI)** | Yes | Yes | Yes | Terminal-based, works anywhere with Node.js. |
| **Cursor IDE** | Yes | Yes | Yes | Electron-based, officially supports all platforms. |
| **Windsurf IDE** | Yes | Yes | Yes | Linux support available, though slightly limited. |
| **Continue (VS Code)** | Yes | Yes | Yes | VS Code extension, fully cross-platform. |
For Linux users, we recommend using **Claude Code**, **Cursor**, **Windsurf**, or **Continue** for the best experience.
Now, pick your assistant and follow the instructions below to connect Lerian MCP.
### ChatGPT Desktop
* `~/Library/Application Support/ChatGPT/mcp.json` (macOS).
* `%APPDATA%\ChatGPT\mcp.json` (Windows).
```bash Shell theme={null}
{
"mcpServers": {
"lerian": {
"command": "npx",
"args": ["@lerianstudio/lerian-mcp-server@latest"]
}
}
}
```
Restart the app.
### Claude Desktop
* `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS).
* `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
```bash Shell theme={null}
{
"mcpServers": {
"lerian": {
"command": "npx",
"args": ["@lerianstudio/lerian-mcp-server@latest"]
}
}
}
```
Restart the app.
#### Backward Compatibility
The old package name (`@lerianstudio/midaz-mcp-server@latest`) still works but is now deprecated. Migrate to `@lerianstudio/lerian-mcp-server`.
```bash Shell theme={null}
{
"mcpServers": {
"midaz": {
"command": "npx",
"args": ["@lerianstudio/midaz-mcp-server@latest"]
}
}
}
```
### Claude Code
If you use Claude Code from the command line, use the following commands:
* For a one-time setup, use:
```bash Shell theme={null}
npx --yes @lerianstudio/lerian-mcp-server
```
* To add it to Claude Code, use:
```bash Shell theme={null}
claude mcp add --scope user lerian -- npx --yes @lerianstudio/lerian-mcp-server
```
#### Migration from the old package
If you enabled the MCP with the old package `@lerianstudio/midaz-mcp-server`, follow these steps:
```bash Shell theme={null}
npm uninstall -g @lerianstudio/midaz-mcp-server
```
```bash Shell theme={null}
npm install -g @lerianstudio/lerian-mcp-server
```
```bash Shell theme={null}
npm install -g @lerianstudio/lerian-mcp-server
```
```bash Shell theme={null}
claude mcp remove midaz\
claude mcp add lerian "lerian-mcp-server"
```
### Cursor IDE
Go to **File** > **Preferences** > **Cursor Settings** > **MCP**.
Click the **+Add new global MCP Server** button.
Add the following code:
```bash Shell theme={null}
{
"mcp.servers": {
"lerian": {
"command": "npm",
"args": ["exec", "@lerianstudio/lerian-mcp-server@latest"]
}
}
}
```
Restart the app.
### Windsurf IDE
Go to **File**> **Preferences** > **Windsurf Settings**.
Click the **Manage plugins** button in the *Cascade* section.
Click **View raw config**.
Add the following code:
```bash Shell theme={null}
{
"mcpServers": {
"lerian": {
"command": "npm",
"args": ["exec", "@lerianstudio/lerian-mcp-server@latest"]
}
}
}
```
Save the file.
Click **Refresh** in the **Manage plugins** tab.
On Windsurf IDE, you must use the Cascade panel to ask about Midaz.
### Continue (VS Code)
On VS Code, install the **Continue** extension and add the Lerian MCP code to the `config.yaml`file.
You can find the file in the following locations:
* `~/.continue/config.yaml` (MacOS / Linux).
* `%USERPROFILE%.continue\config.yaml `(Windows).
You can also open the file via VS Code:
On VS Code, open the **Continue** panel from the activity bar (or press `cmd/ctrl + L`).
Click the **Assistant** selector above the main chat input.
From that dropdown, select the cog icon next to the "Local Assistant" option.
It will open the local `config.yaml`.
Add the following code and save the file:
```bash JSON theme={null}
mcpServers:
- name: Lerian
command: npx
args:
- '@lerianstudio/lerian-mcp-server@latest'
```
Close and reopen VS Code.
Open the **Continue** panel from the *Activity bar*.
## Need help?
***
#### Something’s not working?
Let’s get the basics out of the way first:
**Restart your AI assistant** after saving the configuration.
Are you editing the right config file?
Ask your assistant, “Can you access Lerian documentation?”
#### Still stuck?
* **Using Claude Desktop?** - Make sure MCP is enabled in your version.
* **Using any other AI app?** - Confirm that Node.js is installed on your machine.
* **Need a hand?** - Open a ticket on [GitHub Issues](https://github.com/lerianstudio/lerian-mcp-server/issues) .
#### Migrating from Midaz MCP?
No worries, both packages work exactly the same:
* You can use either `@lerianstudio/midaz-mcp-server` or `@lerianstudio/lerian-mcp-server`.
* Lerian MCP supports both `MIDAZ_*` and `LERIAN_*` environment variables.
* Config files work from either `.midaz/` or `.lerian/` folders.
* CLI commands `midaz-mcp-server` and `lerian-mcp-server` are interchangeable.
**How to switch:**
Point your config to `@lerianstudio/lerian-mcp-server`.
Restart your AI assistant.
(Optional) Update your env vars from `MIDAZ_*` to `LERIAN_*`.
(Optional) Move your config files to `.lerian/`.
## Ready to go?
***
Your assistant is ready. Plug in the config, restart your app, and start building.
# Lerian Support Portal
Source: https://docs.lerian.studio/en/midaz/lerian-support-portal
Open tickets, report incidents, and request services through the official Lerian Support Portal — for Enterprise clients with SLA-backed response times.
The [Lerian Support Portal](https://lerian.atlassian.net/servicedesk) is the official support channel for **Enterprise clients**. Use it to get support, report issues, and manage your service requests.
If something goes wrong, our team responds quickly.
If you use the community version of Midaz, see the [Community support](/en/midaz/community-support) page. It explains how to ask questions, report bugs, and get help from the community.
### How it works
Use the portal to open tickets, track progress, submit service requests, and report incidents.
The portal routes each request to the right team based on its type and context.
## Incident types
***
When you raise an incident, choose one of two categories:
### Non Transactional Incident
Use this option for issues with **platform and operational products** such as:
* Accounts
* CRM
* Reporter
* Lifecycle (Distr)
* Flowker
* Other non-transactional services
### Transactional Incident
Use this option for issues with **financial and payment operations** such as:
* Transactions
* Fees
* Bank transfers
* Payments
* Pix
If you are unsure which category fits, check whether the issue involves money movement or payments. If yes, choose **Transactional Incident**. If not, choose **Non Transactional Incident**.
## Request form fields
***
Both incident types use the same form. When you register a request, provide complete and accurate information. This helps us resolve your issue faster.
| Field | Description |
| :---------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Summary** | A short, clear description of the issue or request. |
| **What are the details of your request?** | A detailed explanation of what happened, the expected behavior, and any relevant technical context. This is the most important field in the form. |
| **Products** | Select the specific product related to your issue. The available options depend on the incident type you chose (Transactional or Non Transactional). |
| **Environment** | Select the environment where the issue occurred (e.g., production, staging). |
| **Business Function Impact** | Select how the issue impacts your business operations. |
| **Current State Affected Service** | Select the severity level based on the current state of the affected service. See the priority table below for guidance. |
| **Current Workaround** | Select whether a workaround is currently available. |
| **Data Integrity Security** | Select if there are any data integrity or security concerns related to the issue. |
| **Expects to be Resolved** | Select your expected resolution timeframe. |
| **Users Affected** | Select the scope of users impacted by this issue. |
| **Attachment** | Attach logs, screenshots, payloads (excluding sensitive data), or request IDs to help us investigate. |
The more context you provide, the fewer follow-up questions we need to resolve your issue.
## Priority levels and SLA
***
When you fill out the **Current State Affected Service** field, select the option that describes your service now. Each option maps to a priority level and an SLA:
| Current State Affected Service | Priority | SLA (Response Time) |
| :---------------------------------------------------------------- | :------------ | :------------------------------------- |
| **Complete outage – service is unreachable** | P1 – Critical | Response within **15 minutes** |
| **Severe degradation – service is very slow or partially broken** | P2 – High | Response within **30 minutes** |
| **Minor degradation – some features are impaired** | P3 – Medium | Analyzed during regular prioritization |
| **Cosmetic issue or minor inconvenience** | P4 – Low | Analyzed during regular prioritization |
For P1 or P2 incidents, provide as much detail as possible in the description field. This helps our team engage quickly.
## How to open an incident
***
To open an incident, follow these steps:
Go to the [Lerian Support Portal](https://lerian.atlassian.net/servicedesk).
Log in with your email and password.
Select **Incidents**.
Choose the incident type: **Non Transactional Incident** or **Transactional Incident**.
Fill in all required fields, especially the description and product selection.
Click **Submit**.
## Still need help?
***
Not sure where your issue fits, even after you check the incident types? Our team can help.
Email us at [**suporte@lerian.studio**](/). We route your message to the right team.
# Environment variables
Source: https://docs.lerian.studio/en/midaz/midaz-environment-variables
Reference for Midaz ledger service environment variables — ports, TLS, deployment mode, and CRM or Fees settings for onboarding and transaction APIs.
This reference lists the environment variables that configure the **Midaz ledger service**. Midaz is the source-available (ELv2) double-entry engine. It serves the onboarding and transaction APIs on a single port. You set these variables at deploy time, through Helm values, Docker Compose, or your orchestrator's environment. A required variable that you do not set makes the server fail on startup.
Every Lerian product shares a set of configuration blocks: TLS posture, OpenTelemetry, Access Manager authentication, multi-tenancy, service discovery, and event streaming. The [BYOC configuration reference](/en/reference/byoc-configuration) documents these blocks. This page focuses on what is distinctive to the ledger.
Consolidation has landed. You deploy the **ledger** service (unified onboarding + transaction routes), and **CRM** and **Fees** are compiled into that same ledger process — the ledger binary reads the CRM and Fees variables below. Tracer lives in the same repository and ships as its own optional service. The former `onboarding`, `transaction`, and `mdz` components no longer exist as separate deployables. The Helm chart still carries a legacy standalone `crm` deployment, disabled by default.
## Ports and health endpoints
The ledger runs one HTTP process. See the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract.
| Surface | Port variable | Default | Endpoints |
| ------------------------------------------------------------------------------ | -------------------------------- | ------- | -------------------------------- |
| Ledger HTTP (onboarding + transaction) | `SERVER_PORT` / `SERVER_ADDRESS` | `3002` | `/health`, `/readyz`, `/version` |
| CRM HTTP (legacy standalone deployment, disabled by default in the Helm chart) | `SERVER_PORT` / `SERVER_ADDRESS` | `4003` | `/health`, `/readyz` |
The ledger uses OTLP push for telemetry and does not expose a `/metrics` scrape endpoint.
## Deployment and TLS
| Variable | Description | Default | Required |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `DEPLOYMENT_MODE` | Deployment flavor: `local`, `byoc`, or `saas`. In `saas`, TLS is mandatory for every dependency connection and the server refuses to start without it. In `byoc`, TLS is recommended and warned-on rather than enforced. Also tags the `/readyz` response. | `local` | No |
| `ALLOW_INSECURE_TLS` | Bypass per-connection TLS enforcement on infrastructure DSNs. Leave unset or `false` in production; set `true` only for local plaintext infrastructure. | `false` | No |
## Application
| Variable | Description | Default | Required |
| --------------------------------- | --------------------------------------------------------------------------------------- | ------------- | -------- |
| `ENV_NAME` | Environment label (for example `development`, `staging`, `production`) | `development` | No |
| `VERSION` | Service version tag | varies | No |
| `LOG_LEVEL` | Log verbosity: `debug`, `info`, `warn`, or `error` | `debug` | No |
| `MAX_PAGINATION_LIMIT` | Maximum page size accepted by list endpoints | `100` | No |
| `MAX_PAGINATION_MONTH_DATE_RANGE` | Maximum month span for date-range queries. The bundled example configuration ships `3`. | `1` | No |
## Database (PostgreSQL)
The ledger keeps two logical databases — `onboarding` and `transaction` — each with a primary and a replica connection block. The variables share one shape. Replace `{MODULE}` with `ONBOARDING` or `TRANSACTION`. Replica variables carry a `_REPLICA_` infix (for example `DB_ONBOARDING_REPLICA_HOST`).
| Variable | Description | Default | Required |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------- | -------- |
| `DB_{MODULE}_HOST` | Primary PostgreSQL host | — | Yes |
| `DB_{MODULE}_PORT` | PostgreSQL port | — | Yes |
| `DB_{MODULE}_USER` | Database user | — | Yes |
| `DB_{MODULE}_PASSWORD` | Database password. Sensitive — do not commit; supply through your secret store. | — | Yes |
| `DB_{MODULE}_NAME` | Database name | `onboarding` / `transaction` | Yes |
| `DB_{MODULE}_SSLMODE` | libpq SSL mode: `disable`, `require`, `verify-ca`, or `verify-full`. Use `require` or stronger in production. | `disable` | No |
| `DB_{MODULE}_MAX_OPEN_CONNS` | Maximum open connections in the pool | `3000` | No |
| `DB_{MODULE}_MAX_IDLE_CONNS` | Maximum idle connections in the pool | `3000` | No |
## Document store (MongoDB)
The MongoDB variables use one namespace per module: `MONGO_ONBOARDING_*`, `MONGO_TRANSACTION_*`, and — on the consolidated binary — `MONGO_CRM_*` and `MONGO_FEES_*`. All share one shape. Replace `{NS}` with the namespace. They may point at one MongoDB deployment (separate logical databases) or dedicated hosts.
| Variable | Description | Default | Required |
| -------------------------- | ---------------------------------------------------------------------- | ------------------- | -------- |
| `MONGO_{NS}_HOST` | MongoDB host | — | Yes |
| `MONGO_{NS}_PORT` | MongoDB port | — | Yes |
| `MONGO_{NS}_USER` | Database user | — | Yes |
| `MONGO_{NS}_PASSWORD` | Database password. Sensitive. | — | Yes |
| `MONGO_{NS}_NAME` | Database name | namespace name | Yes |
| `MONGO_{NS}_URI` | Connection scheme: `mongodb` or `mongodb+srv` | `mongodb` | No |
| `MONGO_{NS}_MAX_POOL_SIZE` | Maximum connection pool size | `1000` (Fees `100`) | No |
| `MONGO_{NS}_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS (for example AWS DocumentDB) | — | No |
| `MONGO_{NS}_PARAMETERS` | Extra connection-string parameters | — | No |
## Cache (Redis / Valkey)
| Variable | Description | Default | Required |
| -------------------------------- | -------------------------------------------------------------- | ------- | -------- |
| `REDIS_HOST` | Redis/Valkey host and port | — | Yes |
| `REDIS_PASSWORD` | Authentication password. Sensitive. | — | No |
| `REDIS_TLS` | Enable TLS for the connection | `false` | No |
| `REDIS_CA_CERT` | Base64-encoded PEM CA certificate for TLS | — | No |
| `REDIS_DB` | Logical database index | `0` | No |
| `REDIS_PROTOCOL` | RESP protocol version | `3` | No |
| `REDIS_POOL_SIZE` | Connection pool size | `10` | No |
| `REDIS_MASTER_NAME` | Sentinel master name (Sentinel deployments) | — | No |
| `REDIS_USE_GCP_IAM` | Authenticate to GCP Memorystore with IAM instead of a password | `false` | No |
| `REDIS_SERVICE_ACCOUNT` | GCP service account for IAM auth | — | No |
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to GCP credentials file for IAM auth | — | No |
## Message broker (RabbitMQ)
The transaction module uses RabbitMQ for balance operations and event fan-out.
| Variable | Description | Default | Required |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `RABBITMQ_HOST` | Broker host | — | Yes |
| `RABBITMQ_PORT_HOST` | **AMQP port** used to dial the broker. Despite the name, this is the port the connection string uses (`3003` in the bundled infrastructure). | — | Yes |
| `RABBITMQ_PORT_AMQP` | **Management/HTTP port** reported on the health-check connection (`3004` in the bundled infrastructure). Despite the name, it is not used to dial AMQP. | — | No |
| `RABBITMQ_URI` | Connection scheme: `amqp` or `amqps`. Use `amqps` in production. | `amqp` | No |
| `RABBITMQ_DEFAULT_USER` | Publisher user | — | Yes |
| `RABBITMQ_DEFAULT_PASS` | Publisher password. Sensitive. | — | Yes |
| `RABBITMQ_CONSUMER_USER` | Consumer user | — | Yes |
| `RABBITMQ_CONSUMER_PASS` | Consumer password. Sensitive. | — | Yes |
| `RABBITMQ_VHOST` | Virtual host | `/` | No |
| `RABBITMQ_NUMBERS_OF_WORKERS` | Consumer concurrency | `5` | No |
| `RABBITMQ_NUMBERS_OF_PREFETCH` | Consumer prefetch count | `10` | No |
| `RABBITMQ_TRANSACTION_ASYNC` | Record transactions asynchronously through the broker | `false` | No |
| `RABBITMQ_TRANSACTION_EVENTS_ENABLED` | Publish transaction events. Any value other than `false` — including unset — enables publishing; set `false` explicitly to disable. The bundled example configuration ships `false`. | enabled | No |
| `RABBITMQ_OVERDRAFT_EVENTS_ENABLED` | Publish overdraft events. Any value other than `false` — including unset — enables publishing; set `false` explicitly to disable. The bundled example configuration ships `false`. | enabled | No |
| `AUDIT_LOG_ENABLED` | Append transactions to an audit log exchange. Any value other than `false` — including unset — enables it; set `false` explicitly to disable. The bundled example configuration ships `false`. | enabled | No |
## Throughput
| Variable | Description | Default | Required |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- |
| `BULK_RECORDER_ENABLED` | Batch transaction writes for high-throughput ingestion | `true` | No |
| `BULK_RECORDER_SIZE` | Batch size trigger. When unset, Midaz derives it from `RABBITMQ_NUMBERS_OF_WORKERS` × `RABBITMQ_NUMBERS_OF_PREFETCH` (50 with the default worker and prefetch values). | workers × prefetch | No |
| `BULK_RECORDER_FLUSH_TIMEOUT_MS` | Flush interval for the batch recorder (milliseconds) | `100` | No |
| `BULK_RECORDER_MAX_ROWS_PER_INSERT` | Maximum rows per batch insert | `1000` | No |
## Tracer integration
The optional seam lets the ledger reserve spending limits against Tracer before it commits a transaction. Leave `TRACER_BASE_URL` unset to disable it.
| Variable | Description | Default | Required |
| ---------------------- | ----------------------------------------------------------------------------- | ------- | --------- |
| `TRACER_BASE_URL` | Tracer service URL; setting it enables the reservation client | — | No |
| `TRACER_TIMEOUT_MS` | Reservation call deadline (milliseconds) | `250` | No |
| `TRACER_TRANSPORT` | Reservation transport: `grpc` or `rest` | `grpc` | No |
| `TRACER_TLS_MODE` | Seam security: `mesh` (default, TLS terminated by the service mesh) or `mtls` | `mesh` | No |
| `TRACER_TLS_CERT_FILE` | Client certificate PEM path (when `mtls`) | — | If `mtls` |
| `TRACER_TLS_KEY_FILE` | Client private-key PEM path (when `mtls`). Sensitive. | — | If `mtls` |
| `TRACER_TLS_CA_FILE` | CA certificate PEM path (when `mtls`) | — | If `mtls` |
With `TRACER_BASE_URL` set, the seam uses the default `grpc` transport unless you set `TRACER_TRANSPORT=rest`. The gRPC transport requires the Tracer service to expose its reservation gRPC seam — set `TRACER_GRPC_PORT` on Tracer (see [Tracer environment variables](/en/tracer/tracer-environment-variables)). Under `TRACER_TLS_MODE=mtls`, you must set the client certificate, key, and CA paths above.
## CRM and Fees
The ledger process reads these variables — CRM and Fees are compiled into the ledger binary. They protect account-holder PII and configure the field-encryption backend.
| Variable | Description | Default | Required |
| ---------------------------- | --------------------------------------------------------------------------------------- | ------- | --------- |
| `LCRYPTO_HASH_SECRET_KEY` | 64-hex hashing key for holder PII. Sensitive — generate a unique value per environment. | — | Yes (CRM) |
| `LCRYPTO_ENCRYPT_SECRET_KEY` | 64-hex encryption key for holder PII. Sensitive. | — | Yes (CRM) |
| `KMS_VENDOR` | Field-encryption backend: `none` or `hashicorp-vault` | `none` | No |
| `KMS_VAULT_ADDR` | Vault address (when `hashicorp-vault`) | — | No |
| `KMS_VAULT_AUTH_METHOD` | Vault auth method: `token` or `approle`. Use `approle` in `byoc`/`saas`. | `token` | No |
| `KMS_VAULT_ROLE_ID` | Vault AppRole role ID (when `approle`) | — | No |
| `KMS_VAULT_SECRET_ID` | Vault AppRole secret ID (when `approle`). Sensitive. | — | No |
| `DEFAULT_CURRENCY` | Fallback fee currency (ISO 4217) | `USD` | No |
## Shared configuration backbone
The following blocks are identical across Lerian products. The [BYOC configuration reference](/en/reference/byoc-configuration) documents them in full. They default off. A single-tenant BYOC deployment can ignore all the optional ones.
* **Access Manager authentication** — `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_HOST`. Enable in production.
* **Multi-tenancy** — `MULTI_TENANT_*`. Off by default. Enables per-tenant database resolution.
* **Service discovery** — `SD_*` (Consul). Off by default.
* **Event streaming** — `STREAMING_*` (lib-streaming producer). Off by default on the ledger.
* **OpenTelemetry** — `ENABLE_TELEMETRY`, `OTEL_*`. Telemetry is OTLP push.
# Installing Midaz
Source: https://docs.lerian.studio/en/midaz/midaz-setup
Install Midaz locally with Docker Compose in a few commands — the full source-available environment, ready for local development.
Midaz is a source-available ledger. You run, extend, and test it locally. Install it with **Docker Compose**, which sets up the full environment.
This guide walks you through the full setup, whether you build something new or explore how Midaz works.
Midaz now ships as a single repository. You no longer install its services separately.
## System requirements
***
| Requirement | Minimum |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Operating System** | - **Windows**: Windows 10 **via WSL** version 2004 or later (Build 19041+) - **macOS**: Current or previous two versions - **Linux**: Ubuntu or Debian recommended |
| **RAM** | 8 GB |
| **Disk Space** | 20 GB |
| **Internet** | Stable connection |
| **Browser** | Chrome, Firefox, or equivalent |
## Tools you’ll need
***
* [Docker Desktop](https://docs.docker.com/get-docker/)
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Postman](https://www.postman.com/downloads/) (optional)
* Package Manager:
* macOS: [Homebrew](https://brew.sh/)
* Linux: Use your system’s package manager (e.g., `apt`, `dnf`)
## On Windows? Install WSL
***
On Windows, you must install WSL (Windows Subsystem for Linux) to run Midaz.
Open PowerShell as an administrator.
Run:
```bash Text theme={null}
wsl --install
```
Restart your machine if needed.
Once installed, open the Ubuntu app from the Start Menu to continue the setup.
## Install Midaz
***
### Step 1 – Open your terminal
* **macOS/Linux:** Use your Terminal app.
* **Windows:** Open Ubuntu via WSL.
On Windows, **run all commands inside Ubuntu (WSL)**. New to terminals? See [this beginner guide](https://www.codecademy.com/learn/learn-the-command-line).
### Step 2 – Install `make`
* **Linux:**
```bash Text theme={null}
sudo apt install make
```
* **macOS:**
```bash Text theme={null}
brew install make
```
### Step 3 – Download the Midaz repo
```bash Text theme={null}
cd ~/Desktop
```
```bash Text theme={null}
git clone https://github.com/LerianStudio/midaz.git
```
```bash Bash theme={null}
cd midaz
```
### Step 4 – Set up environment variables
Docker must already be running: besides copying the `.env` files, `make set-env` generates the `LCRYPTO_*` CRM keys through a Docker one-shot container and fails if Docker is unavailable.
```bash Bash theme={null}
make set-env
```
### Step 5 – Start the services
Make sure Docker is up, then:
```bash Bash theme={null}
make up
```
All bundled services start.
### Step 6 – Access your environment
* Ledger API: [http://localhost:3002](http://localhost:3002/)
Make sure Docker is up and all `.env` files are set before you access the API.
## Try the API
***
Want to explore the Midaz APIs?
Go to `midaz/postman`.
Import `MIDAZ.postman_collection.json` into Postman.
Explore available endpoints.
Ready to build? Start with our [Quick Start Guide](/en/reference/quick-start-guide).
## Authentication
***
By default, Midaz does not require authentication. This makes local testing easier.
If you need authentication, use the [Access Manager](/en/platform/access-manager/access-manager) plugin, available for enterprise customers.
Run Midaz without authentication only in a secured, private environment.
## Community and contribution
***
* Join us on [Discord](https://discord.com/invite/DnhqKwkGv3).
* Report issues on [GitHub](https://github.com/LerianStudio/midaz).
* Follow [@LerianStudio](https://twitter.com/LerianStudio) on Twitter.
Want to contribute? Start with our [Contributing Guide](https://github.com/LerianStudio/midaz/blob/main/CONTRIBUTING.md).
Need expert help? [Talk to our team](https://lerian.studio/contact) for hands-on support.
## License
***
Midaz is source-available under the [Elastic License 2.0](https://github.com/LerianStudio/midaz/blob/main/LICENSE).
# Midaz Terraform Foundation
Source: https://docs.lerian.studio/en/midaz/midaz-terraform-foundation
Provision the base infrastructure for Midaz on AWS, GCP, or Azure with ready-made Terraform examples — networking, databases, and Kubernetes.
Midaz Terraform Foundation is a repository of ready-made Terraform examples. Use them to create the base infrastructure that Midaz needs on AWS, GCP, or Azure. The examples follow each cloud provider's best practices.
This base infrastructure includes:
* Network (VPC, subnets)
* DNS
* Database
* Redis/Valkey
* Kubernetes cluster (EKS, GKE, or AKS)
The templates provision a MongoDB-compatible database and a message broker only on some providers. AWS uses Amazon DocumentDB and Amazon MQ (RabbitMQ). Azure uses Cosmos DB with the MongoDB API. GCP has no managed equivalent, so you must provision MongoDB and RabbitMQ yourself on GCP.
## Why use it
***
Infrastructure provisioning should not be slow, inconsistent, or error-prone. `midaz-terraform-foundation` follows Lerian's best practices for security, observability, and scalability. The tables below compare it to a manual or ad-hoc setup.
### Speed and standardization
| **Criteria** | **With Midaz Terraform** | **Manual Setup / Ad-hoc Scripts** |
| :------------------------ | :----------------------------------------------------------------- | :------------------------------------------- |
| **Setup speed** | **Fast** – provisions everything in minutes with a single `apply`. | **Slow** – takes days to configure and test. |
| **Architecture standard** | **Standardized** – follows Lerian's best practices. | **Unpredictable** – may be inconsistent. |
| **Reusability** | **High** – supports multiple environments with minimal changes. | **Low** – hard to reuse across projects. |
### Security and observability
| **Criteria** | **With Midaz Terraform** | **Manual Setup / Ad-hoc Scripts** |
| :------------------------- | :-------------------------------------------------------------- | :------------------------------------------------------ |
| **Security by default** | **Yes** – secure by design (isolated VPCs, IAM, secrets, etc.). | **No** – depends on the team, increasing exposure risk. |
| **Built-in observability** | **Built-in** – integrates with Prometheus, Grafana, and more. | **Manual** – requires separate setup, often skipped. |
| **Production-ready?** | **Yes** – high availability and autoscaling out of the box. | **Uncertain** – needs extra effort to harden. |
### Maintenance and support
| **Criteria** | **With Midaz Terraform** | **Manual Setup / Ad-hoc Scripts** |
| :---------------------------- | :----------------------------------------------------- | :-------------------------------------------- |
| **Maintainability** | **Easy** – modular and versioned for painless updates. | **Hard** – scripts break easily. |
| **Lerian support** | **Included** – verified and supported by Lerian. | **None** – not guaranteed. |
| **Estimated deployment time** | **1 day** – including validation. | **1–2 weeks** – with higher operational risk. |
Use this repository for a faster, tested setup.
`midaz-terraform-foundation` follows Lerian's engineering standards. It helps you deploy faster and avoid common setup errors.
## What you’ll need
***
Before you start, make sure you have:
* [Terraform v1.5.0 or higher](https://developer.hashicorp.com/terraform/install) — the AWS RDS and Route 53 examples require `>= 1.5.0`; the other modules require `>= 1.0.0`
* A cloud provider account (AWS, GCP, or Azure).
* A storage bucket for Terraform state files.
* The CLI tool for your cloud provider:
* `aws` for AWS
* `gcloud` for GCP
* `az` for Azure
### CI/CD Integration
This repository provides Terraform examples to deploy foundation infrastructure. It **does not include a CI/CD pipeline**. Create one that fits your project's needs.
Do you already run a Terraform CI/CD pipeline? Follow these steps:
**Skip the deployment script.** It is for local use only.
Copy the relevant example configs into your private Infrastructure as Code repo.
Integrate the Terraform configs into your pipeline as needed.
Use your CI/CD platform’s built-in secret management to handle credentials securely.
## Project structure
***
Each cloud provider has its own structure in the repository. Every infrastructure component follows a modular, controlled layout. You can deploy only the components you need, or the entire foundation.
```bash theme={null}
.
├── examples/
├── aws/
│ ├── vpc/
│ ├── route53/
│ ├── rds/
│ ├── documentdb/
│ ├── amazonmq/
│ ├── valkey/
│ └── eks/
├── gcp/
│ ├── vpc/
│ ├── cloud-dns/
│ ├── cloud-sql/
│ ├── valkey/
│ └── gke/
└── azure/
├── network/
├── dns/
├── database/
├── cosmosdb/
├── redis/
└── aks/
```
### Deployment order matters
To avoid errors and connect everything correctly, deploy the components in this order:
1. VPC / Network
2. DNS
3. Database
4. Redis/Valkey
5. Kubernetes cluster
## Creating the state storage
***
Terraform requires a remote backend to manage its state. Before you use these templates, create a storage bucket for the Terraform state files.
### AWS
**Replace `REGION` and `UNIQUE_BUCKET_NAME` with your own values.**
```
aws s3api create-bucket \
--bucket UNIQUE_BUCKET_NAME \
--region REGION \
--create-bucket-configuration LocationConstraint=REGION
```
```
aws s3api put-bucket-versioning \
--bucket UNIQUE_BUCKET_NAME \
--versioning-configuration Status=Enabled
```
```
aws s3api put-bucket-encryption \
--bucket UNIQUE_BUCKET_NAME \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
```
```
aws s3api put-public-access-block \
--bucket UNIQUE_BUCKET_NAME \
--public-access-block-configuration \ '{"BlockPublicAcls":true,"IgnorePublicAcls":true,"BlockPublicPolicy":true,"RestrictPublicBuckets":true}'
```
### Google Cloud Platform
```
gsutil mb -l us-central1 gs://your-terraform-state-bucket
```
```
gsutil versioning set on gs://your-terraform-state-bucket
```
### Azure
```
az group create --name terraform-state-rg --location eastus
```
```
az storage account create --name tfstate$RANDOM --resource-group terraform-state-rg --sku Standard_LRS
```
```
az storage container create --name terraform-state --account-name
```
## Configuration requirements
***
Before you deploy the infrastructure, create and configure the variables file for each cloud component:
```
cd examples//
cp midaz.tfvars-example midaz.tfvars
```
Replace all placeholders in the `midaz.tfvars` file with your actual values. \\
i. **This file holds the key configuration for your infrastructure setup.**
## Production credentials and deployment
***
In production environments, you must manage credentials with care. This guide shows how to handle credentials securely.
### Cloud provider authentication
When you run the deploy script locally, use the cloud provider's CLI authentication tools instead of raw credentials. This method is more secure. It manages credential rotation, MFA, and token refresh automatically.
**Why adopt this approach?**
* Tokens refresh automatically.
* MFA and SSO integration out of the box.
* It rotates and stores credentials securely.
* Full audit trail for authentication events.
#### AWS
Use AWS CLI to assume a role.
```
aws sso login --profile your-profile
```
or
```
aws sts assume-role --role-arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME --role-session-name terraform
```
#### GCP
Use gcloud authentication.
```
gcloud auth application-default login
```
**For service accounts**, use the following code:
```
gcloud auth activate-service-account --key-file=path/to/service-account.json
```
#### Azure
Use Azure CLI.
```
az login
```
For service principals, use the following code:
```
az login --service-principal
```
### Credential management best practices
Stay safe and compliant by following your cloud provider’s official guidance:
* **AWS**: [Managing AWS access keys](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html).
* **GCP**: [Managing service account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys).
* **Azure**: [Identity management best practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/identity-management-best-practices).
#### Recommended practices
* Rotate credentials on a regular schedule.
* Use role-based access control (RBAC) wherever possible.
* Require MFA for user accounts.
* Prefer short-lived, temporary credentials.
* Monitor and audit credential use.
* **Never** commit credentials to version control.
## Using the deploy script
***
The `deploy.sh` script handles the setup sequence, highlights issues, and deploys each component in the correct order.
### What it does
* Allows you to pick your cloud provider (AWS, Azure, or GCP).
* Offers options to deploy or destroy the stack.
* Checks that all backend configuration placeholders have values.
* Runs Terraform commands in the right order for each component.
* Outputs clear, color-coded logs so you know what’s happening at every step.
### How to use it
Make sure that all **prerequisites are complete** and that you **created your remote state bucket**.
Fill in all the **placeholders** in the `backend.tf` files.
```
chmod +x deploy.sh
```
```
./deploy.sh
```
When prompted, select your cloud provider.
i. Check the remaining placeholders. \\
ii. Run `terraform init`, `plan`, and `apply` for each component. \\
iii. Deploy in the correct order and stop if something fails.
### Error handling
We built the script to fail quickly and provide an explanation. If something goes wrong, it will:
* Stop immediately if it finds placeholders you forgot to fill in.
* Exit if any Terraform command fails.
* Show you exactly which component failed and at what step.
## Installing Midaz
***
After deploying the foundation infrastructure, you can install Midaz using Helm. For more information, refer to the [Deploying using Helm](/en/platform/helm/midaz/midaz-installation) page.
#### Prerequisites
* A running Kubernetes cluster (EKS, GKE, or AKS).
* `kubectl` configured to access the cluster.
* Helm v3.x installed.
* Access to the [Midaz Helm repo](https://github.com/LerianStudio/helm).
### Install steps
Add the Midaz Helm repository:
```
helm repo add midaz https://lerianstudio.github.io/helm
helm repo update
```
Create a values file (`values.yaml`) with your configuration:
```bash expandable theme={null}
# Example values.yaml
# Disable the bundled dependencies
valkey:
enabled: false
postgresql:
enabled: false
## Point the ledger at your external PostgreSQL and Valkey/Redis.
## The ledger serves the onboarding and transaction modules in one process,
## so the DSNs are namespaced per module (DB_ONBOARDING_* / DB_TRANSACTION_*).
ledger:
configmap:
DB_ONBOARDING_HOST: "postgresql.midaz.internal"
DB_ONBOARDING_USER: "midaz"
DB_ONBOARDING_NAME: "onboarding"
DB_ONBOARDING_PORT: "5432"
DB_ONBOARDING_REPLICA_HOST: "postgresql-replica.midaz.internal"
DB_ONBOARDING_REPLICA_USER: "midaz"
DB_ONBOARDING_REPLICA_NAME: "onboarding"
DB_ONBOARDING_REPLICA_PORT: "5432"
DB_TRANSACTION_HOST: "postgresql.midaz.internal"
DB_TRANSACTION_USER: "midaz"
DB_TRANSACTION_NAME: "transaction"
DB_TRANSACTION_PORT: "5432"
DB_TRANSACTION_REPLICA_HOST: "postgresql-replica.midaz.internal"
DB_TRANSACTION_REPLICA_USER: "midaz"
DB_TRANSACTION_REPLICA_NAME: "transaction"
DB_TRANSACTION_REPLICA_PORT: "5432"
# REDIS_HOST carries host and port together.
REDIS_HOST: "valkey.midaz.internal:6379"
secrets:
DB_ONBOARDING_PASSWORD: ""
DB_ONBOARDING_REPLICA_PASSWORD: ""
DB_TRANSACTION_PASSWORD: ""
DB_TRANSACTION_REPLICA_PASSWORD: ""
REDIS_PASSWORD: ""
```
Install Midaz:
```
helm install midaz midaz/midaz -f values.yaml
```
For detailed configuration options and advanced setup, please refer to the [Midaz Helm Repository](https://github.com/LerianStudio/helm).
## Security tips
***
The cloud brings opportunities and responsibilities. To keep your Midaz infrastructure secure, follow these recommendations:
* Always use **private Kubernetes clusters** to limit public exposure.
* Access the **Kubernetes API via VPN** instead of allowing public access.
* Set up and **enforce RBAC** (Role-Based Access Control) to manage user permissions effectively.
* Store all secrets in the cloud provider’s **secret management service**.
* Give service accounts only the **permissions they truly need**.
## Contributing
***
Before you make any changes, set up Git hooks. Git hooks make sure every commit follows our standards and passes the required checks.
```
make hooks
```
```
git checkout -b feature/your-feature
```
Make your changes and commit using Conventional Commits.
Open a pull request targeting the `develop` branch.
After tests pass and a maintainer approves, your changes merge into `main`.
Check out our [Contributing Guide](https://github.com/LerianStudio/lerian-terraform-foundation/blob/main/CONTRIBUTING.md) to learn more about how we work together and what we expect from contributors.
## License
***
Midaz Terraform Foundation uses the [Apache License 2.0](https://github.com/LerianStudio/lerian-terraform-foundation/blob/main/LICENSE).
## Need help?
***
* Check the README inside each component folder.
* Search existing [issues](https://github.com/LerianStudio/lerian-terraform-foundation/issues).
* Open a new issue if needed.
# Observability
Source: https://docs.lerian.studio/en/midaz/observability-in-midaz
Monitor Midaz metrics, logs, and traces with OpenTelemetry, Client Collectors, and Lerian's managed Prometheus, Loki, Tempo, and Grafana observability stack.
Midaz gives you metrics, logs, and traces in one place. Monitor performance and find issues before they affect your customers.
Observability in **Midaz** lets you monitor metrics, logs, and traces across your deployed environments. You understand system behavior, find issues, and keep operations healthy.
Midaz collects telemetry with the **OpenTelemetry framework**. A **Client Collector** in your infrastructure processes it. The collector then routes it securely to Lerian's managed stack: Prometheus, Loki, Tempo, and Grafana.
## How it works
***
Midaz uses a **dual observability model**:
* **Client side** — For local development, the collector configuration lives in `components/infra/grafana/otelcol-config.yaml`, mounted read-only into the bundled all-in-one Grafana/OTel container. It sets the collector's processors, filters, and exporters. For production and BYOC, configure the `otel-collector-lerian` subchart values instead.
* **Lerian side** — Lerian operates the managed collector and storage stack. Lerian keeps it secure and consistent.
The bundled collector config masks known sensitive keys — credentials, tokens, `legalDocument`, `accountAlias` — in log bodies and in the `app.request.payload` span attribute. Masking happens in the collector, so it applies after telemetry leaves the application process. The bundled local-development exporters run with TLS disabled (`insecure: true`); enable TLS on the collector exporters for any non-local deployment.
## Key benefits
***
* Unified view of metrics, logs, and traces in Grafana
* Built on OpenTelemetry standards
* Grafana preconfigured and operated by Lerian
* Secure routing and tenant isolation
**Want full configuration details?** See [**Infra Tools → Observability**](/en/platform/observability) for advanced setup, collector configuration, and processor customization.
# Security
Source: https://docs.lerian.studio/en/midaz/security
See how Midaz protects financial operations with security by design, STRIDE threat modeling, OWASP-aligned controls, and a clear shared responsibility model.
Security is the foundation of any financial system.
Midaz builds in protections for data integrity, access control, and tenant isolation. It uses strong identity management, fine-grained permissions, and industry-standard practices.
This page describes the security architecture Midaz provides by default. It also shows how you run secure and compliant operations.
## Architecture
***
Midaz uses a secure architecture. It applied **security by design** and **threat modeling** from the start. It still applies both to every new feature.
* **Security by design**: Midaz embeds security controls across the lifecycle, from design to deployment. It follows OWASP guidelines such as the **OWASP Top 10** and the **OWASP Application Security Verification Standard (ASVS)**.
* **Threat Modeling**: A structured process identifies, assesses, and reduces security risks before an attacker exploits them. Midaz uses the **STRIDE** methodology. STRIDE groups threats into six types:
* **S**poofing (e.g., fake authentication in a banking API).
* **T**ampering (e.g., altering transaction data mid-request).
* **R**epudiation (e.g., lack of audit logs for transactions).
* **I**nformation Disclosure (e.g., leaking sensitive data via API responses).
* **D**enial of Service (e.g., overwhelming the API with fake requests).
* **E**levation of Privilege (e.g., exploiting a bug to gain admin access).
## Shared responsibility model
***
Security is a **shared responsibility** between Lerian and the customer. The exact split depends on your [deployment model](/en/deployment-models).
| Lerian | Customer |
| ---------------------------- | ---------------------------- |
| Application Development | Infrastructure |
| Application Security Updates | Network |
| | Database |
| | Identity & Access Management |
| | Encryption |
| | User Data |
| | Monitoring |
| | Additional Security Layers |
### In the BYOC model
In BYOC (Bring Your Own Cloud), you deploy Lerian in your own infrastructure. Lerian secures the **application layer**. You secure the **environment**.
**What Lerian secures:**
* Secure-by-design services.
* Proactive vulnerability fixes.
* Security updates: dependency upgrades, security patches, and improvements.
**What the customer secures:**
* **Infrastructure**: Harden the OS and container images, manage patches, and apply secure configurations on the hosting platform.
* **Network**: Implement segmentation, firewalls, and IDS/IPS systems. Adopt Zero Trust principles to protect internal and external communication.
* **Database**: Set up backups and audit logging. Follow security best practices for data storage.
* **Identity & Access Management**: Control access to the environment. Use Midaz's RBAC features to enforce least-privilege policies within the platform.
* **Encryption**: Encrypt sensitive data at rest and in transit. Consider tokenization or anonymization where appropriate.
* **User Data**: All user data that Midaz stores or processes stays under your control and responsibility.
* **Monitoring**: Establish monitoring tools that detect unusual access patterns or suspicious behavior.
* **Additional Security Layers**: Reinforce defenses with Web Application Firewalls (WAF), anti-DDoS mechanisms, and bot mitigation tools.
### In the SaaS model
In SaaS, Lerian manages the full infrastructure. Lerian takes on more of the security responsibility.
**What Lerian secures:**
* Everything in the BYOC application layer, plus:
* Cloud infrastructure, networking, and compute environment.
* Database provisioning, encryption at rest, and automated backups.
* OS and container patching.
* Monitoring, alerting, and incident response.
* High availability and disaster recovery.
**What the customer secures:**
* **Business-level access control**: Manage users, roles, and permissions within the platform.
* **API integration security**: Secure the communication between your systems and the Lerian APIs.
* **User Data governance**: Define and enforce data-handling policies that meet your regulatory obligations.
* **Compliance**: Make sure your use of the platform aligns with your institution's regulatory requirements.
For step-by-step guidance, see [Security Recommendations](/en/midaz/security-recommendations) in the Installing & Deploying section.
## Identity and Access Management
***
Midaz accepts a **Bearer JWT** issued by an OAuth 2.0 / OpenID Connect provider. Authentication is off unless you set `PLUGIN_AUTH_ENABLED=true` — and Midaz refuses to start without it when `ENV_NAME=production` or multi-tenancy is enabled. You choose how to manage identity and access. You have two options:
* **Use your own external IAM** (Identity and Access Management) solution.
* **Use Lerian's native Access Manager Plugin** — a good fit for customers without an existing IAM system, or those who want a fully integrated experience.
### Option 1: External IAM
If you integrate your own IAM provider, make sure it follows modern security practices. To keep Midaz secure, we recommend that you:
* Use proven protocols such as **OAuth 2.0** and **OpenID Connect**.
* Require **Multi-Factor Authentication (MFA)**.
* Apply strong password hashing algorithms such as **bcrypt** or **argon2**.
* Enforce fine-grained access controls with **RBAC**, **ABAC**, or similar models.
* Manage sessions securely, with expiration rules and refresh-token policies.
* Protect endpoints from brute-force and replay attacks.
* Enable and review **access logs** regularly.
### Option 2: Access Manager Plugin
The [Access Manager Plugin](/en/platform/access-manager/access-manager) manages authentication and authorization inside Midaz. It is fully integrated, and it provides:
* User lifecycle management
* Session token handling
* Refresh token rotation
* Application registration and management
This option simplifies secure access control. It aligns with Midaz's native permission system (RBAC).
## Tenant isolation in multi-tenant deployments
***
On Lerian SaaS or BYOC Multi-Tenant, Midaz isolates all resources by tenant at the application layer. This covers organizations, ledgers, accounts, and transactions.
Your JWT access token carries the tenant context. On every request, the platform middleware resolves the tenant from the `tenantId` claim in the token. Your API calls never see data from other tenants. Other tenants never see your data.
This isolation works independently of the organization hierarchy. Two tenants can create similar organization structures, and their data stays completely separate. In `DATABASE` [isolation mode](/en/multi-tenancy#isolation-modes), each tenant uses its own dedicated database. In `SCHEMA` mode, tenants share a database, but each tenant's data stays isolated behind its own distinct credentials.
### Credential isolation
Each tenant has its own credentials. Midaz never shares them between tenants, in both `DATABASE` and `SCHEMA` [isolation modes](/en/multi-tenancy#isolation-modes). Under `SCHEMA` mode, tenants share a database instance. Even so, each tenant's access uses its own distinct credentials. One tenant can never authenticate into another tenant's data.
Midaz generates credentials during [automatic provisioning](/en/multi-tenancy/auto-provisioning). It stores them in a **credentials vault**, not in configuration files or environment variables. You can rotate credentials on demand while the platform continues to serve requests. The rotation causes no downtime and no interruption to tenant operations.
### Per-tenant resource limits
Multi-tenant deployments enforce resource limits so that no single tenant degrades others. This is the **noisy neighbor** problem:
* **Kubernetes resource limits** — CPU and memory limits on each workload cap how much compute it can consume. This bounds the impact of a spike or a runaway workload.
* **PostgreSQL statement timeout** — Midaz does not set one itself. Configure `statement_timeout` on your PostgreSQL roles or databases so no single query can hold resources indefinitely.
* **Connection pool per service** — each service keeps its own connection pool, with one pool per active tenant. Per-tenant pool limits come from the connection settings each tenant carries in Tenant Manager, and idle tenant pools are evicted over time. This bounds connection capacity per tenant. One tenant cannot exhaust the database connections that others need.
These limits complement the data isolation above. Data isolation protects each tenant's *data* from other tenants. Resource limits protect each tenant's *performance and availability* from other tenants' load.
For a full overview of multi-tenancy, see [Multi-tenancy](/en/multi-tenancy).
## Data protection
***
Midaz enforces **double-entry principles** by design. Every transaction must have balanced debits and credits. Midaz rejects any entry that fails this validation. This protects **ledger integrity**. It also protects the system against **race condition vulnerabilities** and posting discrepancies.
### Built-in safeguards
Midaz applies strict validation across all transaction flows to keep data consistent and prevent logic errors:
* **Midaz blocks negative balances**, unless you explicitly permit them.
* **Midaz verifies account status** before any operation.
* **Midaz requires a registered, valid asset** before it posts an entry.
These checks reduce risk. They keep financial operations aligned with business rules and regulatory expectations.
### Compliance with LGPD and GDPR
Midaz handles transaction validation and secure communication over **TLS 1.2 and 1.3**. You protect **personally identifiable information (PII)**. To stay compliant with **LGPD**, **GDPR**, and similar data protection laws, we recommend that you:
* Apply **encryption** to sensitive data, at rest and in transit.
* Use **tokenization** or **anonymization** when appropriate.
* Store and manage customer data under clearly defined security policies.
Data protection is a shared commitment, not a single layer. Midaz provides the foundation. You build the safeguards.
## Responsible disclosure policy
***
Transparency builds trust. We share all known security improvements and fixes openly in our [**GitHub Discussions**](https://github.com/LerianStudio/midaz/discussions). This keeps the community informed about security patches and enhancements.
If you find a security vulnerability in Midaz, report it directly to our team before you make it public. We support responsible disclosure. We investigate and resolve issues quickly and thoroughly.
Do not disclose any findings publicly until we review and address them.
The steps to report a vulnerability:
Email us at [security@lerian.studio](mailto:security@lerian.studio).
We respond within 24 hours.
Our team validates the report.
We determine the severity and impact.
We fix the issue and notify the reporter.
We coordinate disclosure with the researcher.
Use a PGP key for secure communication. We prioritize confidentiality and fast resolution of all security reports.
# Security recommendations
Source: https://docs.lerian.studio/en/midaz/security-recommendations
Apply Lerian's shared responsibility security model — infrastructure hardening, network segmentation, TLS, and Zero Trust patterns.
Read this guide before you install and deploy Midaz. Security is critical.
At Lerian, we work under a **Security Shared Responsibility Model**. We secure the application layer: Midaz and all Lerian plugins. You secure the infrastructure where they run. This guide helps your team build and keep a strong security posture across the Lerian ecosystem, from Midaz to every integrated plugin.
These recommendations are not exhaustive. They complement your existing security practices, whether you run in the cloud or on-prem.
## Infrastructure & network security
***
Midaz and all Lerian plugins are cloud-native. They run in containerized, distributed environments. To strengthen your infrastructure and network security:
* Establish a **patch management process** that updates the OS and services with security patches on a regular schedule.
* Apply **hardening** procedures to your operating system images before deployment.
* Use the **Midaz and plugin Docker images**, which follow the **least privilege principle**. Reinforce this at the cluster level with the right controls.
* For **Kubernetes**, enforce security baselines with **policy-as-code** tools such as **Kyverno** or **OPA Gatekeeper**.
* Run **routine vulnerability scans** across all layers of your infrastructure, including the layers that host plugins.
* Use **network segmentation** to restrict access to Midaz components and plugins to only the systems that need it.
* Configure **firewalls** to manage traffic flow and prevent unauthorized access.
* Where possible, implement **Intrusion Detection Systems (IDS)** and **Intrusion Prevention Systems (IPS)** to analyze network behavior and block anomalies.
* Adopt a **Zero Trust Architecture** for sensitive systems and data flows.
* Enforce **TLS 1.2 or higher** for all communication across Midaz and plugin services.
## General security configurations
***
You must protect secrets, storage, and data across all Lerian components, including Midaz and its plugins. Follow these practices:
* Use trusted solutions such as **AWS Secrets Manager**, **Azure Key Vault**, **Google Secret Manager**, or **HashiCorp Vault** to store secrets and configuration data securely.
* **Rotate credentials and secrets** on a regular schedule. Follow the principle of least privilege when you grant access.
* Enable **audit logging**. Configure **automated backups** for all databases that Midaz and plugins use.
* Apply **encryption at rest** to all persistent data, including the data that plugins such as Access Manager process.
* Enforce **access restrictions** on sensitive data. Use **tokenization**, **encryption**, or **anonymization** where appropriate.
* Update Midaz and all Lerian plugins to the **latest releases**, including patches and security improvements.
## Identity & Access Management (IAM)
***
Apply a unified IAM approach across two layers — the environment where you deploy Midaz and plugins, and the services themselves. Follow these practices:
* Implement a centralized IAM process to manage access to the infrastructure that hosts Lerian products.
* Enforce **multi-factor authentication (MFA)** across all user and admin accounts.
* Monitor and audit access to **service accounts** on a regular schedule, especially the accounts that plugins use.
* Define access control models that follow the **least privilege principle** with **RBAC**, **ABAC**, or custom strategies.
* Use **temporary credentials** and time-limited access for high-sensitivity operations.
* Review all access privileges on a regular schedule, including access to Midaz, Access Manager, Reporter, and other products.
## Monitoring & incident response
***
To build observability and incident readiness into how you deploy Midaz and plugins:
* Deploy security monitoring tools to track behavior across all Lerian services, including plugin activity.
* Enable logs and traces across your infrastructure and each component of Midaz and its plugins.
* **Integrate logs** from Midaz and plugin services with fraud detection or anomaly monitoring tools.
* Centralize logs in a **SIEM platform** for more efficient threat detection.
* Maintain an updated **incident response plan**, with specific playbooks for services such as Access Manager or Fees Engine.
## Additional protection layers
***
For production-critical or high-compliance environments, add these protective layers:
* Apply OS and service **hardening procedures** across your entire stack.
* Maintain **regular patch cycles** for all runtime environments, including third-party services and dependencies.
* Conduct **manual penetration tests** to validate your defenses, including the defenses around plugins and integrations.
* Deploy **Web Application Firewalls (WAFs)** to protect plugin APIs and Midaz endpoints from Layer 7 attacks.
* Use **Anti-DDoS services** and **bot mitigation tools** to block automated or mass-scale attacks on exposed components.
Combine infrastructure-level protections with the built-in security validations of **Midaz and its plugins**. Together they create a compliant, scalable financial environment.
## Midaz security configurations
***
Security starts with configuration. Review and adjust all credentials and certificates before you go live, whether you run Midaz locally or deploy it in a managed cluster.
### Credentials and secrets
Each Midaz component has its own `.env` file for per-module configuration. This gives you flexibility and a shared responsibility.
Never deploy Midaz with default credentials. Update all secret values in your environment before you launch.
Review and update the following files to protect sensitive information:
| **Midaz Repo** | **Midaz Helm** |
| :----------------------------- | :-------------------------------------------------- |
| `/components/infra/.env` | `/charts/midaz/values.yaml` |
| `/components/mdz/.env` | `/charts/midaz/templates/onboarding/secrets.yaml` |
| `/components/onboarding/.env` | `/charts/midaz/templates/transactions/secrets.yaml` |
| `/components/transaction/.env` | `/charts/midaz/templates/console/secrets.yaml` |
Store secrets such as database credentials, encryption keys, and API tokens securely. Rotate them on a regular schedule.
### TLS Certificate configuration
When you expose Midaz services through ingress controllers, use valid TLS certificates. Valid certificates secure the communication between services and external clients.
You can enable ingress for the following Midaz components:
* **Transaction**
* **Onboarding**
* **Console**
To configure a custom TLS certificate:
**Generate** a valid certificate with a trusted provider (for example, Let’s Encrypt or GoDaddy).
**Store** the full certificate chain as a Kubernetes secret or in an external vault (for example, AWS Secrets Manager).
**Reference** the secret in your `values.yaml` file:
```bash YAML theme={null}
ingress:
///class, host, annotations, etc
tls:
- secretName: certificate-tls
hosts:
- midaz.example.com
```
Always enforce TLS 1.2 or higher. For internal clusters, issue short-lived certificates automatically with a service mesh or cert manager.
# Updating Midaz
Source: https://docs.lerian.studio/en/midaz/updating-midaz
Update your Midaz environment safely — local Docker upgrades and Helm-based rollouts to keep your installation secure and current.
Keeping Midaz up to date means you're always working with the latest features, security patches, and improvements. Regular updates also keep everything running smoothly with other components and help prevent issues caused by outdated dependencies.
Below are the steps to update Midaz in your local environment and using Helm.
From Midaz 3.x onwards, schema migrations are applied by dedicated migration-runner images (`midaz-ledger-migrations`, `midaz-tracer-migrations`) that run **before** the app rollout — the apps no longer migrate at startup. Local `make up` and the official Helm chart wire this up automatically. See [Database migrations](/en/midaz/database-migrations) for details.
## Updating Midaz locally
***
If you're running Midaz locally for development or testing, follow these steps to update your environment:
### 1. Stop the running containers
Shut down all running services before making updates. This ensures a clean restart with the latest updates.
To stop the containers via the terminal, follow these steps:
Open your terminal and navigate to the `midaz` directory.
Run the stop command:
```Shell Shell theme={null}
make down
```
### 2. (Optional) Clean up unused Docker resources
Cleaning up unused resources helps keep your environment efficient:
* **Free up disk space**: Prevent old images, containers, and volumes from eating up storage.
* **Avoid conflicts with outdated dependencies**: Ensure you're working with the latest configurations.
* **Improve performance**: Reduce unnecessary Docker cache layers and optimize resource management.
* **Prevent debugging headaches**: Eliminate inconsistencies caused by outdated data lingering in the system.
* **Keep your workspace tidy**: Maintain an organized development setup, especially when switching between projects.
To remove unused images and containers:
```Shell Shell theme={null}
docker system prune --all
```
To remove unused Docker volumes:
```Shell Shell theme={null}
docker volume prune
```
### 3. Update your local codebase
Fetch the latest changes from the repository to ensure you're working with the most recent version:
```Shell Shell theme={null}
git pull origin main
```
### 4. Rebuild and restart Midaz
Rebuild necessary Docker images and relaunch the application locally with the latest updates:
```Shell Shell theme={null}
make up
```
## Updating Midaz with Helm
***
For deployments using Helm, use the following commands based on your needs. Helm simplifies Kubernetes-based deployments, making it easy to install, upgrade, or remove Midaz.
### Fresh installation
Perform a fresh installation when setting up Midaz for the first time or when a clean deployment is required.
#### Install Midaz Helm with a custom values file
Use this method if you have specific configuration requirements defined in `values.yaml`:
```Shell Shell theme={null}
helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \
--values values.yaml --version -n midaz --create-namespace
```
#### Install Midaz Helm with default values
Use this method to install Midaz using the default Helm chart values:
```Shell Shell theme={null}
helm install midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \
--version -n midaz --create-namespace
```
### Updating an existing deployment
If Midaz is already deployed, use these commands to upgrade to a new version while preserving configurations.
#### Updating Midaz Helm with a custom values file
Ensure that custom configurations remain intact while applying the update:
```Shell Shell theme={null}
helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \
--values values.yaml --version -n midaz --create-namespace
```
#### Updating Midaz Helm with default values
Use this command if you do not need to apply custom configurations:
```Shell Shell theme={null}
helm upgrade midaz oci://registry-1.docker.io/lerianstudio/midaz-helm \
--version -n midaz --create-namespace
```
## Managing the Helm release
***
Use these commands to check and manage your Midaz Helm deployment.
### List installed releases
List all Helm releases in the `midaz` namespace, along with their status and version:
```Shell Shell theme={null}
helm ls -n midaz
```
### Delete the Midaz Helm release
If you need to completely remove Midaz from your cluster, run:
```Shell Shell theme={null}
helm delete midaz -n midaz
```
By keeping Midaz updated, you ensure a stable, secure, and high-performing environment while benefiting from the latest enhancements and best practices.
```Shell Shell theme={null}
helm delete midaz -n midaz
```
# Midaz in Lerian Console
Source: https://docs.lerian.studio/en/midaz/using-midaz-in-lerian-console
Manage Midaz visually inside the Lerian Console — Organizations, Ledgers, Accounts, Transactions, and plugins from a single web interface.
Midaz is a product module in the [Lerian Console](/en/platform/console/about-lerian-console), the web interface for all Lerian products.
Through the Console, you manage your full ledger infrastructure — from organizations and assets to transactions and plugins.
Access the full documentation for using Midaz in the Lerian Console.
## Available actions
***
From the Lerian Console, you can:
* **Foundation** — Manage [organizations](/en/midaz/console/managing-organizations), [ledgers](/en/midaz/console/managing-ledgers-via-console), and [assets](/en/midaz/console/managing-assets)
* **Accounting** — Configure [account types](/en/midaz/console/managing-account-types) and [routes](/en/midaz/console/managing-accounting-routes)
* **Accounts** — Manage [accounts](/en/midaz/console/managing-accounts), [holders](/en/midaz/console/managing-crm-holders), [aliases](/en/midaz/console/managing-crm-alias-accounts), [segments](/en/midaz/console/managing-segments), and [portfolios](/en/midaz/console/managing-portfolios)
* **Transactions** — [View and create transactions](/en/midaz/console/managing-transactions)
* **Plugins** — Configure integrated capabilities such as [Fees Engine](/en/platform/console/mc-console-plugins)
# Contexts and sources
Source: https://docs.lerian.studio/en/matcher/configuration/matcher-contexts-and-sources
Set up a reconciliation context and its sources in Matcher — pick 1:1, 1:N, or N:M cardinality, add bank and ledger feeds, and trigger match runs.
Contexts and sources are how you tell Matcher **what** to reconcile and **where the numbers come from**. They are the two building blocks you set up before any matching happens.
* A **context** is a single reconciliation you care about — for example, *"our main bank account vs. our books."* It sets the scope: which systems are compared, which rules apply, and over what period.
* A **source** is one of the systems feeding numbers into that comparison — a bank statement, an ERP export, a payment processor's settlement file, or a ledger.
Every context compares exactly two sides against each other, so each one needs at least two sources. Get these right and everything downstream — matching, exceptions, and reporting — follows.
## What is a reconciliation context?
***
A reconciliation context defines the operational boundaries of a reconciliation process.
It specifies:
* Which data sources are compared
* Which matching rules apply
* How exceptions are handled
* The time window covered by reconciliation
**Common examples:**
* *Bank Account 1234 vs General Ledger* (daily bank reconciliation)
* *Payment Gateway vs Revenue System* (payment reconciliation)
* *Intercompany Entity A vs Entity B* (intercompany reconciliation)
## Context types
***
Matcher lets you use different reconciliation cardinalities based on transaction structure.
### One-to-one (1:1)
Each transaction is reconciled against a single counterpart.
**Typical use cases:**
* Bank statements
* Direct payment matching
### One-to-many (1:n)
One transaction is reconciled against multiple counterparts.
**Typical use cases:**
* Split payments
* Batch deposits
* Consolidated invoices
### Many-to-many (n:m)
Multiple transactions are reconciled across multiple counterparts.
**Typical use cases:**
* Netting arrangements
* Complex payment allocation
* Multi-leg financial flows
## Creating a reconciliation context
***
Once you know what you're reconciling, create the context. At this stage you're mainly declaring the cardinality (`type`), a required execution label (`interval`), and any fee tolerance the comparison should allow. The `interval` value does not schedule runs; automatic execution requires a separate [reconciliation schedule](/en/matcher/configuration/matcher-schedules). A new context starts in `DRAFT` and remains there until you explicitly activate it.
#### Request
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily Bank Reconciliation",
"interval": "daily",
"type": "1:1",
"feeToleranceAbs": "0",
"feeTolerancePct": "0",
"feeNormalization": "NET",
"autoMatchOnUpload": false
}'
```
#### Context fields
Descriptive name for the context
Matching cardinality: `1:1`, `1:N`, or `N:M`
Required execution label (e.g. `daily`, `weekly`). It does not schedule runs.
Absolute fee tolerance for amount comparison, as a decimal string (e.g. `"0.01"`)
Percentage fee tolerance for amount comparison, as a decimal string (`"0.5"` means 0.5%)
Optional fee normalization mode: `NET` or `GROSS`. Omit it to leave fee normalization disabled.
Automatically trigger a match run when a file is uploaded
#### Response
```json theme={null}
{
"id":"019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f",
"tenantId":"11111111-1111-1111-1111-111111111111",
"name":"Daily Bank Reconciliation",
"type":"1:1",
"interval":"daily",
"status":"DRAFT",
"feeToleranceAbs":"0",
"feeTolerancePct":"0",
"feeNormalization":"NET",
"autoMatchOnUpload":false,
"createdAt":"2026-02-02T16:31:22Z",
"updatedAt":"2026-02-02T16:31:22Z"
}
```
API Reference: [Create context](/en/reference/matcher/create-context)
## Running reconciliation
***
A context doesn't reconcile on its own — you trigger a **match run**. A run applies the context's active rules to the transactions in its sources, then produces matches and exceptions. You can trigger runs by hand, or let a [schedule](/en/matcher/configuration/matcher-schedules) fire them automatically.
Every run works in one of two modes:
| Mode | What it does |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DRY_RUN` | Does not persist matching results, transaction mutations, fee artifacts, or exceptions, but does persist a completed `MatchRun` and its statistics |
| `COMMIT` | Executes matching and persists the results |
Trigger a run for a context:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "COMMIT"
}'
```
By default a run is **synchronous** — it executes in-request and the response carries the final status. For large volumes, set `"async": true` to submit the run and poll its progress instead. Asynchronous submission requires the match-run worker to be enabled; otherwise Matcher rejects `"async": true` with HTTP 503.
Both modes return **HTTP 202 Accepted**, so read the response `status`, not the HTTP code, to know the outcome.
A synchronous run returns a terminal `COMPLETED` or `FAILED`; an async run returns `QUEUED`, and you poll `GET /v1/matching/runs/{runId}`.
While in flight, a run moves through `PROCESSING` and `FINALIZING` (treat both as not-yet-done) before reaching `COMPLETED` or `FAILED`.
To review past runs, list a context's run history with `GET /v1/matching/contexts/{contextId}/runs`.
API Reference:
* [Run match](/en/reference/matcher/run-match)
* [List match runs](/en/reference/matcher/list-match-runs)
## What is a source?
***
A source represents a system or data feed that supplies transactions to a reconciliation context.
Each context requires at least two sources.
**Typical sources include:**
* Bank statement feeds
* ERP general ledger exports
* Payment processor transaction streams
* Internal accounting systems
## Adding sources to a context
***
A context needs at least two sources — one for each side of the comparison. The `side` field (`LEFT` or `RIGHT`) declares which side a source feeds; Matcher reconciles the `LEFT` side against the `RIGHT` side. Assign one side to each source and keep the assignment consistent.
Create a source with a `name`, `type`, `side`, and a `config` object. Leave `config` empty (`{}`) when the source needs no connection-specific settings — as with a bank feed on the `LEFT` side:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Chase Bank - Account 1234",
"type": "BANK",
"side": "LEFT",
"config": {}
}'
```
Point the other side at a second source. `config` carries source-specific connection and parsing settings when they're needed — for example a payment gateway on the `RIGHT` side:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Payment Gateway",
"type": "GATEWAY",
"side": "RIGHT",
"config": {
"currency": "USD",
"provider": "stripe"
}
}'
```
`name`, `type`, and `side` are required (`name` is 1–50 characters). `config` is optional and defaults to an empty object when omitted.
API Reference: [Create source](/en/reference/matcher/create-source)
### Source types
| Type | Description | Typical use |
| --------- | ----------------------- | -------------------------------------------------------- |
| `LEDGER` | Internal ledger | Internal accounting systems (including Midaz) |
| `BANK` | Bank statement feed | External bank feeds |
| `GATEWAY` | Payment gateway | Payment processors (Stripe, Adyen, PayPal) |
| `CUSTOM` | Bespoke feed | Any other data source |
| `FETCHER` | Discovery-engine source | Aggregator connections supplied through a source binding |
### Fetcher sources
`FETCHER` identifies a source type; it does not enable automatic pulling by itself. Create it like any other source, then wire the upstream aggregator connection through a [source binding](#source-bindings) on the query rail (`connectionId`) — see [Discovery](/en/matcher/integrations/matcher-discovery) for how connections are set up.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Open Banking Aggregator",
"type": "FETCHER",
"side": "LEFT",
"config": {
"provider": "pluggy"
}
}'
```
## Managing sources
***
Sources support a full CRUD lifecycle under `/v1/contexts/{contextId}/sources`. You can rename or reconfigure a source at any time, and **archiving is soft and reversible**. An archived source is excluded from context readiness, matching, and source listings, but keeps its full history until you restore it. Archiving does not disable its bindings; disable or delete them separately to stop scheduler dispatch.
| Action | Method & path | Notes |
| -------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Create source | `POST /v1/contexts/{contextId}/sources` | Body: `name`, `type`, `side`, `config` (see above). |
| List sources | `GET /v1/contexts/{contextId}/sources` | Lists sources in the context. |
| Get source | `GET /v1/contexts/{contextId}/sources/{sourceId}` | Retrieves a single source by id. |
| Update source | `PATCH /v1/contexts/{contextId}/sources/{sourceId}` | Updates mutable source fields (e.g. `name`, `config`). |
| Archive source | `POST /v1/contexts/{contextId}/sources/{sourceId}/archive` | Excludes the source from readiness, matching, and listings; bindings remain enabled until changed separately. |
| Restore source | `POST /v1/contexts/{contextId}/sources/{sourceId}/restore` | Reactivates a previously archived source. |
API Reference:
* [Create source](/en/reference/matcher/create-source)
* [Get source](/en/reference/matcher/retrieve-source)
* [Update source](/en/reference/matcher/update-source)
* [Archive source](/en/reference/matcher/archive-source)
* [Restore source](/en/reference/matcher/restore-source)
## Source bindings
***
Bindings define how the binding scheduler can pull source data without a manual file upload. A **source binding** ties a source to the rail that supplies its transactions, plus a duration that determines when it is due. Exactly one rail is meaningful per binding `kind`:
* `file` — fetches files via a transport (populates `transportConfig`).
* `query` — pulls rows through a discovery-engine connection (populates `connectionId`; see [Discovery](/en/matcher/integrations/matcher-discovery)).
Bindings live under `/v1/contexts/{contextId}/sources/{sourceId}/bindings`.
A binding is dispatched only when the binding scheduler is enabled (it is disabled by default), the binding is enabled, and the binding is due. Creating or enabling a binding does not run it immediately.
| Action | Method & path |
| -------------- | ------------------------------------------------------------------------- |
| Create binding | `POST /v1/contexts/{contextId}/sources/{sourceId}/bindings` |
| List bindings | `GET /v1/contexts/{contextId}/sources/{sourceId}/bindings` |
| Get binding | `GET /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId}` |
| Update binding | `PATCH /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId}` |
| Delete binding | `DELETE /v1/contexts/{contextId}/sources/{sourceId}/bindings/{bindingId}` |
List returns **every** binding, enabled and disabled, so a disabled binding stays visible instead of silently vanishing.
### Create a query-rail binding
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{sourceId}/bindings" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "query",
"connectionId": "550e8400-e29b-41d4-a716-446655440000",
"format": "br/cnab400/default",
"scheduleSpec": "1h",
"enabled": true
}'
```
#### Fields
Rail the source is pulled on: `file` or `query` (required).
Query-rail discovery-engine connection. Required for `query`, rejected for `file`.
Declared format the binding produces (region/family-namespaced descriptor key, e.g. `br/cnab400/default`).
Go duration string the binding scheduler reads, such as `1h` or `30m`. Cron and `@every` syntax are invalid.
Whether the scheduler can dispatch the binding when it is due. Defaults to `true`; enabling it does not run it immediately.
API Reference:
* [Create source binding](/en/reference/matcher/create-source-binding)
* [List source bindings](/en/reference/matcher/list-source-bindings)
* [Get source binding](/en/reference/matcher/get-source-binding)
* [Update source binding](/en/reference/matcher/update-source-binding)
* [Delete source binding](/en/reference/matcher/delete-source-binding)
## Managing contexts
***
As reconciliations evolve, you'll adjust a context's settings, pause it, retire it, or copy it. These lifecycle operations preserve history so you never lose an audit trail.
### Update a context
```bash cURL theme={null}
curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Daily Bank Reconciliation - Updated",
"interval": "weekly",
"status": "PAUSED"
}'
```
API Reference: [Update context](/en/reference/matcher/update-context)
### Pause a context
To temporarily stop a context from being used in reconciliation runs, update its status to `PAUSED`:
```bash cURL theme={null}
curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "PAUSED"
}'
```
Pausing a context:
* Prevents new match runs
* Preserves historical data
* Allows future reactivation by setting status back to `ACTIVE`
### Archive a context
Archiving is a reversible soft-delete. Instead of permanently removing a context, it moves the context to the `ARCHIVED` status, preserving its full history (sources, rules, match runs, and audit records) while excluding it from the default context listing.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/archive" \
-H "Authorization: Bearer $TOKEN"
```
Archiving a context:
* Sets the context status to `ARCHIVED`
* Preserves the complete history and audit trail
* Excludes the context from the default listing
* Can be reversed at any time with the [restore](#restore-a-context) endpoint
API Reference: [Archive context](/en/reference/matcher/archive-context)
### Restore a context
Restoring reverses an archive, moving the context from `ARCHIVED` back to `DRAFT` so it can be reviewed and reconfigured before being reactivated.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/restore" \
-H "Authorization: Bearer $TOKEN"
```
Restoring a context:
* Sets the context status from `ARCHIVED` back to `DRAFT`
* Does **not** resume matching automatically—review and reactivate the context to run reconciliation again
* Returns `409 Conflict` if called on a context that is not archived
API Reference: [Restore context](/en/reference/matcher/restore-context)
### Clone a context
To duplicate an existing context with its sources, rules, fee rules, and field maps, use the clone endpoint. This is useful for creating templates or replicating configurations across environments. Cloned fee rules keep referencing the same fee schedules as the source context; the fee schedules themselves are not copied.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/clone" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 2025 Reconciliation (Copy)",
"includeSources": true,
"includeRules": true
}'
```
The response reports how many sources, rules, fee rules, and field maps were copied. A successful clone is returned in `ACTIVE` status.
API Reference: [Clone context](/en/reference/matcher/clone-context)
## Context lifecycle
***
A reconciliation context follows a well-defined lifecycle that controls when matching can run and how data is preserved.
* A context is first created in **Draft**, where sources and settings are configured.
* A context remains **Draft** until an explicit update sets it to `ACTIVE`. Activation validates the required sources on both `LEFT` and `RIGHT` sides, field mappings or CAMT options, match rules, and fee rules when fee normalization is enabled.
* An active context can be temporarily **Paused** to stop execution without affecting configuration or historical data.
* When a context is no longer needed, it can be **Archived** via the [archive](#archive-a-context) endpoint. Archiving is a reversible soft-delete: it moves the context to `ARCHIVED`, preserves the full history and audit records, and excludes it from the default listing. An archived context can be brought back to **Draft** at any time with the [restore](#restore-a-context) endpoint.
This lifecycle ensures operational control, predictable execution, and full traceability across reconciliation periods.
## Best practices
***
Use explicit names that reflect accounts, systems, and purpose.
Favor accuracy over automation initially. Adjust thresholds based on observed results.
Use multiple contexts instead of a single broad reconciliation.
Always mark sources with compliance requirements.
Ensure source timezones reflect the original data feed.
Explicitly define debit and credit semantics for each source.
## Next steps
***
Define how source fields map to Matcher's schema.
Configure the rules that drive reconciliation.
# Exception routing
Source: https://docs.lerian.studio/en/matcher/configuration/matcher-exception-routing
Understand automatic severity classification and use explicit assignment, bulk actions, caller-directed dispatch, and callbacks.
Matcher automatically classifies unmatched transactions by severity. Assignment, bulk operations, and dispatch are explicit API actions; Matcher does not automatically route or escalate exceptions.
## Severity classification
***
Matcher classifies exceptions automatically from base amount, age, and source signals to support review prioritization.
### Default severity rules
| Severity | Default amount or age criteria |
| ------------ | ---------------------------------------- |
| **Critical** | Base amount ≥ 100,000 OR age ≥ 120 hours |
| **High** | Base amount ≥ 10,000 OR age ≥ 72 hours |
| **Medium** | Base amount ≥ 1,000 OR age ≥ 24 hours |
| **Low** | All other cases |
Source signals can also influence classification. Exceptions with reason `FEE_DATA_MISSING` are capped at `MEDIUM`, even when amount or age thresholds would otherwise classify them as `HIGH` or `CRITICAL`.
## Assignment
***
Assignment is explicit. For an `OPEN` exception, the assignment API accepts one opaque `assignee` string and changes the exception to `ASSIGNED`.
Matcher has no user-group model and does not implement automatic assignment, round-robin routing, or least-loaded routing. If you use a user or group identifier, encode it in the `assignee` string and resolve its meaning in your own identity system.
## SLA behavior
***
Matcher contains domain helpers that can calculate SLA due dates, but the production exception flow does not call them. Matcher does not currently set SLA deadlines, emit SLA warnings, escalate exceptions, or route them automatically. Track and enforce operational SLAs outside Matcher.
## Additional exception endpoints
***
Beyond the basic exception CRUD, Matcher provides endpoints for advanced exception workflows:
| Endpoint | Method | Description |
| -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ |
| [Dispatch exception](/en/reference/matcher/dispatch-exception) | `POST` | Attempt caller-selected dispatch without changing exception status |
| [Process callback](/en/reference/matcher/process-exception-callback) | `POST` | Apply a token-authenticated, idempotent external update |
| [Bulk assign](/en/reference/matcher/bulk-assign-exceptions) | `POST` | Assign exceptions to one `assignee` string |
| [Bulk resolve](/en/reference/matcher/bulk-resolve-exceptions) | `POST` | Resolve multiple exceptions independently |
| [Bulk dispatch](/en/reference/matcher/bulk-dispatch-exceptions) | `POST` | Dispatch multiple exceptions independently |
| [List comments](/en/reference/matcher/list-exception-comments) | `GET` | Retrieve all comments on an exception |
| [Add comment](/en/reference/matcher/add-exception-comment) | `POST` | Add a comment to an exception for audit and collaboration |
| [Delete comment](/en/reference/matcher/delete-exception-comment) | `DELETE` | Remove a comment from an exception |
| [List disputes](/en/reference/matcher/list-disputes) | `GET` | Retrieve all disputes with filtering |
| [Get dispute](/en/reference/matcher/retrieve-dispute) | `GET` | Retrieve details of a specific dispute |
| [Open dispute](/en/reference/matcher/open-dispute) | `POST` | Flag an exception as disputed for escalated review |
| [Close dispute](/en/reference/matcher/close-dispute) | `POST` | Close a dispute with a resolution |
| [Submit evidence ](/en/reference/matcher/submit-evidence) | `POST` | Add evidence to support a dispute case |
Bulk assign, resolve, and dispatch accept 1–100 exception IDs. Matcher processes each ID independently, so partial success is expected. Bulk assignment accepts a single `assignee` string, not a user or group object.
## Dispatch and callbacks
***
Dispatch is caller-directed: each request names the target. Dispatch records an audit event but does not change the exception status. Do not treat the accepted target names as preconfigured integrations.
### Dispatch targets
When dispatching an exception, the `targetSystem` field must be one of the following values:
| Target | Description |
| ------------ | ------------------------------------------------------------------------------------------- |
| `JIRA` | Attempts caller-directed JIRA dispatch; runtime connector configuration is required. |
| `SERVICENOW` | Not supported; it reaches the generic unsupported-target failure path and returns HTTP 500. |
| `WEBHOOK` | Attempts caller-directed webhook dispatch; runtime connector configuration is required. |
| `MANUAL` | Acknowledges the dispatch locally without sending to an external system. |
Inbound callbacks are a separate token-authenticated, idempotent flow. A callback can set an exception to `ASSIGNED` when it includes an assignee, or to `RESOLVED`; it is not bidirectional synchronization performed by dispatch.
### Filtering by external system
When listing exceptions, the `external_system` query parameter accepts any string value for filtering. This allows you to filter exceptions dispatched to any system, including custom identifiers that may have been set through callbacks.
### Dispatch error handling
Request validation and connector failures use API problem responses. In particular, `SERVICENOW` does not return `MTCH-0508`; it currently returns the generic HTTP 500 unsupported-target failure. A successful dispatch acknowledges the target operation but still leaves the exception status unchanged.
## Queue summaries and observability
***
The exception list exposes queue-scoped summary counts. Matcher does not expose SLA breach rates, routing-rule distribution, or integration success and failure analytics. Use your external observability stack for those operational signals.
## Best practices
***
Use the classified severity to prioritize review, and account for the `FEE_DATA_MISSING` cap at `MEDIUM`.
Pass a stable identifier in the opaque `assignee` string and resolve ownership in your identity system.
Define deadlines, warnings, and escalation in your workflow system because Matcher does not enforce them.
Confirm the selected connector is configured before relying on caller-directed JIRA or webhook dispatch.
Treat bulk operations as independent per ID and handle partial success explicitly.
Protect callback tokens and use stable idempotency keys when external systems update exception status.
## Next steps
***
Resolve exceptions through the API or external systems.
Advanced event delivery and callback handling.
# Fee rules
Source: https://docs.lerian.studio/en/matcher/configuration/matcher-fee-rules
Configure fee schedules and predicate rules used by NET or GROSS normalization when the matching side and currencies are compatible.
Fee handling in Matcher is split across two entities that work together:
* A **fee schedule** defines *how much* fee to compute — the currency, rounding, and one or more fee items (flat, percentage, tiered, or expression-based).
* A **fee rule** decides *when* a fee schedule applies. It belongs to a reconciliation context, targets a matching side, and carries predicates that a transaction's metadata must satisfy. When the predicates match, the rule selects its fee schedule.
When fee normalization is enabled and the transaction and schedule currencies match, modeling expected fees this way lets Matcher account for predictable charges before comparing transactions.
## What fee handling solves
***
Reconciliation fails when one side of a transaction includes fees or charges that the other side doesn't record. A payment gateway deducts a processing fee before settling. A bank charges a wire transfer fee. An acquirer nets fees against payouts.
Without modeled fees, Matcher treats these differences as amount mismatches and generates exceptions — even when the difference is expected and documented. Fee schedules describe the charge, and fee rules select when to use it during enabled fee normalization.
## How rules and schedules fit together
***
A fee rule does not contain the fee amount or calculation itself. It **references** a fee schedule by ID and applies it to the transactions its predicates select.
1. A **fee schedule** is created once at the tenant level and can be reused by many rules across many contexts.
2. A **fee rule** is created inside a context. It sets a `side`, a `feeScheduleId`, a `priority`, and a list of `predicates`.
3. When Matcher processes a context with fee normalization enabled, it evaluates the relevant side's fee rules in `priority` order (lowest first). The first matching rule selects the referenced schedule.
This separation means you change *how* a fee is computed by editing the schedule, and change *when* it applies by editing the rule — without touching the other.
A fee rule alone does not change comparison amounts. Fee-rule evaluation affects fee reconciliation only when the context sets `feeNormalization` to `NET` or `GROSS`, the relevant side has rules, and the transaction and schedule currencies match.
## Fee rule structure
***
A fee rule belongs to a context and has the following fields.
| Field | Type | Description |
| --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `side` | Enum | Which matching side the rule applies to: `LEFT`, `RIGHT`, or `ANY`. `ANY` matches transactions on either side. |
| `feeScheduleId` | UUID | The fee schedule this rule applies when its predicates match. |
| `name` | String | Human-readable name of the fee rule. |
| `priority` | Integer | Evaluation priority; lower numbers are evaluated first. Must be unique within the context. `LEFT`, `RIGHT`, and `ANY` rules share the same priority space. |
| `predicates` | Array | Predicates (ANDed together) that a transaction's metadata must satisfy for this rule to apply. |
### Predicates
Each predicate tests one transaction metadata field with an operator.
| Field | Description |
| ---------- | ------------------------------------------------------------------------------ |
| `field` | The transaction metadata field the predicate tests (e.g. `institution`). |
| `operator` | The comparison operator (see below). |
| `value` | Single comparison value, used by `EQUALS`, `NEQ`, and the numeric comparators. |
| `values` | List of candidate values, used by `IN` and `BETWEEN`. |
**Available operators:**
| Operator | Meaning |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `EQUALS` | Case-insensitive string equality with a single `value`. |
| `NEQ` | For a present field, numeric inequality when both values coerce to decimals; otherwise case-insensitive string inequality. |
| `IN` | Matches any entry in `values`. |
| `EXISTS` | Asserts the field is present (no value needed). |
| `GT` / `GTE` / `LT` / `LTE` | Numeric comparison of the field against a single decimal `value`. |
| `BETWEEN` | Inclusive numeric membership in `values` = `[lo, hi]` (with `lo <= hi`). |
Missing fields evaluate to `false` for every operator except `EXISTS`.
## Fee schedule structure
***
A fee schedule is a tenant-level entity that computes a fee from a gross amount.
| Field | Type | Description |
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | String | Human-readable name of the schedule. |
| `currency` | String | ISO 4217 currency the schedule's amounts are denominated in. |
| `applicationOrder` | Enum | How items combine: `PARALLEL` applies every item to the same gross base; `CASCADING` applies each item to the net remaining after prior items. |
| `roundingScale` | Integer | Number of decimal places fee amounts are rounded to. |
| `roundingMode` | Enum | Rounding strategy: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE`. |
| `items` | Array | One or more fee items that make up the schedule (at least one required). |
### Fee items
Each item declares a `name`, a `priority` (application order, relevant for `CASCADING`), a `structureType`, and a type-specific `structure`.
| `structureType` | `structure` shape | Notes |
| --------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FLAT` | `{ "amount": "1.50" }` | Fixed amount. |
| `PERCENTAGE` | `{ "rate": "0.029" }` | `rate` is a `0..1` fraction of the base (`0.029` = 2.9%), **not** a percent value. |
| `TIERED` | `{ "tiers": [ { "rate": "0.01", "upTo": "1000" }, ... ] }` | Same `0..1` fraction semantics per tier rate. |
| `EXPRESSION` | `{ "expression": "gross - desconto + multa" }` | Formula over identifiers (`+ - * /`, parentheses, and the functions `days_late`, `days_between`, `max`, `min`, `abs`, `clamp`). `gross` is the engine-provided base amount and overrides any metadata key named `gross`; other identifiers resolve from metadata. |
## Managing fee schedules
***
Fee schedules are tenant-scoped and managed independently of any context.
| Operation | Endpoint |
| -------------------------- | ---------------------------------------------- |
| Create a fee schedule | `POST /v1/fee-schedules` |
| List fee schedules | `GET /v1/fee-schedules` |
| Retrieve a fee schedule | `GET /v1/fee-schedules/{scheduleId}` |
| Update a fee schedule | `PATCH /v1/fee-schedules/{scheduleId}` |
| Delete a fee schedule | `DELETE /v1/fee-schedules/{scheduleId}` |
| Simulate a fee calculation | `POST /v1/fee-schedules/{scheduleId}/simulate` |
### Create a fee schedule
This schedule computes a 2.9% processing fee on the gross amount, denominated in BRL.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/fee-schedules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Gateway Processing - 2.9%",
"currency": "BRL",
"applicationOrder": "PARALLEL",
"roundingScale": 2,
"roundingMode": "HALF_UP",
"items": [
{
"name": "processing",
"priority": 1,
"structureType": "PERCENTAGE",
"structure": { "rate": "0.029" }
}
]
}'
```
API Reference:
* [Create fee schedule](/en/reference/matcher/create-fee-schedule)
* [List fee schedules](/en/reference/matcher/list-fee-schedules)
* [Retrieve fee schedule](/en/reference/matcher/retrieve-fee-schedule)
* [Update fee schedule](/en/reference/matcher/update-fee-schedule)
* [Delete fee schedule](/en/reference/matcher/delete-fee-schedule)
### Simulate a fee schedule
Before wiring a schedule into a rule, simulate it against a gross amount to confirm the computed fee.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/fee-schedules/{scheduleId}/simulate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"grossAmount": "100.00",
"currency": "BRL"
}'
```
The response returns the `netAmount`, `totalFee`, and a per-item breakdown.
API Reference: [Simulate fee calculation](/en/reference/matcher/simulate-fee-schedule)
A fee schedule cannot be deleted while a fee rule or fee-variance history references it. The delete request returns `409 Conflict` with the code `MTCH-0108`; the API problem does not list the blocking contexts. Remove or repoint current rule references first. Historical variance references continue to block deletion.
## Managing fee rules
***
Fee rules are created inside a context and reference a fee schedule.
| Operation | Endpoint |
| ---------------------------- | ----------------------------------------- |
| Create a fee rule | `POST /v1/contexts/{contextId}/fee-rules` |
| List fee rules for a context | `GET /v1/contexts/{contextId}/fee-rules` |
| Retrieve a fee rule | `GET /v1/fee-rules/{feeRuleId}` |
| Update a fee rule | `PATCH /v1/fee-rules/{feeRuleId}` |
| Delete a fee rule | `DELETE /v1/fee-rules/{feeRuleId}` |
### Create a fee rule
This rule applies the fee schedule created above to right-side transactions whose `institution` metadata equals `Banco do Brasil`.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/fee-rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"side": "RIGHT",
"feeScheduleId": "550e8400-e29b-41d4-a716-446655440000",
"name": "BB Right-Side Processing Fee",
"priority": 0,
"predicates": [
{
"field": "institution",
"operator": "EQUALS",
"value": "Banco do Brasil"
}
]
}'
```
API Reference:
* [Create fee rule](/en/reference/matcher/create-fee-rule)
* [List fee rules](/en/reference/matcher/list-fee-rules)
* [Retrieve fee rule](/en/reference/matcher/retrieve-fee-rule)
* [Update fee rule](/en/reference/matcher/update-fee-rule)
* [Delete fee rule](/en/reference/matcher/delete-fee-rule)
## End-to-end flow
***
Putting it together, expected-fee handling follows three steps:
Define how the fee is computed once at the tenant level with `POST /v1/fee-schedules`. Optionally validate it with the simulate endpoint. Note the returned `id`.
Inside the reconciliation context, create a fee rule with `POST /v1/contexts/{contextId}/fee-rules`. Set `feeScheduleId` to the schedule's `id`, choose the `side`, set a unique `priority`, and add the `predicates` that select the transactions the fee applies to.
Set the context's `feeNormalization` to `NET` or `GROSS`. During matching, Matcher evaluates rules for each relevant side in `priority` order. A matching rule changes the comparison amount only when its schedule and the transaction use the same currency.
## Best practices
***
A fee schedule is tenant-scoped and can be referenced by many rules. Define a schedule once (e.g. "Card Processing - Visa") and point rules in different contexts at it. Editing the schedule updates every rule that uses it.
Priorities are unique within a context and shared across `LEFT`, `RIGHT`, and `ANY` rules. Order them from most specific to most general so a narrow rule wins before a broad fallback.
Predicates are ANDed together and tested against transaction metadata. Combine operators (`EQUALS`, `IN`, `BETWEEN`, `EXISTS`) to target exactly the transactions a fee applies to and avoid unintended fee attribution.
Use `POST /v1/fee-schedules/{scheduleId}/simulate` to confirm a schedule produces the expected fee for representative amounts before referencing it from a rule.
A schedule referenced by a rule or fee-variance history cannot be deleted (`409 MTCH-0108`). The conflict response does not list blocking contexts. Update or remove current rule references first; historical variance references continue to block deletion.
## Next steps
***
Configure how transactions are compared once expected fees are accounted for.
Review explicit assignment, dispatch, and callback behavior for exceptions that fee handling doesn't cover.
Full API reference for fee schedule endpoints.
Full API reference for fee rule endpoints.
# Field mapping
Source: https://docs.lerian.studio/en/matcher/configuration/matcher-field-mapping
Rename each source's raw columns to Matcher's canonical fields — external_id, amount, currency, date, plus optional description and fee slots per source.
A field map tells Matcher which raw column in a source carries each canonical transaction field. Because every source (bank statements, ledger exports, gateway reports) names its columns differently, the field map normalizes those column names into one fixed vocabulary before matching runs.
A field map only **renames columns**. It does not parse, compute, transform, or combine values. Each canonical field is populated from exactly one source column.
## What a field map is
***
A field map belongs to a single **source** inside a **context**. A context reconciles two sides — a `LEFT` source and a `RIGHT` source — and each source has its own field map. Matcher compares the canonical fields produced by both maps, so both sides must resolve to the same vocabulary even when their raw files look nothing alike.
The mapping is a JSON object in the form:
```JSON theme={null}
{
"": ""
}
```
* The **key** is a canonical field. Keys come from a **closed, case-sensitive vocabulary** — Matcher rejects any key outside it.
* The **value** is the name of the column in the raw source that carries that field. Values are free text (whatever your file calls the column) and must be non-empty strings.
## Canonical vocabulary
***
The key space is closed. These are the only keys Matcher accepts.
### Required keys
Every field map must declare all four:
| Key | Description |
| ------------- | ------------------------------------------------- |
| `external_id` | Unique identifier of the record within the source |
| `amount` | Transaction amount |
| `currency` | ISO 4217 currency code |
| `date` | Transaction date |
### Optional keys
Declare these only when the source carries them:
| Key | Description |
| -------------- | ---------------------------------------------------------------- |
| `description` | Free-text label copied into the transaction's description column |
| `fee_amount` | Column carrying a fee amount for the record |
| `fee_currency` | Column carrying the currency of that fee |
`fee_amount` and `fee_currency` are the optional **fee slot**. When present, the mapped column's value is copied into the transaction metadata that fee verification reads, so a column named anything (for example `mdr_fee`) can carry fees end to end without hand-built metadata. Omit them and behavior is identical to a map without a fee slot.
## Creating a field map
***
Field maps are created **per source**. Send the mapping object to the source's field-map endpoint:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{sourceId}/field-maps" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mapping": {
"external_id": "Transaction ID",
"amount": "Amount",
"currency": "Currency",
"date": "Post Date",
"description": "Memo"
}
}'
```
**Response**
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149",
"sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mapping": {
"external_id": "Transaction ID",
"amount": "Amount",
"currency": "Currency",
"date": "Post Date",
"description": "Memo"
},
"version": 1,
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-15T10:00:00Z"
}
```
API Reference: [Create field map](/en/reference/matcher/create-field-map)
## Updating a field map
***
Each source has one field map. To change a mapping, `PATCH` it by its own ID (not the source ID). Send the full mapping — it replaces the previous one and increments `version`.
```bash cURL theme={null}
curl -X PATCH "https://api.matcher.example.com/v1/field-maps/{fieldMapId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mapping": {
"external_id": "Transaction ID",
"amount": "Amount",
"currency": "Currency",
"date": "Value Date",
"description": "Memo",
"fee_amount": "Fee",
"fee_currency": "Fee Currency"
}
}'
```
API Reference: [Update field map](/en/reference/matcher/update-field-map)
Other operations:
| Operation | Endpoint |
| --------------------------------- | ------------------------------------------------------------ |
| Get a source's field map | `GET /v1/contexts/{contextId}/sources/{sourceId}/field-maps` |
| List every field map in a context | `GET /v1/contexts/{contextId}/field-maps` |
| Delete a field map | `DELETE /v1/field-maps/{fieldMapId}` |
## Example: both sides of a context
***
A context reconciles a bank feed against an internal ledger export. The two files use different column names, so each source declares its own map — but both resolve to the same canonical keys.
### LEFT source — bank statement (CSV)
Raw columns:
```csv theme={null}
BankRef,BookingDate,Amount,Ccy,Narrative
BANK-001,2025-01-15,-500.00,USD,Wire to Acme Corp
```
Field map:
```json theme={null}
{
"mapping": {
"external_id": "BankRef",
"amount": "Amount",
"currency": "Ccy",
"date": "BookingDate",
"description": "Narrative"
}
}
```
### RIGHT source — ledger export (CSV)
Raw columns:
```csv theme={null}
entry_id,posted_at,value,asset,memo,mdr_fee,fee_ccy
LDG-9931,2025-01-15,-500.00,USD,Payment Acme Corp,2.50,USD
```
Field map:
```json theme={null}
{
"mapping": {
"external_id": "entry_id",
"amount": "value",
"currency": "asset",
"date": "posted_at",
"description": "memo",
"fee_amount": "mdr_fee",
"fee_currency": "fee_ccy"
}
}
```
Both sources now expose `external_id`, `amount`, `currency`, and `date` in the canonical vocabulary, so match rules can compare them directly — even though one file called the amount `Amount` and the other called it `value`.
## Common mistakes
***
The key is the canonical field and the value is your column — `{"external_id": "BankRef"}`, not `{"BankRef": "external_id"}`. Writing it backwards puts an unknown key (`BankRef`) on the left and is rejected.
Only `external_id`, `amount`, `currency`, `date`, `description`, `fee_amount`, and `fee_currency` are accepted. Keys such as `transaction_id`, `reference`, `counterparty`, or `type` are rejected as unknown keys, and the error names each offender.
Keys are case-sensitive lowercase tokens. `External_Id`, `Amount`, or `CURRENCY` are treated as unknown keys.
All of `external_id`, `amount`, `currency`, and `date` must be present. A map missing any of them fails validation with a "missing required keys" message.
Every value must be a non-empty string naming a source column. `null`, numbers, objects, or `""` are rejected.
Field maps do not parse dates, divide amounts, concatenate columns, or apply conditionals. Deliver values already in the expected shape from the source file, or normalize upstream before upload.
## Next steps
***
Define how the canonical fields are compared and grouped.
Import transactions using your field maps.
# Match rules
Source: https://docs.lerian.studio/en/matcher/configuration/matcher-match-rules
Author exact, tolerance, date-lag, and fuzzy rules in Matcher — set priorities, tolerances, and reference matching to control how transactions pair up.
Match rules are where you set your reconciliation policy — how strict or forgiving Matcher should be when deciding two transactions are the same. Tight rules mean more manual review but fewer false matches; looser rules automate more but need careful oversight. You can enforce exact matches, allow controlled variance, tolerate timing differences, or compare free-text references by similarity.
## How rules work
***
When a matching run starts, Matcher evaluates rules in priority order.
* Rules are evaluated from the lowest priority number to the highest.
* Each rule creates all matches it can from transactions not already used by higher-priority rules.
* After every rule runs, transactions that remain unmatched become exceptions.
This approach keeps higher-priority matches from being reused while allowing progressively looser rules to process the remaining transactions.
## Rule types
***
### Exact
Requires a strict match on configured fields.
* **Best for**: Deterministic matches where values should align 1:1.
### Tolerance
Allows controlled variance in amount matching.
* **Best for**: Known variance patterns such as fees, rounding, or FX differences.
### Date lag
Allows date differences between transactions.
* **Best for**: Posting delays between systems.
### Fuzzy
Replaces exact reference equality with normalized string-similarity scoring. Amount, currency, and date gates require exact equality by default, but `matchAmount`, `matchCurrency`, and `matchDate` independently control whether each gate applies. FUZZY always **proposes** a match for review and never auto-confirms.
* **Best for**: Free-text memos or truncated references where the reference varies but the enabled financial gates still align.
## Creating match rules
***
### Exact rule
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "EXACT",
"priority": 1,
"config": {
"matchAmount": true,
"matchCurrency": true,
"matchDate": true,
"matchReference": true,
"datePrecision": "DAY",
"caseInsensitive": true,
"referenceMustSet": false,
"matchBaseAmount": false,
"matchBaseCurrency": false,
"matchScore": 100,
"matchBaseScore": 90
}
}'
```
#### Config reference
Require exact amount match
Require exact currency match
Require exact date match
Require exact reference match
Date comparison precision: `DAY` or `TIMESTAMP`
Case-insensitive reference comparison
Require reference to be present on both sides
Match on base (converted) amount instead of original
Match on base currency instead of original
Accepted and validated, but **reserved/inert** — does not change the calculated confidence score (see note below)
Accepted and validated, but **reserved/inert** — does not change the calculated confidence score (see note below)
**`matchScore` and `matchBaseScore` are currently inert.** They are accepted and validated in the rule config, but the scoring engine ignores them: confidence is always computed from the fixed internal component weights (amount 40, currency 30, date 20, reference 10). These fields are reserved for future use and setting them does **not** alter the confidence score or auto-confirm behavior. See [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring).
The response echoes the persisted rule with its assigned `id` and timestamps.
API Reference: [Create match rule](/en/reference/matcher/create-match-rule)
### Tolerance rule
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "TOLERANCE",
"priority": 2,
"config": {
"percentTolerance": 0.005,
"absTolerance": 0.50,
"dateWindowDays": 3,
"roundingScale": 2,
"roundingMode": "HALF_UP",
"percentageBase": "MAX",
"matchCurrency": true,
"matchReference": true,
"caseInsensitive": true,
"referenceMustSet": false,
"matchBaseAmount": false,
"matchBaseCurrency": false,
"matchScore": 85,
"matchBaseScore": 80
}
}'
```
#### Config reference
Percentage threshold applied to `percentageBase` (0.005 = 0.5%). Defaults to `0`; Matcher compares this threshold with `absTolerance` and uses the larger one
Absolute amount threshold. Defaults to `0`; Matcher compares it with the percentage threshold and uses the larger one
Both thresholds default to zero, so you must configure any permitted amount variance explicitly.
Number of days allowed between transaction dates
Decimal places for rounding
Rounding strategy: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE`
Base for percentage calculation: `MAX`, `MIN`, `AVERAGE`, `LEFT`, or `RIGHT`
Require currency match
Require reference match
Case-insensitive reference comparison
Require reference to be present on both sides
Match on base (converted) amount
Match on base currency
Accepted and validated, but **reserved/inert** — does not change the calculated confidence score
Accepted and validated, but **reserved/inert** — does not change the calculated confidence score
**Example:**
* Transaction A: \$1,000.00
* Transaction B: \$1,005.00
* Amount difference: \$5.00
* Percentage threshold: $1,005.00 × 0.5% = $5.025 (`percentageBase: MAX`)
* Absolute threshold: \$0.50
* Effective threshold: `MAX($5.025, $0.50)` = \$5.025 → **Matches**
### Fuzzy rule
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "FUZZY",
"priority": 4,
"config": {
"minSimilarity": 0.85,
"matchAmount": true,
"matchCurrency": true,
"matchDate": true,
"datePrecision": "DAY",
"referenceMustSet": true,
"matchScore": 70
}
}'
```
#### Config reference
Minimum normalized reference similarity (0–1) required to gate as a match
When `true`, require an exact amount match
When `true`, require an exact currency match
When `true`, require an exact date match
Date comparison precision: `DAY` or `TIMESTAMP`
Require a non-empty reference on both sides
Accepted and defaulted to `70`, but **reserved/inert** — it does not cap or change calculated confidence or auto-confirm behavior
FUZZY replaces reference equality with similarity. By default, it also requires exact amount, currency, and date matches; disable each gate independently with `matchAmount`, `matchCurrency`, or `matchDate`. FUZZY always proposes matches for human review and never auto-confirms them.
### Date lag rule
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "DATE_LAG",
"priority": 3,
"config": {
"maxDays": 3,
"minDays": 0,
"inclusive": true,
"direction": "ABS",
"feeTolerance": 0,
"matchScore": 80,
"matchCurrency": true
}
}'
```
#### Config reference
Maximum number of days difference allowed
Minimum number of days difference required
Whether the boundary days are inclusive
How to measure the lag: `ABS` (absolute), `LEFT_BEFORE_RIGHT`, or `RIGHT_BEFORE_LEFT`
Allowed amount difference to account for fees
Accepted and validated, but **reserved/inert** — does not change the calculated confidence score. Note that DATE\_LAG rules always score the reference component as 0, capping the maximum score at 90
Require currency match
### Allocation settings (all rule types)
All rule types accept additional allocation settings for split and aggregate matching:
| Field | Type | Description |
| -------------------------- | ------- | -------------------------------------------------------- |
| `allowPartial` | Boolean | Allow partial allocation of transaction amounts |
| `allocationDirection` | String | Allocation order: `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT` |
| `allocationToleranceMode` | String | How tolerance is measured: `ABS` (absolute) or `PERCENT` |
| `allocationToleranceValue` | Decimal | Tolerance threshold for allocation |
| `allocationUseBaseAmount` | Boolean | Use base (converted) amount for allocation |
## Rule priority
***
Rules are evaluated by priority. Lower numbers run first.
### Priority strategy
| Priority | Rule type | Use case |
| -------- | --------- | -------------------------------- |
| 1–10 | EXACT | Deterministic matches |
| 11–50 | TOLERANCE | Small, expected variance |
| 51–100 | DATE\_LAG | Date differences between systems |
### Reorder rules
You can reorder rules by providing the rule IDs in the desired order:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules/reorder" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ruleIds": [
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002",
"550e8400-e29b-41d4-a716-446655440000"
]
}'
```
API Reference: [Reorder match rules](/en/reference/matcher/reorder-match-rules)
## Testing rules
***
Test rules in dry-run mode before committing matches.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "DRY_RUN"
}'
```
Dry run mode evaluates all rules and returns potential matches. It does not create exceptions, but Matcher completes and persists the `MatchRun` with statistics and emits its completion event.
## Managing rules
***
### List rules
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN"
```
#### Response
The list endpoint returns a summary view of rules. To see the full configuration details for a specific rule, use the individual rule endpoint or the create response which includes the complete `config` object.
```json theme={null}
{
"items": [
{
"id": "019c96a0-2b20-7123-9a1b-2c3d4e5f6a7b",
"contextId": "019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f",
"type": "EXACT",
"priority": 1,
"config": {
"matchAmount": true,
"matchCurrency": true,
"matchDate": true,
"matchReference": true,
"datePrecision": "DAY",
"matchScore": 100,
"matchBaseScore": 90
},
"createdAt": "2026-02-02T16:40:00Z",
"updatedAt": "2026-02-02T16:40:00Z"
}
],
"limit": 20,
"hasMore": false
}
```
API Reference: [List match rules](/en/reference/matcher/list-match-rules)
### Update a rule
```bash cURL theme={null}
curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}/rules/{ruleId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"priority": 5,
"type": "TOLERANCE",
"config": {
"percentTolerance": 0.02,
"absTolerance": 10.0
}
}'
```
API Reference: [Update match rule](/en/reference/matcher/update-match-rule)
### Delete a rule
```bash cURL theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/contexts/{contextId}/rules/{ruleId}" \
-H "Authorization: Bearer $TOKEN"
```
API Reference: [Delete match rule](/en/reference/matcher/delete-match-rule)
## Best practices
***
Lead with exact rules. Add tolerance rules only for the variance you can justify and explain.
Use gaps (1, 10, 20, 50) so you can insert rules without renumbering your entire set.
Treat rule updates as production changes. Validate match rates and exception volume before committing.
A rule should document the variance it covers and the risk it introduces.
If a rule never matches, it may be unnecessary. If it matches too often, it may be too broad.
High tolerance increases false positives. Use it as a fallback and review results carefully.
## Next steps
***
Configure classification, assignment, and escalation for unmatched transactions.
Understand how scores are calculated and how thresholds impact automation.
# Schedules
Source: https://docs.lerian.studio/en/matcher/configuration/matcher-schedules
Automate reconciliation runs with cron-based schedules per context, and manage them through a full create, list, retrieve, update, and delete lifecycle.
Schedules let you run reconciliation automatically on a recurring cadence instead of triggering match runs by hand. Each schedule belongs to a reconciliation context and fires on a cron expression, with a minimum interval of five minutes between firings.
## What is a schedule?
***
A schedule is a cron-based trigger attached to a context. When it fires, Matcher launches a reconciliation run for that context using its active rules and sources.
| Field | Type | Description |
| ------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- |
| `id` | UUID | Unique identifier for the schedule |
| `contextId` | UUID | Context this schedule belongs to |
| `cronExpression` | String | Cron expression defining the frequency (e.g. `0 0 * * *` for daily at midnight) |
| `enabled` | Boolean | Whether the schedule is active |
| `lastRunAt` | Timestamp | Time Matcher last dispatched the asynchronous match-run trigger; it does not indicate completion or success (RFC 3339) |
| `nextRunAt` | Timestamp | Next scheduled run time (RFC 3339) |
| `createdAt` / `updatedAt` | Timestamp | Creation and last-update timestamps (RFC 3339) |
## Schedule lifecycle
***
Schedules support a full CRUD lifecycle under `/v1/contexts/{contextId}/schedules`.
| Action | Method & path |
| ----------------- | -------------------------------------------------------- |
| Create schedule | `POST /v1/contexts/{contextId}/schedules` |
| List schedules | `GET /v1/contexts/{contextId}/schedules` |
| Retrieve schedule | `GET /v1/contexts/{contextId}/schedules/{scheduleId}` |
| Update schedule | `PATCH /v1/contexts/{contextId}/schedules/{scheduleId}` |
| Delete schedule | `DELETE /v1/contexts/{contextId}/schedules/{scheduleId}` |
API Reference:
* [Create schedule](/en/reference/matcher/create-schedule)
* [List schedules](/en/reference/matcher/list-schedules)
* [Get schedule](/en/reference/matcher/retrieve-schedule)
* [Update schedule](/en/reference/matcher/update-schedule)
* [Delete schedule](/en/reference/matcher/delete-schedule)
## Creating a schedule
***
Provide a `cronExpression`; `enabled` defaults to active when omitted.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/schedules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cronExpression": "0 0 * * *",
"enabled": true
}'
```
Cron expression defining the run frequency (1–100 chars), with at least five minutes between firings. Per-minute and sub-minute schedules are rejected
Whether the schedule is active immediately
The response returns the created schedule, including its `id`, `nextRunAt`, and timestamps. Listing schedules (`GET`) returns every schedule for the context, enabled and disabled alike.
## Pausing vs. deleting a schedule
***
When you need to stop a recurring reconciliation, you have two options — and the choice matters.
**Disable** the schedule to pause automatic runs while keeping its configuration and history. Re-enabling later is a single call, with no need to recreate it. Update the cron expression, toggle `enabled`, or both (omitted fields are left unchanged):
```bash cURL theme={null}
curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}/schedules/{scheduleId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cronExpression": "0 6 * * *",
"enabled": false
}'
```
**Delete** the schedule (`DELETE .../schedules/{scheduleId}`) only when the cadence is gone for good — deletion is permanent. To simply take a break, disable instead.
## Best practices
***
Schedule runs to fire after all sources for the context have delivered their data for the period. Running before ingestion completes produces avoidable exceptions.
When pausing a reconciliation cadence, disable the schedule so history and configuration are preserved and re-enabling is a single call.
Keep cron expressions readable and documented (e.g. `0 0 * * *` = daily at 00:00). Verify the timezone assumptions for your deployment before relying on a schedule for SLA-sensitive runs.
## Next steps
***
Configure the contexts and sources a schedule reconciles.
Review the results produced by scheduled runs.
# Generating reports
Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-generating-reports
Generate reconciliation summaries, match details, exception views, and variance reports to monitor outcomes and support compliance.
Reports turn a reconciliation run into something you can act on: how much matched, what's still open, and how much money is exposed. Operations teams use them to work the day's queue; finance and compliance use them to close the books and document outcomes.
## Available reports
***
Each report answers a different question:
* **Reconciliation summary**: A high-level snapshot of match rates, exception volume, and total variances.
* **Match detail report**: A complete list of matches, including transaction details, confidence scores, and variance breakdowns.
* **Unmatched report**: A list of transactions that remain unmatched for follow-up.
* **Exception report**: A focused view of unresolved exceptions with aging, severity, and resolution status.
* **Variance report**: A breakdown of differences between matched transactions.
Use the reporting dashboard endpoints to access reconciliation metrics and export data.
API Reference:
* [Dashboard aggregates](/en/reference/matcher/get-dashboard-aggregates)
* [Export matched report](/en/reference/matcher/export-matched-report)
* [Export unmatched report](/en/reference/matcher/export-unmatched-report)
## Dashboard analytics
***
The reporting dashboard provides real-time reconciliation metrics for a context and date range. Reporting endpoints default to the 30-day window ending tomorrow (UTC) and normally limit the window to 90 days. Only the unmatched list and count endpoints accept `unbounded=true` to query without date bounds.
### Dashboard aggregates
Use the combined dashboard aggregates endpoint for a single call that returns volume, match-rate, and SLA statistics for a context:
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/dashboard?date_from=2025-01-01&date_to=2025-01-31" \
-H "Authorization: Bearer $TOKEN"
```
`GET /v1/reports/contexts/{contextId}/dashboard` accepts `date_from`, `date_to`, and an optional `source_id` filter, and returns a `DashboardAggregatesResponse`:
| Field | Description |
| ----------- | ------------------------------------------------- |
| `volume` | Transaction volume statistics for the range |
| `matchRate` | Match-rate statistics (matched vs. total) |
| `sla` | SLA statistics for exception handling |
| `updatedAt` | When the aggregates were last computed (RFC 3339) |
More granular dashboard slices are available under `/v1/reports/contexts/{contextId}/dashboard/*` (for example `metrics`, `match-rate`, `sla`, `volume`, `source-breakdown`, and `cash-impact`).
There is no bare `GET /v1/reports/contexts/{contextId}` endpoint. Report data is served through the typed sub-paths under `/v1/reports/contexts/{contextId}/...` — for example `dashboard`, `summary`, `matched`, `unmatched`, and `variance` (each with an `/export` variant).
API Reference: [Get dashboard aggregates](/en/reference/matcher/get-dashboard-aggregates)
### Source breakdown
View reconciliation performance by source — including match rates, transaction counts, and unmatched amounts:
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/dashboard/source-breakdown?date_from=2025-01-01&date_to=2025-01-31" \
-H "Authorization: Bearer $TOKEN"
```
API Reference: [Get source breakdown](/en/reference/matcher/get-source-breakdown)
### Cash impact
Assess the total financial exposure from unmatched transactions, broken down by currency and age:
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/dashboard/cash-impact?date_from=2025-01-01&date_to=2025-01-31" \
-H "Authorization: Bearer $TOKEN"
```
The response includes `byCurrency` and `byAge` breakdowns to help prioritize resolution efforts.
API Reference: [Get cash impact](/en/reference/matcher/get-cash-impact)
## Pagination
***
The matched, unmatched, and variance report endpoints use cursor-based pagination. Pass the `cursor` value from a previous response to retrieve the next page of results.
If an invalid cursor value is provided, the API returns a `400 Bad Request` error with a message indicating the pagination parameters are invalid. Previous versions returned a `500` error in this case.
## Quick counts
***
Use count endpoints for lightweight status checks without fetching full result sets:
| Endpoint | Description |
| -------------------------------------------------------------- | ------------------------------------- |
| [Count matches](/en/reference/matcher/count-matches) | Total matched items in a date range |
| [Count transactions](/en/reference/matcher/count-transactions) | Total transactions in a date range |
| [Count exceptions](/en/reference/matcher/count-exceptions) | Total exceptions in a date range |
| [Count unmatched](/en/reference/matcher/count-unmatched) | Total unmatched items in a date range |
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/matches/count?date_from=2025-01-01&date_to=2025-01-31" \
-H "Authorization: Bearer $TOKEN"
```
Each count endpoint returns a single `count` value — ideal for lightweight dashboards or health checks that don't need the full result set.
## Best practices
***
Automate a daily summary report delivered each morning to keep stakeholders aligned.
Store reports in secure, durable storage. Financial artifacts often require multi-year retention.
Generate targeted reports by date and source—avoid exporting everything by default.
Include source names, rule names, and key identifiers so the output can stand alone outside Matcher.
Large reports can fail or stall. Export jobs use `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `EXPIRED`, and `CANCELED`; alert on `FAILED` or prolonged `RUNNING`.
## Next steps
***
Set up reconciliation contexts and data sources.
Learn how access control and data protection work in Matcher.
# Resolving exceptions
Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-resolving-exceptions
Review, prioritize, and resolve transactions Matcher couldn't reconcile automatically using severity, lifecycle, and audit-friendly actions.
Exceptions are transactions that Matcher can't reconcile automatically. This guide shows how to review exceptions, prioritize work based on severity, and resolve items with the right level of documentation.
## What is an exception?
***
An exception is created when a transaction from one source has no valid counterpart in another source. Common causes include:
* **No candidate found**: No transaction in the other source meets the active rule criteria.
* **Below confidence threshold**: Candidates exist, but score below the minimum confidence (default: 60).
* **Duplicate rejection**: A previous match was rejected and no alternative candidate remains.
* **Source imbalance**: One source contains transactions that are missing from the other.
## Exception lifecycle
***
Exceptions move through a simple workflow:
* When Matcher can't reconcile a transaction, it creates an exception in `OPEN` status.
* Assigning the exception moves it from `OPEN` to `ASSIGNED`. The API does not expose an unassign operation; `assignee` is required and must be non-empty.
* Force match and adjust entry persist `PENDING_RESOLUTION` only while the operation is in progress. Success moves the exception to `RESOLVED`; failure returns it to its previous `OPEN` or `ASSIGNED` status.
* Direct resolution moves an `OPEN` or `ASSIGNED` exception to `RESOLVED`.
* Dispatch sends the connector request, writes a `DISPATCH` audit event, and emits `exception.dispatched`. It does not change the exception status.
### Status definitions
| Status | Description | Who Can Transition |
| -------------------- | ------------------------------------------ | ------------------ |
| `OPEN` | New exception waiting for assignment | System |
| `ASSIGNED` | Assigned to an analyst for investigation | System, Analyst |
| `PENDING_RESOLUTION` | Force match or adjust entry is in progress | System |
| `RESOLVED` | Closed with an auditable resolution | Analyst, System |
### State-machine endpoints
The following single-exception endpoints change the lifecycle or record related actions. Each is addressed by the exception's `exceptionId` in the path.
| Endpoint | Method & path | Purpose |
| -------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Assign exception | `POST /v1/exceptions/{exceptionId}/assign` | Assigns the exception to an analyst. Body: `assignee` (required). Returns the updated exception. (`OPEN` → `ASSIGNED`) |
| Dispatch exception | `POST /v1/exceptions/{exceptionId}/dispatch` | Sends the exception through the configured connector. Writes a `DISPATCH` audit event and emits `exception.dispatched` without changing status. |
| Resolve exception | `POST /v1/exceptions/{exceptionId}/resolve` | Resolves a single exception. Body: `resolution` (required), `reason` (optional). Mirrors bulk-resolve validation for one exception. (`OPEN` or `ASSIGNED` → `RESOLVED`) |
| Force match | `POST /v1/exceptions/{exceptionId}/force-match` | Resolves an exception with `overrideReason` and `notes`. Uses `PENDING_RESOLUTION` while the operation is in progress, then resolves or returns to the previous status on failure. |
| Adjust entry | `POST /v1/exceptions/{exceptionId}/adjust-entry` | Resolves an exception by creating an accounting adjustment entry. Body: `amount`, `currency`, `effectiveAt`, `notes`, `reasonCode` (all required). Uses `PENDING_RESOLUTION` while the operation is in progress, then resolves or returns to the previous status on failure. |
| Exception history | `GET /v1/exceptions/{exceptionId}/history` | Returns the ordered history of state transitions and actions for the exception (`HistoryResponse`). Supports `cursor`/`limit` pagination. *(read-only)* |
| Select exception IDs | `GET /v1/exceptions/ids` | Returns the full set of exception IDs matching the current filters (`contextId`, `status`, `severity`, `reason`, …). Use it to drive a bulk selection before calling the bulk endpoints. *(read-only)* |
Matcher wires the webhook connector path and callback handling. JIRA connector code exists but is not configured by default. `MANUAL` acknowledges the dispatch locally without calling an external system. ServiceNow dispatch is not implemented: `SERVICENOW` reaches the generic unsupported-destination failure path and returns HTTP 500. See [Exception routing](/en/matcher/configuration/matcher-exception-routing) for the full dispatch contract.
API Reference:
* [Assign exception](/en/reference/matcher/assign-exception)
* [Dispatch exception](/en/reference/matcher/dispatch-exception)
* [Resolve exception](/en/reference/matcher/resolve-exception)
* [Force match](/en/reference/matcher/force-match-exception)
* [Adjust entry](/en/reference/matcher/adjust-entry-exception)
* [Get exception history](/en/reference/matcher/retrieve-exception-history)
* [Select exception IDs](/en/reference/matcher/select-exception-ids)
#### Assign example
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/assign" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "assignee": "john.doe@company.com" }'
```
#### Resolve example
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/resolve" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "resolution": "ACCEPTED", "reason": "Variance within tolerance" }'
```
#### Adjust entry example
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/adjust-entry" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": "150.50",
"currency": "BRL",
"effectiveAt": "2026-02-02T16:40:00Z",
"reasonCode": "AMOUNT_CORRECTION",
"notes": "Correcting processing fee discrepancy"
}'
```
#### Bulk selection with `selectExceptionIDs`
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/exceptions/ids?contextId={contextId}&status=OPEN&severity=CRITICAL" \
-H "Authorization: Bearer $TOKEN"
```
Feed the returned IDs into the [bulk operations](#bulk-operations) below.
## Exception severity
***
Matcher classifies exceptions by severity so you can work the queue in the right order.
| Severity | Criteria | SLA |
| ------------ | ------------------------------------- | ------------------ |
| **Critical** | Amount >= 100,000 OR age >= 120 hours | 24 hours |
| **High** | Amount >= 10,000 OR age >= 72 hours | 72 hours |
| **Medium** | Amount >= 1,000 OR age >= 24 hours | 120 hours (5 days) |
| **Low** | All others | 168 hours (7 days) |
### Severity escalation
Severity is re-evaluated as an exception ages. The classification uses OR logic — either the amount or the age threshold is enough to trigger a higher severity:
* An exception under 1,000 starts as **Low**, but escalates to **Medium** after 24 hours.
* An exception under 10,000 escalates to **High** after 72 hours.
* Any unresolved exception escalates to **Critical** after 120 hours.
## Resolution methods
***
Matcher exposes three exception-resolution actions.
### 1. Resolve directly
Close an exception with a required `resolution` and an optional `reason` when no force match or adjustment is needed.
### 2. Force match
Manually link transactions when you've confirmed they belong together, but the system couldn't match them.
**Use Force Match when:**
* The correct counterpart exists, but variances blocked automatic matching.
* You can clearly explain and document the rationale.
* The variance is expected (fees, timing, rounding).
Force Match bypasses scoring and rule logic. Use it only when you can justify the decision in writing.
### 3. Create adjustment
Create an adjusting entry to account for a variance or to balance an unmatched item.
**Adjustment reason codes:**
| Reason code | Use case |
| --------------------- | ------------------------------------ |
| `AMOUNT_CORRECTION` | Correct the transaction amount |
| `CURRENCY_CORRECTION` | Correct the transaction currency |
| `DATE_CORRECTION` | Correct the effective date |
| `OTHER` | Record another documented correction |
**Validation rules:**
* Adjustment amounts must be positive. A request with a zero or negative amount returns a `400 Bad Request` error.
* `POST /v1/exceptions/{exceptionId}/adjust-entry` requires a valid ISO 4217 currency code. `POST /v1/matching/adjustments` accepts any non-empty currency string and does not validate ISO 4217 membership.
* `reasonCode` must use `AMOUNT_CORRECTION`, `CURRENCY_CORRECTION`, `DATE_CORRECTION`, or `OTHER`.
## Resolution records
***
Matcher records supported resolution actions in the exception history and audit stream.
| Resolution | Request fields |
| ----------------- | ---------------------------------------------------------- |
| Direct resolution | `resolution` (required), `reason` (optional) |
| Force match | `overrideReason`, `notes` |
| Adjust entry | `reasonCode`, `amount`, `currency`, `effectiveAt`, `notes` |
Matcher does not expose exception split or independent write-off resolution contracts, and it does not enforce amount-based approval thresholds for these actions. Apply any additional approval requirements through your organization's controls.
## Bulk operations
***
When dealing with large volumes of exceptions, bulk endpoints let you process up to 100 exceptions in a single request.
### Bulk assign
Assign multiple exceptions to a team member at once:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/bulk/assign" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"exceptionIds": ["019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "019c96a0-2b20-7123-9a1b-2c3d4e5f6a7b"],
"assignee": "john.doe@company.com"
}'
```
API Reference:
* [Bulk assign](/en/reference/matcher/bulk-assign-exceptions)
* [Bulk resolve](/en/reference/matcher/bulk-resolve-exceptions)
* [Bulk dispatch](/en/reference/matcher/bulk-dispatch-exceptions)
### Bulk resolve
Resolve multiple exceptions with a shared resolution:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/bulk/resolve" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"exceptionIds": ["019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f", "019c96a0-2b20-7123-9a1b-2c3d4e5f6a7b"],
"resolution": "ACCEPTED",
"reason": "Verified as valid bank fees"
}'
```
The response includes `succeeded` and `failed` arrays, so you can handle partial failures gracefully.
### Bulk dispatch
Dispatch multiple exceptions to an external system:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/bulk/dispatch" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"exceptionIds": ["019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f"],
"targetSystem": "WEBHOOK",
"queue": "RECON-TEAM"
}'
```
## Exception comments
***
Comments give each exception an audit trail of investigation notes and team discussion — invaluable when someone else has to pick up or review the case later. Add a comment as an analyst works an item:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/comments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Contacted bank to verify wire transfer fee. Awaiting confirmation."
}'
```
Listing (`GET`) returns the full thread ordered oldest-first. You cannot add comments after an exception is resolved. Only the comment author can delete it, and the comment must belong to the exception identified in the URL.
| Action | Method & path | Key fields |
| -------------- | ---------------------------------------------------------- | ----------------------------------------- |
| Add comment | `POST /v1/exceptions/{exceptionId}/comments` | `content` (comment body) |
| List comments | `GET /v1/exceptions/{exceptionId}/comments` | — (returns the full thread, oldest first) |
| Delete comment | `DELETE /v1/exceptions/{exceptionId}/comments/{commentId}` | `commentId` in path |
API Reference:
* [List comments](/en/reference/matcher/list-exception-comments)
* [Add comment](/en/reference/matcher/add-exception-comment)
* [Delete comment](/en/reference/matcher/delete-exception-comment)
## Disputes
***
When an exception needs formal investigation or involves an external party — a chargeback, a bank query — escalate it to a **dispute**. Disputes track evidence, state changes, and the final outcome. List disputes with `GET /v1/disputes` (filter by `state`, e.g. `OPEN`) or retrieve one by its `disputeId`.
API Reference:
* [List disputes](/en/reference/matcher/list-disputes)
* [Get dispute](/en/reference/matcher/retrieve-dispute)
* [Open dispute](/en/reference/matcher/open-dispute)
* [Close dispute](/en/reference/matcher/close-dispute)
### Dispute states and transitions
A dispute has five states: `DRAFT`, `OPEN`, `PENDING_EVIDENCE`, `WON`, and `LOST`. The flow is **not** strictly linear:
* `PENDING_EVIDENCE` is **optional** — an `OPEN` dispute can go directly to `WON` or `LOST` without ever collecting evidence.
* A `LOST` dispute can be **reopened** back to `OPEN`.
* `WON` is terminal.
The complete set of valid transitions:
| From state | Allowed next states | Notes |
| ------------------ | --------------------------------- | ------------------------------------------------------- |
| `DRAFT` | `OPEN` | Dispute is opened for investigation |
| `OPEN` | `PENDING_EVIDENCE`, `WON`, `LOST` | Can resolve directly, or request evidence first |
| `PENDING_EVIDENCE` | `OPEN`, `WON`, `LOST` | Returns to `OPEN` or resolves once evidence is reviewed |
| `WON` | *(none)* | Terminal state |
| `LOST` | `OPEN` | A lost dispute can be reopened |
## Exception resolution workflow
***
Use this flow to keep reviews consistent and audit-friendly.
Review the queue by severity and SLA. Start with Critical and High.
Use the exception payload to understand what failed and what candidates exist.
* Read `reason_details` to see why matching failed.
* Review `candidates` for near matches below threshold.
* Look for patterns (same counterparty, recurring reference formats).
Pick the resolution that best reflects reality and policy.
* **Resolve directly**: You can close the exception without a force match or adjustment.
* **Force Match**: You found the correct counterpart.
* **Adjust**: You need an adjusting entry for variance.
Capture enough detail for someone else to replay your decision later:
* What you checked
* What you concluded
* Links or IDs for supporting evidence
If the exception requires external handling, dispatch it through the configured webhook connector. Dispatch records the action but does not change the exception status. JIRA requires connector configuration that Matcher does not provide by default; ServiceNow is unavailable.
## Best practices
***
Start with Critical and High items. They carry the highest risk and the tightest deadlines.
Notes aren't optional. Treat them as part of the resolution:
* What you checked
* Why this resolution is correct
* Any ticket IDs, statements, or confirmations
Repeating exceptions usually point to configuration issues:
* Same counterparty → Normalize names or mapping
* Same date window → Validate ingestion completeness
* Same source → Review field mapping and sign conventions
If you force-match regularly, your rules or tolerances need attention.
Assign exceptions through the assignment endpoints. Matcher does not apply assignment rules automatically.
## Next steps
***
Create reconciliation reports, export results, and support audits.
Review severity, SLA, and routing concepts for exceptions.
# Reviewing matches
Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-reviewing-matches
Review matches after a run — understand the 0–100 confidence score, its four weighted components, variance causes, and how to reject or revoke a match group.
After running a matching job, you'll need to review the results. This guide explains how to interpret match results, understand confidence scores, and reject proposed or revoke confirmed match groups.
## Match status lifecycle
***
Matches progress through a defined lifecycle:
* When the matching engine finds a pair of transactions that belong together, it creates a match in `PROPOSED` status.
* Eligible engine-created matches (score 90 or above) from EXACT and TOLERANCE rules are auto-confirmed immediately. Manual matches are created in `CONFIRMED` status. FUZZY and DATE\_LAG matches remain `PROPOSED` and are never auto-confirmed.
* Matches that are not auto-confirmed remain in `PROPOSED`. The public match-group API does not expose manual confirmation, but you can reject a proposed group with the unmatch operation and a required reason.
* Rejecting a `PROPOSED` group returns its transactions to the unmatched pool. A `CONFIRMED` group can be revoked only when Matcher can also reverse every residual/open-item effect that confirmation applied; on success, those changes and the transaction return happen atomically.
**FUZZY and DATE\_LAG matches are never auto-confirmed.** Auto-confirmation at score ≥ 90 applies to eligible engine-created EXACT and TOLERANCE matches. A manual match is created in `CONFIRMED` status. A match produced by a FUZZY or DATE\_LAG rule stays in `PROPOSED`, regardless of its score — even a FUZZY match scoring 90+. You can reject it through the unmatch operation; there is no public manual-confirm operation. See [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring#fuzzy-matches-never-auto-confirm).
### Status definitions
| Status | Description | Next Actions |
| ----------- | ----------------------------------------------------- | ------------------------------------------------- |
| `PROPOSED` | Match identified by the system but not confirmed | Review or reject through the unmatch operation |
| `CONFIRMED` | Match was auto-confirmed or created as a manual match | Revoke through the unmatch operation if incorrect |
| `REJECTED` | Match was declined | Transactions return to unmatched pool |
| `REVOKED` | A previously confirmed match was revoked | Transactions return to unmatched pool |
## Confidence tiers
***
Matcher assigns a confidence score (0-100) to each proposed match. The score determines how the match is handled.
### Confidence levels
| Tier | Score range | Behavior |
| --------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Auto-confirm eligible | 90-100 | Eligible engine-created EXACT and TOLERANCE matches are automatically confirmed without manual review. Manual matches are created `CONFIRMED`; FUZZY and DATE\_LAG matches remain `PROPOSED`. |
| Needs Review | 60-89 | Medium confidence matches remain `PROPOSED`. You can review and reject them, but the public API does not expose manual confirmation. |
| No Match | Below 60 | Low confidence candidates are not proposed as matches and become exceptions. |
### Understanding the score
The confidence score is calculated from weighted components:
| Component | Weight | What it Measures |
| --------------- | ------ | ------------------------------------- |
| Amount match | 40% | How closely transaction amounts align |
| Currency match | 30% | Whether currencies are the same |
| Date tolerance | 20% | How close the transaction dates are |
| Reference match | 10% | Whether transaction references match |
**Example Score Breakdown:**
```
Match: BANK-001 ↔ LED-001
Amount: $1,000.00 vs $1,000.00 → 100% × 40% = 40 points
Currency: USD vs USD → 100% × 30% = 30 points
Date: 2024-01-15 vs 2024-01-15 → 100% × 20% = 20 points
Reference: PAY-001 vs PAY-001 → match → 10 points
─────────────────────────
Total Confidence: 100 points
```
## Understanding variances
***
When matches have differences, review the variance details:
### Amount variance
Common causes of amount variance:
* Bank fees
* Currency conversion differences
* Rounding differences
* Partial payments
### Date variance
Common causes of date variance:
* Settlement timing
* Time zone differences
* Posting vs. transaction date
* Weekend/holiday processing
## Match candidates, open items, and adjustments
***
As you work a review queue, three questions come up again and again: *why did the engine propose this pairing?*, *what's still unsettled after a partial match?*, and *how do I book a small difference so both sides balance?* Matcher answers each with a dedicated surface — match candidates, open items, and adjustments.
### List match candidates
Retrieve the ranked candidate proposals the engine considered for a transaction — the "why" behind a proposed match, including the per-component contributions.
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/matching/candidates?contextId={contextId}&transactionId={transactionId}&limit=20" \
-H "Authorization: Bearer $TOKEN"
```
`GET /v1/matching/candidates` accepts `contextId`, `transactionId`, and `limit` query parameters and returns a `CandidateProposalsResponse`. The `limit` defaults to 50 and accepts values from 1 to 200.
The candidate picker scans at most 5,000 unmatched transactions from the opposite side and returns only 1:1 candidates. It applies the shared scorer to raw transaction data, without the run-time fee normalization or FX variance-band matching used by a matching run.
### List open items
Open items are residual balances left when a transaction is only partially netted. Track them to surface amounts that still need clearing or that have aged past their threshold.
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/matching/contexts/{contextId}/open-items?status=OPEN&limit=50" \
-H "Authorization: Bearer $TOKEN"
```
`GET /v1/matching/contexts/{contextId}/open-items` supports a `status` filter, plus `limit`/`cursor` pagination.
#### Open item statuses
| Status | When it occurs |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPEN` | Fresh residual — the item has a remaining balance and no leg has netted against it yet. |
| `PARTIALLY_CLEARED` | At least one leg has netted against the balance, but a residual still remains. |
| `CLEARED` | The residual has been fully netted within tolerance. |
| `AGED` | The item remained open beyond its aging threshold, measured from the obligation business date or the legacy first-seen time. |
| `WITHDRAWN` | A confirmed-group unmatch removed the final live contribution behind the obligation. The item remains as history, but is terminal and cannot be netted or carried forward. |
The normal lifecycle flows `OPEN` → `PARTIALLY_CLEARED` → `CLEARED`, with any still-open item able to become `AGED` once it passes the aging threshold. A successful confirmed-group unmatch withdraws that group's residual effects. If no live contribution remains behind the obligation, the item ends `WITHDRAWN` — it was taken back, not cleared or aged.
### Create an adjustment
Post an accounting adjustment to account for a variance (bank fee, FX difference, rounding, write-off, and so on) against a match group or transaction.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/adjustments?contextId={contextId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": "10.50",
"currency": "BRL",
"direction": "DEBIT",
"type": "BANK_FEE",
"reason": "Variance due to bank processing fee",
"description": "Bank wire fee adjustment",
"matchGroupId": "019c96a0-0b74-768c-8d25-2bf065dca2f8"
}'
```
`POST /v1/matching/adjustments` requires the `contextId` query parameter. Body fields:
Adjustment amount
Non-empty currency string. Matcher does not validate ISO 4217 membership for this adjustment.
`DEBIT` or `CREDIT`
`BANK_FEE`, `FX_DIFFERENCE`, `ROUNDING`, `WRITE_OFF`, or `MISCELLANEOUS`
Business reason for the adjustment
Human-readable description
Match group the adjustment applies to
Transaction the adjustment applies to
At least one of `matchGroupId` or `transactionId` is required.
API Reference:
* [List match candidates](/en/reference/matcher/list-match-candidates)
* [List open items](/en/reference/matcher/list-open-items)
* [Create adjustment](/en/reference/matcher/create-adjustment)
## Rejecting or revoking match groups
***
Use the **unmatch** operation to reject a `PROPOSED` group or revoke a `CONFIRMED` group. After a successful operation, Matcher returns the group's transactions to `UNMATCHED` so they can be matched again.
Use `DELETE /v1/matching/groups/{matchGroupId}` with the required `contextId` query parameter and a `reason` in the request body (operationId `unmatch`):
```bash cURL theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/matching/groups/{matchGroupId}?contextId={contextId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "incorrect match - amounts do not match"
}'
```
A successful unmatch returns **204 No Content**. The `reason` field is required (non-empty). For a `CONFIRMED` group, Matcher verifies the residual/open-item reversal before changing the group or its transactions; a successful operation atomically appends the compensating ledger entries, revokes the group, and returns its transactions to `UNMATCHED`.
The unmatch operation requires a non-empty reason. For a confirmed group, it either reverses the residual/open-item effects together with the group and its transactions, or returns **409 Conflict** before changing anything. A later live entry on the residual blocks the earlier reversal; a newer live obligation on the same identity also blocks it when restoring a terminal item would conflict.
### When to reject or revoke
Common scenarios for rejecting or revoking a group:
* **Incorrect match confirmed**: The match was confirmed but the transactions belong to different records
* **New information**: Additional data shows the match is wrong
* **Source correction**: The source system issued a correction or reversal
* **Duplicate transaction**: One of the transactions was a duplicate that should be removed
### What happens after unmatch
When a match group is unmatched:
1. **Confirmed residuals are checked first**: Matcher verifies that every residual/open-item effect of a `CONFIRMED` group can be reversed. If a later entry or a conflicting newer obligation blocks it, the operation returns `409 Conflict` and leaves the group, transactions, and open items unchanged.
2. **Match status changes**: A `CONFIRMED` group becomes `REVOKED`; a still-`PROPOSED` group becomes `REJECTED`.
3. **Transactions returned**: On success, all associated transactions revert to `UNMATCHED` status.
4. **Open-item effects reversed**: On a confirmed-group unmatch, compensating ledger entries take back the group's residual effects in the same transaction. If that leaves no live contribution behind an obligation, it becomes terminal `WITHDRAWN` and is not carried forward.
5. **Reason recorded**: The group stores the supplied rejection or revocation reason.
6. **Streaming event emitted**: A `match_group.unmatched` event is emitted only when the group was previously `CONFIRMED`.
7. **Re-matching possible**: Transactions can be matched again in the next run after a successful unmatch.
## Best practices
***
Review matches with the lowest confidence scores first. These are most likely to be incorrect and need the most attention.
Confidence thresholds are fixed in the system. Eligible engine-created EXACT and TOLERANCE matches with scores of 90 or above are auto-confirmed. Manual matches are created `CONFIRMED`. Scores between 60 and 89 remain `PROPOSED`, and scores below 60 become exceptions. These values are not configurable per context. **FUZZY and DATE\_LAG matches are the exception: they are never auto-confirmed and remain `PROPOSED`, regardless of score.** Use rule tuning (priority, tolerance values) to influence how many matches land in each tier.
Provide a specific reason when rejecting a proposed group or revoking a confirmed group. The unmatch operation requires it and stores it with the group.
Review the group and its source transactions before using the unmatch operation. Matcher exposes this operation per group; it does not expose bulk confirmation.
Regardless of confidence score, give extra attention to high-value matches. The impact of an incorrect match is proportional to the amount.
## Next steps
***
Handle transactions that couldn't be matched automatically.
Deep dive into how confidence scores are calculated.
# Uploading files
Source: https://docs.lerian.studio/en/matcher/daily-reconciliation/matcher-uploading-files
Import transaction data into Matcher from CSV, JSON, XML, or banking formats like camt.053 — preview column detection, then upload files to a source.
This guide covers how to import transaction data from external sources into Matcher for reconciliation.
## Supported formats
***
Matcher accepts transaction files in three general-purpose formats:
* **CSV**: Comma-separated values with headers. Most common for bank exports.
* **JSON**: Array of transaction objects. Best for API integrations.
* **XML**: Structured elements. Common for enterprise systems.
Beyond these, the upload endpoint also accepts specialized banking formats such as `camt053` and the namespaced descriptor keys from the format catalog (CNAB, acquirer layouts) — see [Import formats](/en/matcher/imports/matcher-import-formats) for the full catalog.
## File structure requirements
***
Each file must contain transaction records with fields that can be mapped to Matcher's internal schema.
### Required fields
Every transaction must have these fields (or mappable equivalents):
| Field | Type | Description |
| ---------------- | ------------- | ----------------------------------------- |
| `transaction_id` | String | Unique identifier within the source |
| `amount` | Decimal | Transaction amount (positive or negative) |
| `currency` | String | ISO 4217 currency code |
| `date` | Date/DateTime | Transaction date |
### Optional fields
| Field | Type | Description |
| -------------- | ------ | -------------------------------------- |
| `reference` | String | External reference or description |
| `counterparty` | String | Other party in the transaction |
| `type` | String | Transaction type (credit, debit, etc.) |
| `metadata` | Object | Additional custom fields |
## Format examples
***
### CSV
**CSV Requirements:**
* First row must be column headers
* UTF-8 encoding
* Comma delimiter (configurable)
* Quote fields containing commas or newlines
**Code example**
```csv theme={null}
transaction_id,amount,currency,date,reference,type
BANK-2024-001,1500.00,USD,2024-01-15,Invoice #1234,credit
BANK-2024-002,-250.00,USD,2024-01-15,Service fee,debit
BANK-2024-003,3200.50,USD,2024-01-16,Customer payment,credit
BANK-2024-004,-89.99,USD,2024-01-16,Subscription,debit
```
### JSON
**JSON Requirements:**
* Root element must be an array
* Consistent field names across objects
* UTF-8 encoding
**Code example**
```json theme={null}
[
{
"transaction_id": "BANK-2024-001",
"amount": 1500.0,
"currency": "USD",
"date": "2024-01-15",
"reference": "Invoice #1234",
"type": "credit"
},
{
"transaction_id": "BANK-2024-002",
"amount": -250.0,
"currency": "USD",
"date": "2024-01-15",
"reference": "Service fee",
"type": "debit"
}
]
```
### XML
**XML Requirements:**
* Valid XML with declaration
* Root element containing transaction elements
* UTF-8 encoding
**Code example**
```xml theme={null}
BANK-2024-0011500.00USD2024-01-15Invoice #1234creditBANK-2024-002-250.00USD2024-01-15Service feedebit
```
## Upload via API
***
Use the import endpoint to upload transaction files.
### Preview before uploading
Before committing a file for ingestion, you can preview it to verify column detection and sample data. This helps catch field mapping issues early.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/preview" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@bank_statement_january.csv" \
-F "max_rows=5"
```
#### Response
```json theme={null}
{
"columns": ["transaction_id", "amount", "currency", "date", "reference"],
"sampleRows": [
["BANK-2024-001", "1500.00", "USD", "2024-01-15", "Invoice #1234"],
["BANK-2024-002", "-250.00", "USD", "2024-01-15", "Service fee"]
],
"rowCount": 2,
"format": "csv"
}
```
API Reference: [Preview file](/en/reference/matcher/preview-upload)
### Single file upload
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "format=csv" \
-F "file=@bank_statement_january.csv"
```
Send the `format` field **before** the `file` part. If `file` arrives first, the format is inferred from the filename extension for `.csv` and `.json` only; `.xml` is never inferred (it is a format family — plain XML, camt.053 — so the explicit `format` field is required, otherwise the upload is rejected). Upload returns **202 Accepted** with the created job.
The upload limit defaults to **1 GiB** and applies to the entire multipart request, including every part, header, and boundary—not only the file. You can configure `ingestion.max_upload_bytes` from **1 MiB** to **8 GiB**.
API Reference: [Upload file](/en/reference/matcher/upload-transaction-file)
#### Response
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149",
"sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "QUEUED",
"fileName": "bank_statement_january.csv",
"totalRows": 0,
"persistedRows": 0,
"droppedDuplicateRows": 0,
"failedRows": 0,
"failureRatePercent": 0,
"completedWithErrors": false,
"createdAt": "2024-01-20T10:30:00Z"
}
```
### Check import status
```bash cURL theme={null}
curl -X GET https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs/{jobId} \
-H "Authorization: Bearer $TOKEN"
```
API Reference: [Get import status](/en/reference/matcher/retrieve-ingestion-job)
#### Response (Processing)
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149",
"sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "PROCESSING",
"fileName": "bank_statement_january.csv",
"totalRows": 1250,
"startedAt": "2024-01-20T10:30:05Z"
}
```
#### Response (Completed)
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149",
"sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "COMPLETED",
"completedWithErrors": true,
"fileName": "bank_statement_january.csv",
"totalRows": 1250,
"persistedRows": 1233,
"droppedDuplicateRows": 12,
"failedRows": 5,
"failureRatePercent": 1,
"reviewRows": 0,
"diagnosis": "",
"startedAt": "2024-01-20T10:30:05Z",
"completedAt": "2024-01-20T10:30:45Z"
}
```
Per-row parse/normalization errors are **not** embedded in the job. When `completedWithErrors` is `true` (or the job `FAILED`), fetch the details from `GET /v1/imports/contexts/{contextId}/jobs/{jobId}/errors` (capped at 100 stored rows, with `totalErrors`/`truncated` accounting). For a wholesale `FAILED` job, `diagnosis` carries a safe one-line reason.
### Import job status values
| Status | Description |
| ------------ | ------------------------------------------------------------------ |
| `QUEUED` | Job received, awaiting a worker |
| `PROCESSING` | File is being parsed and normalized |
| `COMPLETED` | Import finished (check `completedWithErrors` for partial failures) |
| `FAILED` | Import aborted wholesale (see `diagnosis`) |
## Validation and error handling
***
Matcher validates uploaded files at multiple stages.
### Validation stages
Verifies the file is valid CSV, JSON, or XML with correct structure.
Checks that required fields are present and match the configured field map.
Validates amounts are valid decimals, dates are parseable, currencies are valid ISO codes.
Applies context-specific rules like date ranges, amount limits, etc.
### Common validation errors
| Error | Cause | Solution |
| ------------------------ | ------------------------------- | ----------------------------------------------- |
| `INVALID_FORMAT` | File cannot be parsed | Check file encoding and structure |
| `MISSING_REQUIRED_FIELD` | Required field not found | Verify field mapping configuration |
| `INVALID_AMOUNT` | Amount not a valid number | Check for currency symbols or commas in numbers |
| `INVALID_DATE` | Date cannot be parsed | Use ISO 8601 format (YYYY-MM-DD) |
| `UNKNOWN_CURRENCY` | Currency code not recognized | Use ISO 4217 codes (USD, EUR, BRL) |
| `DATE_OUT_OF_RANGE` | Date before/after allowed range | Check context date boundaries |
### Handling errors
By default, valid rows are imported even if some rows have errors. Configure error handling behavior through context settings or handle errors after import completion by reviewing the job status response.
## Duplicate detection
***
Matcher automatically detects and handles duplicate transactions to prevent double-counting.
### How duplicates are detected
Duplicates are identified by the row's dedup key within a source:
* `source_id`
* `external_id` (the source-system transaction identifier)
If a row repeats that key—within the same upload or against already-persisted data—it is treated as a duplicate.
### Duplicate handling options
Set the `duplicate_policy` key in the source `config` to control handling:
| Policy | Behavior |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `KEEP_FIRST` (default) | Keeps the first occurrence and silently drops repeats, counted in `droppedDuplicateRows` |
| `REJECT` | Turns each repeated row into an ingestion row error, counted in `failedRows` |
| `FLAG_AS_EXCEPTION` | Drops the repeat and raises a `DUPLICATE_TRANSACTION` exception on the surviving transaction (subset reported as `flaggedDuplicateRows`) |
When the key is absent, `KEEP_FIRST` applies.
### Viewing duplicate details
The import summary shows how many duplicates were found:
```json theme={null}
{
"totalRows": 1000,
"persistedRows": 950,
"droppedDuplicateRows": 50,
"failedRows": 0,
"failureRatePercent": 0
}
```
## Batch uploads
***
For large reconciliation jobs, you can upload multiple files in sequence.
### Upload multiple files
```bash theme={null}
# Upload bank statement
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{bankSourceId}/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@bank_january.csv" \
-F "format=csv"
# Upload ledger export
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{ledgerSourceId}/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@ledger_january.csv" \
-F "format=csv"
```
### Wait for all imports
Before running matching, ensure all imports are complete:
```bash theme={null}
# List imports for context
curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs" \
-H "Authorization: Bearer $TOKEN"
```
## Search uploaded transactions
***
After importing files, you can search across all transactions in a context to verify data quality or investigate specific records.
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/transactions/search?q=Invoice&amount_min=1000&status=UNMATCHED" \
-H "Authorization: Bearer $TOKEN"
```
#### Response
```json theme={null}
{
"items": [
{
"id": "019c96a0-2a10-7dfe-b5c1-8a1b2c3d4e5f",
"sourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"amount": "1500.00",
"currency": "USD",
"date": "2024-01-15T00:00:00Z",
"description": "Invoice #1234",
"status": "UNMATCHED"
}
],
"total": 1,
"limit": 20,
"offset": 0
}
```
API Reference: [Search transactions](/en/reference/matcher/search-transactions)
Supported filters include `amount_min`, `amount_max`, `date_from`, `date_to`, `currency`, `source_id`, `status`, and free-text search via the `q` parameter.
## Best practices
***
Check file format and encoding locally before uploading. This catches obvious errors faster.
```bash theme={null}
# Check CSV is valid
head -5 transactions.csv
# Check encoding
file transactions.csv
```
Standardize on ISO 8601 format (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ`) across all sources to avoid parsing issues.
Always include unique transaction IDs from the source system. This enables proper duplicate detection and audit trails.
Decide on a convention (negative for debits, positive for credits) and apply it consistently. Document this in your field mapping.
For files larger than 50 MB, consider splitting them into smaller chunks by date range. This is a reliability recommendation, not the upload limit, and allows partial retries.
For recurring reconciliation, automate file uploads using scheduled jobs or webhooks from source systems.
```bash theme={null}
# Example: Daily upload via cron
0 6 * * * /scripts/upload_bank_statement.sh
```
## Next steps
***
Learn how to interpret match results and confidence scores.
Configure how source fields map to Matcher's schema.
# Installing Matcher
Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-installation
Deploy Matcher locally with Docker Compose or in production with the official Helm chart — configure PostgreSQL, Redis, RabbitMQ, and object storage.
Matcher automates financial reconciliation across multiple data sources, eliminating manual matching work and providing a full audit trail for every transaction. Setting up Matcher means putting in place the foundation for exception management, compliance reporting, and operational visibility. This guide walks you through deploying Matcher in development and production environments.
Matcher is available to licensed customers; its repository is maintained internally. The instructions below assume you already have access to the required Matcher project files.
## Docker compose (development)
***
Docker Compose is the recommended approach for local development and testing.
### 1. Access the Matcher project
From the Matcher project directory:
```bash theme={null}
cd matcher
```
### 2. Configure the environment
The `docker-compose.yml` file includes sensible defaults for local development. You can override any value by setting environment variables in your shell or by creating a `.env` file in the project root.
Refer to [Environment variables](#environment-variables) for details on available settings.
### 3. Start services
Start the required infrastructure services:
```bash theme={null}
docker-compose up -d postgres redis rabbitmq
```
Wait until all services report a healthy status:
```bash theme={null}
docker-compose ps
```
Start the Matcher application:
```bash theme={null}
docker-compose up -d app
```
To start all services at once:
```bash theme={null}
docker-compose up -d
```
### 4. Verify the installation
Confirm that Matcher is running by listing configuration contexts. On a fresh install, the cursor-paginated response has an empty `items` array:
```bash theme={null}
curl -H "Authorization: Bearer $TOKEN" http://localhost:4018/v1/contexts
```
Then verify the required dependencies through the public readiness endpoint:
```bash theme={null}
curl http://localhost:4018/readyz
```
The endpoint returns `200` when every required dependency is ready. It returns `503` with per-check details when a required dependency is unavailable.
### Docker compose services
The default `docker-compose.yml` includes:
| Service | Port | Purpose |
| ------------------ | ----------- | --------------------------------- |
| `postgres` | 5432 | PostgreSQL primary database |
| `postgres-replica` | 5433 | PostgreSQL read replica |
| `redis` | 6379 | Valkey (Redis-compatible) cache |
| `rabbitmq` | 5672, 15672 | RabbitMQ (AMQP and management UI) |
| `seaweedfs` | 8333, 9333 | S3-compatible object storage |
| `app` | 4018 | Matcher API |
### Development with hot reload
For active development, use:
```bash theme={null}
make dev
```
This starts Matcher with live reload enabled using Air.
## Kubernetes / helm (production)
***
Production deployments should use the official Helm chart.
### Prerequisites
* Kubernetes 1.28+
* Helm 3.12+
* `kubectl` configured for the target cluster
### 1. Create a namespace
```bash theme={null}
kubectl create namespace matcher
```
### 2. Configure values
Create a `values.yaml` file with your deployment configuration:
```yaml theme={null}
replicaCount: 2
image:
repository: lerianstudio/matcher
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 4018
ingress:
enabled: true
className: nginx
hosts:
- host: matcher.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: matcher-tls
hosts:
- matcher.example.com
postgresql:
external: true
host: postgres.example.com
port: 5432
database: matcher
username: matcher
existingSecret: matcher-db-credentials
existingSecretKey: password
redis:
external: true
host: redis.example.com
port: 6379
existingSecret: matcher-redis-credentials
rabbitmq:
external: true
host: rabbitmq.example.com
port: 5672
username: matcher
existingSecret: matcher-rabbitmq-credentials
auth:
enabled: true
serviceAddress: https://auth.example.com
observability:
enabled: true
otelExporterEndpoint: http://otel-collector:4317
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
```
### 3. Create secrets
Create Kubernetes secrets for sensitive credentials:
```bash theme={null}
kubectl create secret generic matcher-db-credentials \
--from-literal=password=your-db-password \
-n matcher
kubectl create secret generic matcher-redis-credentials \
--from-literal=password=your-redis-password \
-n matcher
kubectl create secret generic matcher-rabbitmq-credentials \
--from-literal=password=your-rabbitmq-password \
-n matcher
```
### 4. Install the chart
```bash theme={null}
helm install matcher oci://registry-1.docker.io/lerianstudio/matcher-helm \
--version 4.1.0 \
--namespace matcher \
--values values.yaml
```
### 5. Verify the deployment
```bash theme={null}
kubectl get pods -n matcher
kubectl get svc -n matcher
kubectl logs -f deployment/matcher -n matcher
```
### Upgrading
To upgrade an existing deployment:
```bash theme={null}
helm upgrade matcher oci://registry-1.docker.io/lerianstudio/matcher-helm \
--version 4.1.0 \
--namespace matcher \
--values values.yaml
```
## Environment variables
***
Environment variables provide Matcher's bootstrap configuration. Systemplane can override runtime-mutable settings after startup.
### Application
| Variable | Default | Description |
| ----------------------- | ------------- | ----------------------------------------------------------- |
| `ENV_NAME` | `development` | Runtime environment name |
| `LOG_LEVEL` | `info` | Log verbosity |
| `DEPLOYMENT_MODE` | `local` | Deployment mode (`local`, `byoc`, `saas`) |
| `SERVER_ADDRESS` | `:4018` | HTTP server bind address |
| `HTTP_BODY_LIMIT_BYTES` | `104857600` | Maximum buffered request body size (bytes, default 100 MiB) |
### CORS
| Variable | Default | Description |
| ---------------------- | ------------------------------------------------------- | ----------------------- |
| `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | Allowed origins |
| `CORS_ALLOWED_METHODS` | `GET,POST,PUT,PATCH,DELETE,OPTIONS` | Allowed HTTP methods |
| `CORS_ALLOWED_HEADERS` | `Origin,Content-Type,Accept,Authorization,X-Request-ID` | Allowed request headers |
### Database (PostgreSQL)
| Variable | Default | Description |
| ---------------------------------- | ---------------------- | ---------------------------------- |
| `POSTGRES_HOST` | `localhost` | Primary database host |
| `POSTGRES_PORT` | `5432` | Primary database port |
| `POSTGRES_USER` | `matcher` | Username |
| `POSTGRES_PASSWORD` | `matcher_dev_password` | Password |
| `POSTGRES_DB` | `matcher` | Database name |
| `POSTGRES_SSLMODE` | `disable` | SSL mode |
| `POSTGRES_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap |
| `POSTGRES_MAX_OPEN_CONNS` | `25` | Max open connections |
| `POSTGRES_MAX_IDLE_CONNS` | `5` | Max idle connections |
| `POSTGRES_CONN_MAX_LIFETIME_MINS` | `30` | Connection max lifetime (minutes) |
| `POSTGRES_CONN_MAX_IDLE_TIME_MINS` | `5` | Connection max idle time (minutes) |
| `POSTGRES_CONNECT_TIMEOUT_SEC` | `10` | Connection timeout (seconds) |
| `POSTGRES_QUERY_TIMEOUT_SEC` | `30` | Query timeout (seconds) |
### Database replica (PostgreSQL)
| Variable | Default | Description |
| ------------------------------- | ------- | ----------------------- |
| `POSTGRES_REPLICA_HOST` | — | Replica host |
| `POSTGRES_REPLICA_PORT` | — | Replica port |
| `POSTGRES_REPLICA_USER` | — | Replica username |
| `POSTGRES_REPLICA_PASSWORD` | — | Replica password |
| `POSTGRES_REPLICA_DB` | — | Replica database name |
| `POSTGRES_REPLICA_SSLMODE` | — | Replica SSL mode |
| `POSTGRES_REPLICA_TLS_REQUIRED` | `false` | Enforce TLS for replica |
### Cache (Redis)
| Variable | Default | Description |
| ------------------------ | ---------------- | ---------------------------- |
| `REDIS_HOST` | `localhost:6379` | Redis address (host:port) |
| `REDIS_MASTER_NAME` | — | Sentinel master name |
| `REDIS_PASSWORD` | — | Password |
| `REDIS_DB` | `0` | Database index |
| `REDIS_TLS` | `false` | Enable TLS |
| `REDIS_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap |
| `REDIS_CA_CERT` | — | CA certificate path |
| `REDIS_POOL_SIZE` | `10` | Connection pool size |
| `REDIS_MIN_IDLE_CONNS` | `2` | Minimum idle connections |
| `REDIS_READ_TIMEOUT_MS` | `3000` | Read timeout (milliseconds) |
| `REDIS_WRITE_TIMEOUT_MS` | `3000` | Write timeout (milliseconds) |
| `REDIS_DIAL_TIMEOUT_MS` | `5000` | Dial timeout (milliseconds) |
### Messaging (RabbitMQ)
| Variable | Default | Description |
| -------------------------------------- | ------------------------ | ------------------------------------ |
| `RABBITMQ_URI` | `amqp` | URI scheme (`amqp` or `amqps`) |
| `RABBITMQ_HOST` | `localhost` | Broker host |
| `RABBITMQ_PORT` | `5672` | Broker port |
| `RABBITMQ_USER` | `matcher_admin` | Username |
| `RABBITMQ_PASSWORD` | `matcher_dev_password` | Password |
| `RABBITMQ_VHOST` | `/` | Virtual host |
| `RABBITMQ_HEALTH_URL` | `http://localhost:15672` | Management API URL for health checks |
| `RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK` | `false` | Allow HTTP (non-TLS) health check |
| `RABBITMQ_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap |
### Authentication
| Variable | Default | Description |
| --------------------- | ------- | ---------------------------------------------------------------------------------------- |
| `PLUGIN_AUTH_ENABLED` | `false` | Enable authentication |
| `PLUGIN_AUTH_ADDRESS` | — | Auth service URL (token validation is delegated here; Matcher holds no local JWT secret) |
### Object storage (S3-compatible)
| Variable | Default | Description |
| ---------------------------------------- | ----------------------- | ----------------------------- |
| `OBJECT_STORAGE_ENDPOINT` | `http://localhost:8333` | S3 endpoint URL |
| `OBJECT_STORAGE_REGION` | `us-east-1` | S3 region |
| `OBJECT_STORAGE_BUCKET` | `matcher-exports` | Bucket for exports |
| `OBJECT_STORAGE_ACCESS_KEY_ID` | — | Access key ID |
| `OBJECT_STORAGE_SECRET_ACCESS_KEY` | — | Secret access key |
| `OBJECT_STORAGE_USE_PATH_STYLE` | `true` | Use path-style addressing |
| `OBJECT_STORAGE_ALLOW_INSECURE_ENDPOINT` | `false` | Allow HTTP (non-TLS) endpoint |
| `OBJECT_STORAGE_TLS_REQUIRED` | `false` | Enforce TLS at bootstrap |
### Observability
| Variable | Default | Description |
| -------------------------------------- | --------------------------------- | ------------------------------- |
| `ENABLE_TELEMETRY` | `false` | Enable OpenTelemetry |
| `OTEL_RESOURCE_SERVICE_NAME` | `matcher` | Service name for traces/metrics |
| `OTEL_LIBRARY_NAME` | `github.com/LerianStudio/matcher` | Instrumentation library name |
| `OTEL_RESOURCE_SERVICE_VERSION` | `1.1.0` | Service version |
| `OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT` | `development` | Deployment environment label |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `localhost:4317` | OTLP collector endpoint |
| `DB_METRICS_INTERVAL_SEC` | `15` | DB metrics collection interval |
### TLS
| Variable | Default | Description |
| ------------------------- | ------- | ---------------------------------------------------- |
| `SERVER_TLS_CERT_FILE` | — | TLS certificate path |
| `SERVER_TLS_KEY_FILE` | — | TLS private key path |
| `TLS_TERMINATED_UPSTREAM` | `false` | Trust upstream TLS termination (e.g., load balancer) |
| `TRUSTED_PROXIES` | — | Trusted proxy CIDR ranges |
### Rate limiting
| Variable | Default | Description |
| -------------------------------- | ------- | ------------------------------------ |
| `RATE_LIMIT_ENABLED` | `true` | Enable global rate limiting |
| `RATE_LIMIT_MAX` | `100` | Max requests per window |
| `RATE_LIMIT_EXPIRY_SEC` | `60` | Rate limit window (seconds) |
| `EXPORT_RATE_LIMIT_MAX` | `10` | Max export requests per window |
| `EXPORT_RATE_LIMIT_EXPIRY_SEC` | `60` | Export rate limit window (seconds) |
| `DISPATCH_RATE_LIMIT_MAX` | `50` | Max dispatch requests per window |
| `DISPATCH_RATE_LIMIT_EXPIRY_SEC` | `60` | Dispatch rate limit window (seconds) |
| `ADMIN_RATE_LIMIT_MAX` | `30` | Max admin requests per window |
| `ADMIN_RATE_LIMIT_EXPIRY_SEC` | `60` | Admin rate limit window (seconds) |
### Swagger
| Variable | Default | Description |
| ----------------- | ------- | -------------------------------------- |
| `SWAGGER_ENABLED` | `false` | Enable Swagger UI |
| `SWAGGER_HOST` | — | Override Swagger spec host |
| `SWAGGER_SCHEMES` | `https` | Swagger spec schemes (comma-separated) |
### Idempotency
| Variable | Default | Description |
| ------------------------------- | ------- | -------------------------------------------------------- |
| `IDEMPOTENCY_RETRY_WINDOW_SEC` | `300` | Window for retrying failed idempotent requests (seconds) |
| `IDEMPOTENCY_SUCCESS_TTL_HOURS` | `168` | How long completed idempotency keys are cached (hours) |
| `IDEMPOTENCY_HMAC_SECRET` | — | HMAC secret for signing idempotency keys (min 32 bytes) |
### Deduplication
| Variable | Default | Description |
| ---------------- | ------- | ------------------------------------ |
| `DEDUPE_TTL_SEC` | `3600` | TTL for deduplication keys (seconds) |
### Outbox
| Variable | Default | Description |
| ------------------------------ | ------- | ------------------------------------------------ |
| `OUTBOX_RETRY_WINDOW_SEC` | `300` | Cooldown before retrying failed events (seconds) |
| `OUTBOX_DISPATCH_INTERVAL_SEC` | `2` | Dispatcher poll interval (seconds) |
### Workers
| Variable | Default | Description |
| --------------------------------- | ------- | -------------------------------------------- |
| `EXPORT_WORKER_ENABLED` | `true` | Enable export worker |
| `EXPORT_WORKER_POLL_INTERVAL_SEC` | `5` | Export worker poll interval (seconds) |
| `EXPORT_WORKER_PAGE_SIZE` | `1000` | Rows per export page |
| `EXPORT_PRESIGN_EXPIRY_SEC` | `3600` | Pre-signed URL expiry for exports (seconds) |
| `CLEANUP_WORKER_ENABLED` | `true` | Enable cleanup worker |
| `CLEANUP_WORKER_INTERVAL_SEC` | `3600` | Cleanup worker interval (seconds) |
| `CLEANUP_WORKER_BATCH_SIZE` | `100` | Cleanup batch size |
| `CLEANUP_WORKER_GRACE_PERIOD_SEC` | `3600` | Grace period before cleanup (seconds) |
| `WEBHOOK_TIMEOUT_SEC` | `30` | Webhook dispatch timeout (seconds) |
| `CALLBACK_RATE_LIMIT_PER_MIN` | `60` | Max callbacks per external system per minute |
### Scheduler
| Variable | Default | Description |
| ------------------------ | ------- | -------------------------------------------- |
| `SCHEDULER_INTERVAL_SEC` | `60` | Cron-based scheduler poll interval (seconds) |
### Archival
| Variable | Default | Description |
| -------------------------------- | ------------------ | -------------------------------------------- |
| `ARCHIVAL_WORKER_ENABLED` | `false` | Enable audit log archival worker |
| `ARCHIVAL_WORKER_INTERVAL_HOURS` | `24` | Archival run interval (hours) |
| `ARCHIVAL_HOT_RETENTION_DAYS` | `90` | Days to keep data in hot storage |
| `ARCHIVAL_WARM_RETENTION_MONTHS` | `24` | Months to keep data in warm storage |
| `ARCHIVAL_COLD_RETENTION_MONTHS` | `84` | Months to keep data in cold storage |
| `ARCHIVAL_BATCH_SIZE` | `5000` | Rows per archival batch |
| `ARCHIVAL_STORAGE_BUCKET` | `matcher-archives` | S3 bucket for archives |
| `ARCHIVAL_STORAGE_CLASS` | `GLACIER` | S3 storage class for archives |
| `ARCHIVAL_PARTITION_LOOKAHEAD` | `3` | Partition lookahead count |
| `ARCHIVAL_PRESIGN_EXPIRY_SEC` | `3600` | Pre-signed URL expiry for archives (seconds) |
### Fetcher / Discovery
These settings control Discovery, which reads from external databases through an in-process extraction engine embedded in Matcher — not a separate networked service. See [Discovery](/en/matcher/integrations/matcher-discovery) for how it works.
| Variable | Default | Description |
| -------------------------------- | ------------ | ------------------------------------------------------------------------ |
| `FETCHER_DISCOVERY_INTERVAL_SEC` | `60` | Discovery poll interval (seconds) |
| `FETCHER_SCHEMA_CACHE_TTL_SEC` | `300` | Schema cache TTL (seconds) |
| `FETCHER_EXTRACTION_TIMEOUT_SEC` | `600` | Extraction timeout (seconds) |
| `FETCHER_MAX_EXTRACTION_BYTES` | `2147483648` | Max extraction payload size (bytes, default 2 GiB) |
| `APP_ENC_KEY` | — | Base64-encoded master key for the embedded engine's credential protector |
### Infrastructure
| Variable | Default | Description |
| --------------------------- | ------- | --------------------------------------------------- |
| `INFRA_CONNECT_TIMEOUT_SEC` | `30` | Infrastructure startup connection timeout (seconds) |
| `HEALTH_CHECK_TIMEOUT_SEC` | `5` | Legacy per-check probe timeout (seconds) |
| `HEALTH_CHECK_TIMEOUT_MS` | `800` | Per-check probe timeout (milliseconds, preferred) |
For multi-tenant deployment settings, see [Multi-Tenant Mode](/en/matcher/configuration/matcher-multi-tenant). For runtime configuration management, see [Runtime Configuration (Systemplane)](/en/matcher/configuration/matcher-systemplane).
## Verify the installation
***
Validate that Matcher and its required dependencies are ready:
```bash theme={null}
curl http://localhost:4018/readyz
```
The endpoint returns `200` when every required dependency is ready. It returns `503` with per-check details when a required dependency is unavailable. Configure Kubernetes readiness probes to use this endpoint.
## Troubleshooting
***
### Common issues
* **Cause:** PostgreSQL is not running or unreachable.
* **Resolution:**
1. Verify PostgreSQL is running: `docker-compose ps postgres`
2. Check connection values in `.env`
3. Test connectivity: `nc -zv localhost 5432`
4. Review logs: `docker-compose logs postgres`
* **Cause:** Redis is not running or credentials are incorrect.
* **Resolution:**
1. Verify Redis is running: `docker-compose ps redis`
2. Confirm `REDIS_PASSWORD`
3. Test connectivity: `redis-cli -h localhost ping`
* **Cause:** RabbitMQ is still initializing or the virtual host is missing.
* **Resolution:**
1. Wait until RabbitMQ is healthy
2. Access the management UI at [http://localhost:15672](http://localhost:15672)
3. Verify `RABBITMQ_VHOST`
* **Cause:** Auth service is unreachable or the token is invalid.
* **Resolution:**
1. Verify `PLUGIN_AUTH_ADDRESS`
2. Disable auth for development: `PLUGIN_AUTH_ENABLED=false`
3. Review auth service logs
* **Cause:** Database migrations could not be applied.
* **Resolution:**
1. Check migration status: `make migrate-status`
2. Review migration logs
3. Apply migrations manually: `make migrate-up`
4. Inspect the `schema_migrations` table if needed
### Viewing logs
```bash theme={null}
docker-compose logs -f app
kubectl logs -f deployment/matcher -n matcher
```
### Debug mode
Enable debug logging for additional visibility:
```bash theme={null}
LOG_LEVEL=debug docker-compose up app
```
## Next steps
***
Run your first reconciliation.
Configure contexts, sources, and match rules.
# Prerequisites
Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-prerequisites
Check the infrastructure, dependencies, authentication, and file-format requirements your environment needs before you deploy Matcher for reconciliation.
Before deploying Matcher, ensure that your environment meets the requirements described on this page.
These prerequisites define the baseline for running reconciliation reliably in development and production environments.
## System requirements
***
### Infrastructure
The values below are platform-validated operational starting points, not product-enforced minimums. Adjust them for your transaction volume and retention needs.
| Component | Validated starting point / scaling guidance | Purpose |
| ----------- | ------------------------------------------- | -------------------------------------------------- |
| **CPU** | 2 cores / 4+ cores | Matching and scoring logic is CPU-intensive |
| **Memory** | 2 GB / 4+ GB | In-memory processing of transaction batches |
| **Storage** | 10 GB / 50+ GB | Persistent storage for transactions and audit logs |
### Dependencies
The local Compose stack is the platform-validated dependency baseline. It pins:
* **PostgreSQL 17**: Primary data store for reconciliation contexts, transactions, matches, and audit logs.
* **Valkey 8**: Redis-compatible service used for caching, duplicate detection, distributed locking, and idempotency control.
* **RabbitMQ 4.1.3**: Message broker for asynchronous processing across bounded contexts.
### Runtime
The following versions are the platform-validated tooling baseline, not a product support matrix:
* **Go 1.26+** (only required when building from source)
* **Docker 24+** and **Docker Compose 2.20+** for containerized deployments
* **Kubernetes 1.28+** for production-grade deployments using Helm
## Optional: reconciling Midaz data
***
Matcher pairs naturally with Midaz Ledger, but there is **no live connector between them** — Matcher has no `MIDAZ_API_URL` and opens no connection to Midaz. Reconciling Midaz data is entirely optional; Matcher works as a stand-alone product reconciling any data sources.
### When to reconcile Midaz data
Reconcile ledger data from Midaz if:
* You use Midaz as your ledger system
* You want to reconcile Midaz postings against external sources (bank statements, gateway reports)
### When Midaz is not involved
Matcher works independently when:
* Reconciling between external systems (banks, ERPs, payment processors)
* Using a different ledger system
* Importing ledger data via CSV/JSON/XML files
### How it works
Matcher reconciles Midaz data the same way it ingests any source — by import, not a live query:
1. Export the ledger data for the period you want to reconcile.
2. Import that export into a Matcher context as a source of type `LEDGER`.
3. Import the counterparty data (bank statement or gateway report) as the other side.
4. Matcher matches the two sides using your match rules.
See the [Matcher and Midaz](/en/matcher/integrations/matcher-midaz-integration) guide for the full flow.
## Authentication
***
Matcher uses **lib-auth** for authentication and authorization, consistent with the rest of the Lerian ecosystem.
### Authentication flow
1. The client obtains a JWT from the identity provider
2. The token is sent in the `Authorization: Bearer ***` header.
3. Matcher validates the token via lib-auth
4. Tenant identity and permissions are extracted from token claims
### Required permissions
Access to Matcher features is controlled through fine-grained permissions:
| Permission | Description |
| -------------------- | ------------------------------ |
| `contexts:create` | Create reconciliation contexts |
| `contexts:read` | View context configuration |
| `rules:create` | Create and update match rules |
| `imports:create` | Upload transaction files |
| `match-runs:run` | Execute matching jobs |
| `exceptions:read` | View exceptions |
| `exceptions:resolve` | Resolve exceptions |
| `reports:read` | Access reports and audit views |
### Single-tenant mode
`MULTI_TENANT_ENABLED` controls this mode. Its default is `false`, which makes Matcher use the default tenant below. Authentication state or a missing JWT tenant claim does not switch Matcher into single-tenant mode.
```bash theme={null}
# Default tenant configuration (single-tenant mode)
DEFAULT_TENANT_ID=11111111-1111-1111-1111-111111111111
DEFAULT_TENANT_SLUG=default
```
## Generic import formats
***
Matcher's generic importers accept CSV, JSON, and XML. Built-in parsers also support CAMT.053, CNAB 240/400, OFX, several acquirer formats, and receivables formats. See the [import format catalog](/en/matcher/imports/matcher-import-formats) for the complete inventory.
Each generic format has specific structural requirements for successful ingestion.
### CSV (comma-separated values)
Commonly used for bank statements and exports.
**Requirements:**
* Header row is required
* UTF-8 encoding
* Comma delimiter (configurable)
* Quoted fields for values containing delimiters
**Example:**
```csv theme={null}
transaction_id,amount,currency,date,reference
TXN-001,1000.00,USD,2024-01-15,Invoice payment
TXN-002,-250.50,USD,2024-01-16,Refund
```
### JSON (javascript object notation)
Recommended for API-based integrations.
**Requirements:**
* Valid JSON array of transaction objects
* UTF-8 encoding
* Consistent field names across records
**Example:**
```json theme={null}
[
{
"transaction_id": "TXN-001",
"amount": 1000.0,
"currency": "USD",
"date": "2024-01-15",
"reference": "Invoice payment"
}
]
```
### XML (extensible markup language)
Common in enterprise and banking integrations.
**Requirements:**
* Single root element
* UTF-8 encoding
* Consistent element structure
**Example:**
```xml theme={null}
TXN-0011000.00USD2024-01-15Invoice payment
```
### File size limits
| Limit | Default | Configuration |
| ----------------------------- | ------- | ------------------------------------------------------------------------------------ |
| Maximum upload file size | 1 GiB | `INGESTION_MAX_UPLOAD_BYTES` at bootstrap or Systemplane at runtime (1 MiB to 8 GiB) |
| Maximum buffered request body | 100 MiB | `HTTP_BODY_LIMIT_BYTES` (non-upload requests) |
## Network requirements
***
### Inbound access
Matcher exposes a REST API that must be reachable by clients:
| Port | Protocol | Purpose |
| ---- | ---------- | ----------------------------------------------------------------- |
| 4018 | HTTP/HTTPS | API server (default `:4018`; serves HTTPS when TLS is configured) |
### Outbound access
Matcher must be able to reach the following services:
| Service | Purpose | Required |
| ---------------------------- | ------------------------ | ------------------------------------------------------ |
| PostgreSQL | Data persistence | Yes |
| Redis | Caching and coordination | Yes |
| RabbitMQ | Messaging | Yes |
| S3-compatible object storage | Exports and archival | When the export worker is enabled (enabled by default) |
| Auth service | Token validation | If authentication is enabled |
| JIRA | Exception routing | Optional |
| Custom webhooks | Event notifications | Optional |
### TLS configuration
For production environments, configure TLS:
```bash theme={null}
SERVER_TLS_CERT_FILE=/path/to/cert.pem
SERVER_TLS_KEY_FILE=/path/to/key.pem
```
## Environment checklist
***
Before proceeding with installation, confirm that:
* **Infrastructure is ready**: PostgreSQL, Redis, and RabbitMQ are running and accessible; S3-compatible object storage is also ready when the export worker is enabled (the default)
* **Authentication is configured**: Auth service is available, or auth is explicitly disabled
* **Network access is validated**: Required inbound and outbound connectivity is in place
* **Credentials are available**: Database credentials and API tokens are configured
* **Sample data is prepared**: Transaction files are ready for testing (see [Quick Start](/en/matcher/getting-started/matcher-quick-start))
## Next steps
***
Deploy Matcher using Docker or Kubernetes.
Run your first reconciliation.
# Getting started with Matcher
Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-quick-start
Walk the five-stage reconciliation lifecycle in Matcher — define a context, connect sources, set rules, run matching, and resolve exceptions step by step.
This guide walks through the reconciliation lifecycle in Matcher, from initial setup to reviewing results. It focuses on the concepts and decisions involved at each stage.
For step-by-step API instructions with request and response examples, see the [Matcher API quick start](/en/reference/matcher/matcher-developer-quick-start).
## The reconciliation lifecycle
***
Every reconciliation in Matcher follows the same five-stage lifecycle:
Create a context that describes what you are reconciling and records its reconciliation interval.Register the systems whose transactions you want to compare.Configure the criteria Matcher uses to pair transactions.Upload data and let Matcher find pairs, starting with a preview before committing.Review unmatched transactions and decide how to handle them.
The sections below explain each stage.
## Define scope with a context
***
A **context** is the top-level container for a reconciliation workflow. It answers three questions:
* **What are you reconciling?** For example, a bank account against your general ledger.
* **What type of pairing?** One-to-one, one-to-many, or many-to-many.
* **What interval label describes the reconciliation period?** For example, `daily`, `weekly`, or `on-demand`.
The required `interval` value is free-text metadata; it does not schedule execution. Automated runs use a separate cron-backed `ReconciliationSchedule`, with a minimum cadence of five minutes.
| Pairing type | When to use | Example |
| ------------ | --------------------------------------------------- | ------------------------------------- |
| `1:1` | Each transaction has exactly one counterpart | Bank statement vs. ledger entries |
| `1:N` | One record maps to several on the other side | A single invoice paid in installments |
| `N:M` | Multiple records on both sides relate to each other | Batch payments split across accounts |
Most reconciliations start with `1:1`. You can change the pairing type later as your process evolves.
API reference:
* [Create context](/en/reference/matcher/create-context)
* [Update context](/en/reference/matcher/update-context)
## Connect data sources
***
Each context needs at least **two sources**: the systems whose transaction data you want to compare. A source represents a single data feed such as a bank statement, a ledger export, or a payment gateway file.
### Source types
| Type | Typical use |
| --------- | -------------------------------------------- |
| `BANK` | Bank statements and account extracts |
| `LEDGER` | General ledger or ERP exports |
| `GATEWAY` | Payment processor data (Stripe, Adyen, etc.) |
| `FETCHER` | Data extracted through Fetcher/Discovery |
| `CUSTOM` | Any other structured data |
### Field mapping
Transaction files from different systems rarely use the same column names. **Field maps** translate your source columns into Matcher's standard schema so transactions can be compared.
For example, a bank file might call the transaction date "Post Date", while your ledger calls it "posting\_date". Field maps normalize both to Matcher's `date` field.
Every transaction must provide at least four fields after mapping:
| Field | Description |
| ------------- | --------------------------------------- |
| `external_id` | Unique identifier within the source |
| `amount` | Transaction value |
| `currency` | ISO 4217 currency code (e.g., USD, BRL) |
| `date` | Transaction date |
API reference: [Create source](/en/reference/matcher/create-source) | [Create field map](/en/reference/matcher/create-field-map)
## Set match rules
***
Rules define **how Matcher decides whether two transactions are the same**. You can stack multiple rules with different priorities. Matcher evaluates them in order: only transactions left unmatched by the first rule pass to the next.
### Rule types
| Rule | What it does | When to use |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Exact** | Requires identical values on the selected fields | When data is clean and systems are in sync |
| **Tolerance** | Allows small numeric or date differences | When bank fees, rounding, or processing delays cause minor discrepancies |
| **Fuzzy** | Uses normalized string similarity for references. Amount, currency, and date must match by default, but you can configure each gate independently. | When free-text or truncated references vary between sources |
| **Date lag** | Permits a configurable date window | When settlement dates differ between systems |
### Recommended starting configuration
1. **Priority 1 — Exact rule** on amount, currency, and date. This catches all perfect matches first.
2. **Priority 10 — Tolerance rule** with a small amount tolerance (e.g., 1%) and a 2-day date window. This catches near-matches caused by fees or timing.
As you observe results over time, adjust rules or add new ones to improve your match rate.
API reference: [Create match rule](/en/reference/matcher/create-match-rule)
## Run matching
***
Once sources are configured and data is uploaded, you can run the matching engine.
### Preview first, commit second
Matcher supports two execution modes:
| Mode | Behavior |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Dry run** | Calculates matches and generates a preview without persisting match artifacts or exceptions. It creates and completes a `MatchRun` record. |
| **Commit** | Persists the results: confirmed matches, confidence scores, and exceptions. |
Always start with a dry run. Review the preview to verify match quality before committing.
### Understanding confidence scores
Every match receives a confidence score from 0 to 100:
| Score range | Meaning | Action required |
| ----------- | ----------------- | ------------------------------------------------------------------------------------------------- |
| 90–100 | High confidence | Eligible for auto-confirmation, except `FUZZY` and `DATE_LAG` groups, which always require review |
| 60–89 | Medium confidence | Flagged for manual review |
| Below 60 | Low confidence | Not matched — becomes an exception |
Scores derive from the matched components and their configured weights. Exact and tolerance rules use the same weighting framework; neither rule type inherently yields higher scores.
API reference: [Run match](/en/reference/matcher/run-match) | [List match run groups](/en/reference/matcher/list-match-run-groups)
## Resolve exceptions
***
Exceptions are transactions that Matcher could not pair automatically. They represent the items that need human attention.
### Exception severity
Matcher classifies each exception by severity based on the transaction amount and how long it has been unmatched:
| Severity | Criteria | Suggested SLA |
| ------------ | ------------------------------------------- | ------------- |
| **Critical** | Amount >= 100,000 or unmatched >= 120 hours | 24 hours |
| **High** | Amount >= 10,000 or unmatched >= 72 hours | 72 hours |
| **Medium** | Amount >= 1,000 or unmatched >= 24 hours | 5 days |
| **Low** | All others | 7 days |
### Resolution options
* **Force match** — Manually pair the transaction with a counterpart when you know they belong together.
* **Create adjustment** — Record a correcting entry to account for the difference.
* **Unmatch** — If a confirmed match is incorrect, undo it so both transactions return to the unmatched pool.
* **Dispatch** — Send the exception through its configured JIRA or webhook route. This caller-directed action does not change its status.
API reference:
* [List exceptions](/en/reference/matcher/list-exceptions)
* [Unmatch group](/en/reference/matcher/unmatch-group)
## Example scenario
***
A fintech company reconciles its daily bank statement against internal ledger records.
**Setup:**
* Context: "Daily Bank Reconciliation", type `1:1`, interval `daily`
* Two sources: Chase Bank statement (`BANK`) and General Ledger (`LEDGER`)
* Two rules: Exact match (priority 1) and Tolerance match with 1% and 2-day window (priority 10)
**Daily workflow:**
1. Finance uploads the bank statement and ledger export.
2. Matcher runs a dry run. The preview shows 95% of transactions matched with high confidence.
3. The team reviews the preview and commits the results.
4. Five transactions remain as exceptions: two have small fee differences, three have no counterpart.
5. The team resolves the fee exceptions by creating adjustments. The three missing transactions are escalated for investigation.
## Next steps
***
Full guide to setting up reconciliation contexts.
Deep dive into all rule types and configuration options.
Map different file formats to Matcher's standard schema.
Strategies for handling unmatched transactions.
# Setup progress
Source: https://docs.lerian.studio/en/matcher/getting-started/matcher-setup-progress
Read a context's aggregated setup and activation-readiness state in a single request to drive an onboarding checklist or setup wizard.
The setup-progress endpoint returns configured-resource counts, the last-run state, and activation readiness for a context in a single aggregate — so a setup wizard or onboarding checklist derives its state from one request instead of many.
## Get setup progress
***
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/contexts/{contextId}/setup-progress" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"contextId": "550e8400-e29b-41d4-a716-446655440000",
"status": "DRAFT",
"sources": { "total": 2, "left": 1, "right": 1 },
"fieldMaps": { "mappedSources": 2 },
"matchRules": { "total": 3 },
"schedules": { "total": 1 },
"lastRun": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"completedAt": "2025-01-15T10:30:00Z"
},
"readiness": {
"ready": true,
"missing": []
},
"next": null
}
```
## What it returns
***
* **`status`** — the context lifecycle status: `DRAFT` (being configured), `ACTIVE` (running), `PAUSED` (suspended), or `ARCHIVED` (retired).
* **`sources`** — source counts split by matching side: `total`, `left`, `right`.
* **`fieldMaps.mappedSources`** — number of **mapped** sources: those with a field map, plus CAMT.053 sources, which are self-mapped (the parser embeds the ISO 20022 mapping and ignores field maps).
* **`matchRules.total`** — match-rule count for the context.
* **`schedules.total`** — schedule count for the context.
* **`lastRun`** — the most recent match run (`id`, `status` of `PROCESSING`/`COMPLETED`/`FAILED`, and `completedAt`). It is `null` when the context has never run.
* **`readiness`** — activation-readiness summary (see below).
* **`next`** — the deterministic next setup action to take, or `null` when the context is ready (see below).
## Readiness and the checklist
***
The `readiness` block reports whether the context satisfies every activation requirement:
```json theme={null}
{
"ready": false,
"missing": ["field_maps", "match_rules"]
}
```
`missing` holds **stable slugs** you can map to per-requirement checklist items. The possible values are:
* `sources_left` — the context needs at least one LEFT-side source.
* `sources_right` — the context needs at least one RIGHT-side source.
* `field_maps` — at least one source is unmapped: it has no field map and is not a self-mapped CAMT.053 source.
* `match_rules` — the context needs at least one match rule.
* `fee_rules` — the context enables fee normalization but has no fee rule. This requirement is **conditional**: it appears only when `feeNormalization` is set (`NET` or `GROSS`), and it mirrors the run precondition, which gates on fee **rules** — not fee schedules — being non-empty.
When a context is transitioned to `ACTIVE` before it is ready, the update is rejected with the generic configuration invalid-state `409`. That response does not include the missing slugs. Re-read setup progress after the conflict and use `readiness.missing` to render the remaining setup guidance.
## The next action
***
`next` turns `readiness.missing` into a concrete call. It is derived from `missing[0]` — the first unsatisfied requirement in the stable order above — and is `null` when the context is ready:
```json theme={null}
{
"slug": "field_maps",
"action": "add_field_map",
"operationId": "createFieldMap",
"method": "POST",
"path": "/v1/contexts/{contextId}/sources/{sourceId}/field-maps",
"requiredFields": ["mapping"],
"forSource": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Bank statement"
}
}
```
* **`slug`** — the readiness slug this action satisfies.
* **`action`** — a stable semantic label you can key UI copy on.
* **`operationId`**, **`method`**, **`path`** — the endpoint to call to satisfy the requirement.
* **`requiredFields`** — the **names** of the fields that create request requires. They never carry values; you supply those.
* **`forSource`** — present only for the `field_maps` action, naming the first unmapped source so you can fill `{sourceId}` without a separate lookup.
The full slug-to-action table:
| `slug` | `action` | Endpoint | `requiredFields` |
| --------------- | ------------------ | ------------------------------------------------------------- | ------------------------------- |
| `sources_left` | `add_left_source` | `POST /v1/contexts/{contextId}/sources` | `name`, `type`, `side` |
| `sources_right` | `add_right_source` | `POST /v1/contexts/{contextId}/sources` | `name`, `type`, `side` |
| `field_maps` | `add_field_map` | `POST /v1/contexts/{contextId}/sources/{sourceId}/field-maps` | `mapping` |
| `match_rules` | `add_match_rule` | `POST /v1/contexts/{contextId}/rules` | `priority`, `type`, `config` |
| `fee_rules` | `add_fee_rule` | `POST /v1/contexts/{contextId}/fee-rules` | `side`, `feeScheduleId`, `name` |
Both source-side actions resolve to the same `createSource` operation — the `side` field is what distinguishes them.
## How to use it during setup
***
1. **Render the checklist.** On each step of the wizard, GET setup-progress and use the counts (`sources`, `fieldMaps`, `matchRules`, `schedules`) to tick off completed items.
2. **Drive the primary button from `next`.** Instead of reimplementing the requirement order client-side, call the operation `next` names; re-read setup-progress afterwards to get the following action.
3. **Gate the "Activate" button.** Enable activation only when `readiness.ready` is `true`; otherwise list `readiness.missing` as the remaining steps.
4. **Show run health.** Once `lastRun` is present, surface its `status` and `completedAt` so operators can confirm the context is producing results.
Because the whole state comes from one call, you can poll this endpoint to keep the wizard live without orchestrating separate source, rule, and run reads.
## Response codes
***
| Status | Meaning |
| ------ | ----------------------- |
| `200` | Setup progress returned |
| `404` | Context not found |
# Extraction reviews
Source: https://docs.lerian.studio/en/matcher/imports/matcher-extraction-reviews
Review, approve, or reject AI-extracted transaction candidates before they are ingested, and use mapping proposals and job actions to prepare source data.
Matcher can extract transaction candidates from documents and propose field mappings using AI — but **AI output is never authoritative**. Nothing is reconciled until a human approves it. This guide covers the human-in-the-loop (HITL) extraction-review queue, AI mapping proposals, and the related job actions.
The document-extraction lane is gated by a global kill-switch **and** a per-tenant opt-in. A tenant that has not opted in receives `403` before any document bytes are stored or egressed.
## Enqueue a document for extraction
***
Upload a source document (PDF) to run deterministic + AI extraction. The resulting transaction candidates are queued in a review — nothing is reconciled yet.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/extract-document" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/pdf" \
--data-binary @statement.pdf
```
The response (`202 Accepted`) returns the queued review id, the candidate count, and a status that is always `PENDING_REVIEW` on enqueue:
```json theme={null}
{
"reviewId": "550e8400-e29b-41d4-a716-446655440000",
"candidateCount": 12,
"status": "PENDING_REVIEW"
}
```
## The review queue
***
### List reviews
Cursor-paginated list of extraction reviews for a context, optionally filtered by lifecycle status.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews?status=PENDING_REVIEW&limit=50" \
-H "Authorization: Bearer $TOKEN"
```
Query parameters: `status` (`PENDING_REVIEW`, `APPROVED`, `REJECTED`), `limit` (1–200), and `cursor`.
### Get one review
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}" \
-H "Authorization: Bearer $TOKEN"
```
A review carries its lifecycle, the proposed candidates, provenance, and linkage state:
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"contextId": "550e8400-e29b-41d4-a716-446655440000",
"sourceId": "550e8400-e29b-41d4-a716-446655440000",
"status": "PENDING_REVIEW",
"candidates": [
{
"source": "text_layer",
"fields": [
{ "canonicalKey": "amount", "value": "100.50", "confidence": 0.95, "page": 1 },
{ "canonicalKey": "date", "value": "2025-06-01", "confidence": 0.9, "page": 1 }
]
}
],
"version": 1,
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
```
Each candidate declares the lane that produced it: `text_layer` (PDF text, higher trust) or `vision` (OCR/vision model, lower trust). Field values are **verbatim tokens** — money stays a string, never a parsed amount.
## Approve or reject
***
### Approve
Approving a `PENDING_REVIEW` review runs the single deterministic handoff into the normal ingestion pipeline (dedup + outbox + match-trigger) and links the resulting job to the review. This is the **only** path from an AI candidate to a reconciled transaction, and it runs only on explicit human approval.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}/approve" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"reviewId": "550e8400-e29b-41d4-a716-446655440000",
"ingestionJobId": "550e8400-e29b-41d4-a716-446655440000",
"candidateCount": 12
}
```
### Reject
Rejecting discards the candidates — nothing is ingested. The body is optional; an empty body is a valid "reject with no reason".
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/extraction-reviews/{reviewId}/reject" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "poor scan quality, re-upload" }'
```
The approving/rejecting principal is recorded for audit.
## Mapping proposals
***
Before you declare a field map by hand, ask the advisor to inspect a representative sample and propose a **config-only** mapping. It is advisory and side-effect-free: producing a proposal **persists nothing**. You confirm the result through the existing field-map declaration path.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/mapping-proposal" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sample": "id;value;ccy;posted_at\nA1;10,50;BRL;2025-06-01\n",
"format": "csv",
"hints": { "locale": "pt-BR", "has_header": "true" }
}'
```
The response carries the proposed field map, source dialect, and a per-field breakdown with confidence and rationale:
```json theme={null}
{
"mapping": { "amount": "value", "external_id": "id" },
"dialect": {
"encoding": "utf-8",
"delimiter": "semicolon",
"decimalStyle": "comma",
"dateStyle": "iso"
},
"fields": [
{ "canonicalKey": "amount", "sourceColumn": "value", "confidence": 0.92, "rationale": "numeric column with comma decimal" }
]
}
```
The response never carries parsed values, amounts, or transactions.
## Fetch from an external transport
***
Trigger a manual fetch-and-ingest that lists every object matching the supplied transport coordinates (SFTP today) and streams each into the trusted-content ingestion pipeline. The body carries connection coordinates plus an **opaque credential reference — never a secret**.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/fetch" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "sftp",
"host": "sftp.bank.example",
"port": 22,
"path": "outbound/returns",
"glob": "*.ret",
"credentialRef": "cred-handle-123",
"format": "br/cnab240/febraban-base"
}'
```
The response (`202 Accepted`) returns a per-file outcome in fetch order. Per-file intake failures are reported without failing the batch:
```json theme={null}
{
"files": [
{ "name": "statement-2025-06.ret", "ingestionJobId": "550e8400-...", "transactionCount": 42 }
]
}
```
A transport-level failure (endpoint unreachable or credential rejected) returns `503`.
## Inspect job errors
***
After an import, list the stored per-row parse/normalization errors for a job (capped at 100 per job) to explain failed or partially-failed imports.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/imports/contexts/{contextId}/jobs/{jobId}/errors" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"items": [ ... ],
"totalErrors": 137,
"storedErrors": 100,
"errorCap": 100,
"truncated": true
}
```
`totalErrors` is the uncapped failure total; `truncated` is `true` when it exceeds the stored (capped) set.
## Response codes
***
| Status | Meaning |
| ------ | ----------------------------------------------------------------------------------------- |
| `200` | Review, list, mapping proposal, or job errors returned |
| `202` | Document enqueued / fetch accepted |
| `400` | Invalid input (empty body, bad status filter, invalid pagination, missing mapping sample) |
| `403` | Tenant not opted into document extraction |
| `404` | Review or job not found |
| `409` | Invalid review state transition |
| `422` | No candidates could be extracted / structurally invalid request body |
| `503` | Extraction, review, proposal, or fetch not enabled on this deployment |
# Formats and templates
Source: https://docs.lerian.studio/en/matcher/imports/matcher-import-formats
Browse the built-in format catalog Matcher can parse and register per-tenant fixed-width layout templates for operator-specific files.
Matcher parses incoming files against a catalog of built-in formats and, when a file does not fit one of them, against per-tenant fixed-width **layout templates** you define. This guide covers browsing the format catalog and managing layout templates.
## The format catalog
***
The catalog is the read-only inventory of formats the ingestion engine can parse. It is **global-first and static**: built-in parsers carry no tenant, so the response is identical for every authenticated caller. The catalog is organized as a `region → family → variant` tree, matching the canonical format descriptor axes.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/imports/formats" \
-H "Authorization: Bearer $TOKEN"
```
Each variant carries the canonical namespaced registry key — the identity an upload or source declaration pins:
```json theme={null}
{
"regions": [
{
"region": "BR",
"families": [
{
"family": "cnab240",
"variants": [
{ "variant": "febraban-base", "key": "br/cnab240/febraban-base" }
]
}
]
},
{
"region": "XX",
"families": [
{
"family": "camt",
"variants": [
{ "variant": "camt053", "key": "xx/camt/camt053" }
]
}
]
}
]
}
```
Regions use the ISO-3166 alpha-2 code (uppercased), or `XX` for region-neutral formats. Even a single-canonical-layout family (for example `camt`) names its layout as the `variant` — `camt053` above.
The catalog takes no path, query, or body parameters — the tenant is irrelevant to the built-in catalog.
## Layout templates
***
When a file uses an operator- or brand-specific fixed-width layout that no built-in parser covers, register a **layout template**. A template namespaces a positional layout under the `{region, family, variant}` axes and is resolved by the parse path as an additive layout source for your tenant.
Every submission and edit runs through a **well-formedness gate** *before* storage. Overrun, overlap, missing-required fields, a zero-field record, or a mis-marked money column all reject with `422` and the template is never stored.
Money third rail: a money column **must** declare `kind: "decimal"`. A money field that omits or mis-marks its kind is rejected by the submission gate — it never reaches the parse path.
### Create a template
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/formats/templates" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"region": "BR",
"family": "cnab400",
"variant": "acme-cobranca",
"discriminatorStart": 0,
"discriminatorLength": 1,
"records": [
{
"recordType": "1",
"width": 33,
"fields": [
{ "name": "external_id", "startByte": 1, "length": 10, "kind": "string" },
{ "name": "amount", "startByte": 11, "length": 12, "kind": "decimal" },
{ "name": "date", "startByte": 23, "length": 10, "kind": "date" }
]
}
],
"requiredFields": ["external_id", "amount", "date"]
}'
```
Field values:
* `region` — ISO alpha-2 region (uppercased) or `XX`.
* `family` — closed-enum format family the template namespaces under.
* `variant` — open operator/brand axis (must be non-blank).
* `discriminatorStart` / `discriminatorLength` — the byte range the parser reads to select a record type.
* `records[]` — each record type with its fixed `width` (bytes) and ordered positional `fields`.
* `fields[].kind` — `string`, `decimal` (money/numeric verbatim token, parsed downstream), or `date`.
* `requiredFields` — field names the variant must declare across its record types.
A successful create returns `201` with the stored template, including its `formatKey` (for example `br/cnab400/acme-cobranca`), the discriminator, the full positional layout, and `recordWidths`.
### List and get templates
```bash theme={null}
# List every active template on the tenant (unpaginated)
curl -X GET "https://api.matcher.example.com/v1/imports/formats/templates" \
-H "Authorization: Bearer $TOKEN"
# Get one template by id
curl -X GET "https://api.matcher.example.com/v1/imports/formats/templates/{templateId}" \
-H "Authorization: Bearer $TOKEN"
```
The list is unpaginated: layout templates are bounded operator config.
### Update and delete a template
`PUT` is a **full replace**, not a sparse patch — the byte-range invariants are whole-layout properties. The replacement runs through the same well-formedness gate the create path enforces; a failing layout rejects with `422` and the stored template is left unchanged.
```bash theme={null}
curl -X PUT "https://api.matcher.example.com/v1/imports/formats/templates/{templateId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "region": "BR", "family": "cnab400", "variant": "acme-cobranca", "discriminatorStart": 0, "discriminatorLength": 1, "records": [ ... ] }'
```
```bash theme={null}
# Soft-delete, freeing the format/variant key for reuse
curl -X DELETE "https://api.matcher.example.com/v1/imports/formats/templates/{templateId}" \
-H "Authorization: Bearer $TOKEN"
```
Delete responds `204`. A missing template returns `404`; a format-key collision with another active template returns `409`.
## Response codes
***
| Status | Meaning |
| ------ | --------------------------------------------------------------------------------------------------------- |
| `200` | Catalog, template list, get, or update returned |
| `201` | Template created |
| `204` | Template soft-deleted |
| `400` | Structurally malformed field/layout |
| `404` | Template not found |
| `409` | Format/variant key already claimed |
| `422` | Layout failed the well-formedness gate (overrun, overlap, missing-required, zero-field, mis-marked money) |
| `503` | Format catalog or template store not wired on this deployment |
# Aggregator connections
Source: https://docs.lerian.studio/en/matcher/integrations/matcher-aggregator-connections
Provision Open-Finance data-aggregator connections (Pluggy, Belvo), test them, browse connector types, and mint webhook tokens for inbound pulls.
Aggregator connections let Matcher pull transaction data from Open-Finance data aggregators (Pluggy, Belvo). You create a connection with a sealed credential, mint a webhook token bound to it, and the aggregator's webhooks then drive inbound pulls. This guide covers the full lifecycle.
Credentials (`clientId`/`secret`) are **inbound-only**: they are sealed before persistence and are never returned in a response, a log, or an error. Every response on this surface is secret-free by construction. The tenant is always resolved from the JWT, never from the request body.
## Create a connection
***
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vendor": "pluggy",
"configName": "pluggy-main",
"baseUrl": "https://api.pluggy.ai",
"accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"clientId": "...",
"secret": "..."
}'
```
Field values:
* `vendor` — `pluggy` or `belvo`.
* `configName` — unique (tenant-scoped) connection identity; a duplicate is a `409`. **The webhook token-mint endpoint binds a token to this name.**
* `baseUrl` — vendor API base URL, stored as the connection host.
* `accountRef` — opaque vendor account reference (Pluggy `itemId`, Belvo link id) threaded onto the webhook pull.
* `clientId` / `secret` — aggregator API credential; sealed and never emitted.
A successful create returns `201` with the secret-free connection descriptor:
```json theme={null}
{
"vendor": "pluggy",
"configName": "pluggy-main",
"baseUrl": "https://api.pluggy.ai",
"accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}
```
## List, get, update, delete
***
```bash theme={null}
# List (cursor-paginated, secret-free)
curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections?limit=20" \
-H "Authorization: Bearer $TOKEN"
# Get by opaque id
curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \
-H "Authorization: Bearer $TOKEN"
```
### Update
Edit an existing connection by id so a mistyped `baseUrl` is not permanent. The **vendor is immutable**. The credential is optional: supply **both** `clientId` and `secret` to rotate the sealed credential, or omit **both** to leave the stored secret intact. Supplying exactly one is a `400`.
```bash theme={null}
curl -X PUT "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"configName": "pluggy-main",
"baseUrl": "https://api.pluggy.ai",
"accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}'
```
### Delete
```bash theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \
-H "Authorization: Bearer $TOKEN"
```
Delete soft-deletes the connection (`204`), freeing its config name for reuse. A non-aggregator connection id returns `404` on any by-id operation — this surface never confirms the existence of a non-aggregator row.
## Test a connection
***
Run a live connectivity check for an existing connection using its already-sealed credential, addressed by `(vendor, configName)`. No credential is supplied or returned.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections/test" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "vendor": "pluggy", "configName": "pluggy-main" }'
```
```json theme={null}
{ "vendor": "pluggy", "configName": "pluggy-main", "healthy": true }
```
A credentials-don't-work outcome is an **expected** test result surfaced as `"healthy": false` with a `200` — not an error. A missing connection is a `404`.
## Connector types
***
List the connector types the engine registry has actually registered for this deployment, each tagged with a backend-derived category (`database` or `rest`). The list reflects the live registry — only connectors registered at boot appear. It drives the connection form's type-select.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/connector-types" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"types": [
{ "type": "POSTGRESQL", "category": "database" },
{ "type": "STRIPE", "category": "rest" }
]
}
```
Aggregator-vendor types (Pluggy/Belvo) are excluded here — they are provisioned through the aggregator-connections surface above.
## Mint a webhook token
***
Mint a webhook token bound to an existing aggregator connection. The raw token and its provider-facing webhook URL are returned **once** — only the token's SHA-256 hash is stored.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/webhooks/tokens" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vendor": "pluggy",
"connection_config_name": "pluggy-main"
}'
```
```json theme={null}
{
"token": "",
"webhook_url": "https://api.matcher.example.com/v1/discovery/webhooks/pluggy/",
"vendor": "pluggy"
}
```
Configure the returned `webhook_url` in the aggregator's dashboard. A missing target connection returns `404`.
## Response codes
***
| Status | Meaning |
| ------ | -------------------------------------------------------------- |
| `200` | Get, list, test, or connector-types returned |
| `201` | Connection created / token minted |
| `204` | Connection soft-deleted |
| `400` | Invalid vendor, partial credential pair, or invalid pagination |
| `401` | Tenant could not be resolved |
| `404` | Connection not found (or not an aggregator) |
| `409` | Connection with that config name already exists |
# Discovery
Source: https://docs.lerian.studio/en/matcher/integrations/matcher-discovery
Use Discovery and Fetcher to detect external data sources, inspect their schemas, and pull transactions into Matcher automatically.
Discovery automates data source detection and extraction through Fetcher. Instead of manually uploading files, Discovery connects to external systems, identifies available data, and extracts transactions directly into Matcher.
## What Discovery solves
***
Manual file uploads create friction at every step. Teams export files, transfer them, monitor for failures, and re-upload when something goes wrong. This process is time-consuming, error-prone, and breaks when data volume grows.
Discovery replaces the manual pipeline. It connects to external systems through Fetcher, detects available data sources automatically, and pulls transactions into Matcher on demand. When a new data source appears — a new bank connection, a new payment processor — Discovery finds it without reconfiguration.
## How Discovery works
***
Discovery runs on Fetcher's extraction engine, which Matcher hosts in-process; Fetcher is not a remote service. The embedded engine manages connections to external databases and runs extractions locally. Discovery exposes those connections and coordinates the extraction process, handing results directly to Ingestion.
The workflow has seven steps:
1. **Check status** — Confirm Discovery and its embedded engine are available.
2. **Browse connections** — See all data sources the embedded engine has access to.
3. **Inspect a connection** — Review the schema to understand what fields are available.
4. **Test a connection** — Validate the connection before committing to an extraction.
5. **Create an extraction** — Request that Matcher pull data from a specific source.
6. **Monitor progress** — Track extraction status as data flows in.
7. **Refresh connections** — Rescan when new data sources are added.
## Discovery workflow
***
### Check Discovery status
Verify that Discovery and the embedded Fetcher engine are operational before starting.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/status" \
-H "Authorization: Bearer $TOKEN"
```
API Reference: [Get Discovery status](/en/reference/matcher/discovery-status)
### Browse connections
List all data sources available through the embedded Fetcher engine.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/connections" \
-H "Authorization: Bearer $TOKEN"
```
The response lists each connection with its name, type (database, API, file store), and current status.
API Reference: [List connections](/en/reference/matcher/list-discovery-connections)
### Get a connection
Retrieve a single discovered Fetcher connection by its internal identifier:
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/connections/{connectionId}" \
-H "Authorization: Bearer $TOKEN"
```
`GET /v1/discovery/connections/{connectionId}` returns the full `ConnectionResponse` (name, type, status, and metadata) for one connection — useful when you already hold a `connectionId` (for example from a source binding's query rail) and want its current details without listing every connection.
API Reference: [Get discovery connection](/en/reference/matcher/retrieve-discovery-connection)
### Inspect a connection
Review the schema of a specific connection to understand what data fields are available before extracting.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/schema" \
-H "Authorization: Bearer $TOKEN"
```
Use schema inspection to confirm that required fields — transaction IDs, amounts, dates, references — exist before building field mappings.
API Reference: [Get connection schema](/en/reference/matcher/get-connection-schema)
### Test a connection
Validate that Matcher can reach and read from a connection before creating an extraction.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/test" \
-H "Authorization: Bearer $TOKEN"
```
A successful test confirms connectivity and read access. Always test before creating an extraction — especially for new or recently modified connections.
API Reference: [Test connection](/en/reference/matcher/test-discovery-connection)
### Create an extraction
Request that Matcher pull transaction data from a specific connection into the current context.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/extractions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tables": {
"transactions": {}
},
"startDate": "2026-06-01",
"endDate": "2026-06-30"
}'
```
The response returns an extraction ID. Use it to monitor progress.
API Reference: [Create extraction](/en/reference/matcher/create-extraction)
### Monitor extraction progress
Track the status of an active extraction by polling its status with `GET`.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/extractions/{extractionId}" \
-H "Authorization: Bearer $TOKEN"
```
Extraction status transitions from `PENDING` → `SUBMITTED` → `EXTRACTING` → `COMPLETE` (or `FAILED`/`CANCELLED`). The response carries the extraction `status`, an `errorMessage` when it failed, and the linked `ingestionJobId` once the extraction bridges into ingestion.
API Reference: [Get extraction](/en/reference/matcher/retrieve-extraction)
### Refresh available connections
When new data sources are registered with the embedded engine, trigger a refresh so Discovery picks them up.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/refresh" \
-H "Authorization: Bearer $TOKEN"
```
API Reference: [Refresh connections](/en/reference/matcher/refresh-discovery)
### List connector types
List the connector (datasource) types the engine registry has registered for this deployment. Each entry carries a backend-derived `category` (`database` or `rest`). The registry is live—only connectors registered at boot appear. Aggregator vendors (Pluggy/Belvo) are excluded; provision those through the aggregator-connections surface below.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/discovery/connector-types" \
-H "Authorization: Bearer $TOKEN"
```
#### Response
```json theme={null}
{
"types": [
{ "type": "POSTGRESQL", "category": "database" },
{ "type": "MYSQL", "category": "database" }
]
}
```
## Aggregator connections (Open Finance)
***
Open-Finance data-aggregator connections (Pluggy or Belvo) let Matcher pull transactions from bank aggregators. Credential material (`clientId`/`secret`) is **sealed on write and never returned**—every read is secret-free by construction.
### Create an aggregator connection
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vendor": "pluggy",
"configName": "pluggy-main",
"baseUrl": "https://api.pluggy.ai",
"accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"clientId": "...",
"secret": "..."
}'
```
All six fields are required. `vendor` is one of `pluggy` or `belvo`. `configName` is the tenant-scoped name the webhook token-mint endpoint binds to. Returns **201** with a secret-free connection.
#### Response
```json theme={null}
{
"vendor": "pluggy",
"configName": "pluggy-main",
"baseUrl": "https://api.pluggy.ai",
"accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}
```
### List, get, update, and delete
```bash theme={null}
# List (cursor-paginated, secret-free)
curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections" \
-H "Authorization: Bearer $TOKEN"
# Get one by id
curl -X GET "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \
-H "Authorization: Bearer $TOKEN"
# Update (PUT). vendor is immutable. Supply clientId+secret together to rotate
# the sealed credential, or omit both to keep the stored secret intact.
curl -X PUT "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"configName": "pluggy-main",
"baseUrl": "https://api.pluggy.ai",
"accountRef": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}'
# Delete (soft-delete; frees the config name for reuse). Returns 204.
curl -X DELETE "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}" \
-H "Authorization: Bearer $TOKEN"
```
### Test an aggregator connection
Run a live connectivity check against an existing connection's already-sealed credential, addressed by `(vendor, configName)`. No credential is supplied or returned—the result is a secret-free boolean health.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections/test" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vendor": "pluggy",
"configName": "pluggy-main"
}'
```
#### Response
```json theme={null}
{
"vendor": "pluggy",
"configName": "pluggy-main",
"healthy": true
}
```
## Aggregator webhook tokens
***
Aggregators push data change signals to Matcher via webhooks. Mint an opaque token bound to an aggregator connection, then configure the returned URL in the vendor dashboard.
### Mint a webhook token
The raw token and its provider-facing URL are returned **exactly once**—only the token's SHA-256 hash is stored.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/discovery/webhooks/tokens" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vendor": "pluggy",
"connection_config_name": "pluggy-main"
}'
```
#### Response
```json theme={null}
{
"vendor": "pluggy",
"token": "",
"webhook_url": "https://api.matcher.example.com/v1/discovery/webhooks/pluggy/"
}
```
### Receiving webhooks
The vendor calls `POST /v1/discovery/webhooks/{provider}/{webhookToken}` (no operator JWT). It is authenticated by the opaque path token **plus** a per-provider source check: a valid HMAC-SHA256 of the raw body in the `X-Webhook-Signature` header, **or** membership in the provider's source-IP allowlist. Both layers fail closed. A valid first delivery returns **202 Accepted** and the signaled data is pulled asynchronously into the ingestion pipeline; a replay of an already-processed event returns **200 OK**.
## Best practices
***
A failed extraction mid-run is harder to recover from than a failed test. Test every connection before creating an extraction — especially when connecting to a new source or after a credential rotation.
Field names vary across systems. A bank might call the transaction date `value_date` while your ledger uses `posting_date`. Check the schema before configuring field mappings to avoid silent mismatches.
Large extractions take time. Don't assume completion — poll the extraction status and confirm the record count before starting a match run. Starting a run on incomplete data generates incorrect exceptions.
Discovery doesn't scan for new connections automatically. When a new payment processor is added or a new database is registered with the embedded engine, trigger a refresh. Otherwise, Discovery won't show the new source.
Use date range parameters to extract only the data relevant to the current reconciliation period. Extracting unbounded data increases processing time and may pull records that belong to already-closed contexts.
## Next steps
***
Configure the external data sources that Discovery connects to.
Map fields from extracted data to Matcher's transaction model.
Full API reference for Discovery endpoints.
# External sources
Source: https://docs.lerian.studio/en/matcher/integrations/matcher-external-sources
Connect banks, payment gateways like Stripe and Adyen, ERPs such as SAP or Oracle, and card networks to Matcher via LEDGER, BANK, GATEWAY, or CUSTOM types.
External sources provide transaction data from systems outside your organization. This guide covers how to connect banks, payment gateways, and other external systems to Matcher.
## Supported source types
***
Matcher supports five source types. Each represents a category of data origin:
| Type | Description | Typical use |
| --------- | --------------------- | --------------------------------------------- |
| `LEDGER` | Internal ledger | Internal accounting systems (including Midaz) |
| `BANK` | Bank statement feed | External bank feeds |
| `GATEWAY` | Payment gateway | Payment processors (Stripe, Adyen, PayPal) |
| `CUSTOM` | Bespoke feed | ERPs, card networks, or any other data source |
| `FETCHER` | Discovery-engine pull | Aggregator connections pulled automatically |
## Ingestion methods
***
Transaction data reaches Matcher through several paths:
| Method | Use Case |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| **File upload** | Manual uploads (CSV, JSON, XML, OFX, camt.053, CNAB, acquirer EDIs) |
| **Transport fetch** | Matcher pulls files from a configured transport (for example SFTP) and ingests them |
| **Discovery extraction** | [Discovery](/en/matcher/integrations/matcher-discovery) extracts data from Fetcher connections into ingestion |
| **Aggregator webhooks** | [Open-Finance aggregators](/en/matcher/integrations/matcher-aggregator-connections) signal new data, pulled asynchronously |
## File-based ingestion
***
The most common method for bank statements and ERP exports.
### Manual upload
Use the file upload endpoint to import transaction files manually.
API Reference: [Upload transaction file](/en/reference/matcher/upload-transaction-file)
## Bank connections
***
### Standard Bank format
Most banks provide statements in a format Matcher parses natively — CSV, OFX, camt.053, or the Brazilian CNAB layouts:
```json theme={null}
{
"name": "Chase Business Account",
"type": "BANK",
"config": {
"bank_name": "Chase",
"account_number": "****1234",
"currency": "USD",
"statement_format": "CSV",
"timezone": "America/New_York"
}
}
```
The `config` object is free-form descriptive metadata — Matcher stores it but does not interpret keys like `bank_name` or `statement_format`. Parsing behavior is driven by the declared format dialect and the pinned configuration keys (error-rate policy, duplicate policy, `blank_external_id`, and camt.053 options), not by these labels.
API Reference: [Create source](/en/reference/matcher/create-source)
## ERP and custom connections
***
Use the `CUSTOM` source type for ERP systems (SAP, Oracle, NetSuite, etc.) and any other data source that doesn't fit the `BANK`, `LEDGER`, or `GATEWAY` categories.
### Example: ERP source
```json theme={null}
{
"name": "SAP S/4HANA",
"type": "CUSTOM",
"config": {
"erp_type": "SAP",
"company_codes": ["1000", "2000"]
}
}
```
Export transaction data from your ERP and upload it through Matcher's file upload endpoint. Use [field mapping](/en/matcher/configuration/matcher-field-mapping) to translate ERP-specific fields into Matcher's canonical format.
## Payment processor connections
***
### Stripe
```json theme={null}
{
"name": "Stripe Payments",
"type": "GATEWAY",
"config": {
"provider": "stripe"
}
}
```
### Adyen
```json theme={null}
{
"name": "Adyen Settlements",
"type": "GATEWAY",
"config": {
"provider": "adyen",
"merchant_account": "CompanyECOM"
}
}
```
Export settlement reports from your payment processor and upload them through Matcher's file upload endpoint.
### Card networks
For card network settlement files (Visa, Mastercard, Elo), use the `CUSTOM` source type:
```json theme={null}
{
"name": "Visa Settlement",
"type": "CUSTOM",
"config": {
"network": "VISA",
"file_format": "TC33"
}
}
```
## Connection security
***
### Credential storage
All credentials should be stored securely in an encrypted vault and referenced by ID in source configurations.
### Ip allowlisting
Configure IP allowlisting at the infrastructure level (load balancer, API gateway, or firewall) to restrict which IPs can push data to Matcher. Source entities do not have a `settings.security` configuration. Manage IP restrictions outside the application.
### Webhook signatures
Matcher signs outbound webhook payloads with HMAC-SHA256. For inbound data, verify signatures at the infrastructure level before data reaches Matcher. Source entities do not have a `settings.webhook` configuration.
## Data format requirements
***
### Required fields
Every transaction must include:
Field maps use a **closed** canonical vocabulary: the mapping *keys* are fixed, and the *values* name the source's raw column. These canonical keys are required:
| Canonical key | Type | Description |
| ------------- | ------- | ------------------------------------ |
| `external_id` | String | Source-system transaction identifier |
| `amount` | Decimal | Transaction amount |
| `currency` | String | ISO 4217 code |
| `date` | Date | Transaction date |
### Optional fields
| Canonical key | Type | Description |
| -------------- | ------- | ----------------------------------------------------------------------- |
| `description` | String | Reference/description text (feeds the transaction's description column) |
| `fee_amount` | Decimal | Fee slot: source column carrying the fee amount |
| `fee_currency` | String | Fee slot: source column carrying the fee currency |
No other keys are accepted—keys outside this vocabulary are rejected.
### Field mapping
Manage field maps with the dedicated endpoint (not the source's `config` object). The request body is a single `mapping` object of `{ canonicalKey: sourceColumnName }` pairs:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/sources/{sourceId}/field-maps" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mapping": {
"external_id": "TXN_ID",
"amount": "trans_amount",
"currency": "CCY",
"date": "POST_DATE",
"description": "memo",
"fee_amount": "mdr_fee",
"fee_currency": "fee_ccy"
}
}'
```
Refer to [Field Mapping](/en/matcher/configuration/matcher-field-mapping) for details.
## Best practices
***
Check that uploaded files contain columns for the required canonical fields (external\_id, amount, currency, date) before uploading. This prevents ingestion errors.
Standardize on a single format (CSV, JSON, or XML) per source to simplify field mapping and reduce errors.
Store all API keys and passwords in the vault. Never include credentials in configuration payloads.
Validate field mapping and data quality with sample files before uploading production data.
## Next steps
***
Configure how source fields map to Matcher.
Manual file upload procedures.
# Matcher and Midaz
Source: https://docs.lerian.studio/en/matcher/integrations/matcher-midaz-integration
See how Matcher works alongside Midaz Ledger — two independent services that share data through exports and imports rather than a live API connection.
**There's no direct Matcher-to-Midaz connector.** Matcher is a stand-alone reconciliation service. It doesn't open a live connection to Midaz, and it has no Midaz-specific configuration. This page explains how the two products work together conceptually.
Matcher and [Midaz](/en/midaz) are separate Lerian services with distinct jobs:
* **Midaz** is the ledger — the system of record for balances and postings.
* **Matcher** is the reconciliation engine — it compares two independent datasets and reports what agrees, what doesn't, and why.
They are **complementary, not coupled**. You get value from Midaz-plus-Matcher by reconciling ledger data *against* an external dataset (a bank statement, a gateway settlement report), which is exactly the problem Matcher exists to solve. The data moves between them by **export and import**, not through a real-time link.
## How they fit together
***
A typical Lerian reconciliation flow:
1. **Midaz records postings.** Midaz books transactions in the ledger as usual.
2. **You export the ledger data** for the period you want to reconcile (for example, a day's cash-account entries).
3. **You import that export into Matcher** as one side of a context — a source of type `LEDGER`.
4. **You import the counterparty data** — the bank statement or gateway report — as the other side.
5. **Matcher matches the two sides** using your match rules and surfaces exceptions for review.
Midaz is the origin of one dataset; the bank or gateway is the origin of the other. Matcher never talks to Midaz directly — it works from the exported data you feed it.
## Modeling Midaz as a source
***
Inside a Matcher context, ledger data from Midaz is represented by a source with type `LEDGER`. `LEDGER` is simply Matcher's category for "internal ledger / accounting system" data; it's not a Midaz driver.
```json theme={null}
{
"name": "Midaz Cash Ledger",
"type": "LEDGER",
"side": "LEFT",
"config": {}
}
```
API Reference: [Create source](/en/reference/matcher/create-source)
The other side of the context is the external dataset you're reconciling against — usually a `BANK` or `GATEWAY` source on the opposite `side`.
Source types available in Matcher:
| Type | Meaning |
| --------- | -------------------------------------------------------- |
| `LEDGER` | Internal ledger / accounting system (e.g. Midaz exports) |
| `BANK` | Bank statement feed |
| `GATEWAY` | Payment gateway report |
| `CUSTOM` | Any other bespoke feed |
| `FETCHER` | Discovery-engine pull |
## Getting ledger data into Matcher
***
Matcher ingests the data you export from Midaz the same way it ingests any other source — there's no Midaz-specific transport.
* **File upload.** Export ledger entries (CSV/JSON) and upload the file to the source. This is the most common path.
* **Discovery-engine query rail.** For sources bound on the `query` rail, Matcher pulls rows through a discovery-engine connection instead of a file. This is a generic ingestion rail, not a Midaz connector.
Either way, you then define a [field map](/en/matcher/configuration/matcher-field-mapping) that renames the exported columns to Matcher's canonical fields (`external_id`, `amount`, `currency`, `date`, and optional `description`, `fee_amount`, `fee_currency`).
API Reference: [Upload transaction file](/en/reference/matcher/upload-transaction-file)
### Custom field mapping
Most ledger columns map straight across. Field-map lookups are **flat** — a mapping value must name a column that exists at the top level of the exported row, so a value that lives inside transaction metadata must be exported as its own flat column before Matcher can match on it.
For example, Brazil's Pix `endToEndId` is stored in Midaz transaction metadata. To reconcile on it, export it as a flat column (here `endToEndId`) and map `external_id` to that column — `external_id` is the reference the matching engine compares across sources, so both sides of the context must map it to a value they share (the `description` key is display-only and never feeds matching):
```json theme={null}
{
"mapping": {
"external_id": "endToEndId",
"amount": "amount",
"currency": "asset_code",
"date": "created_at"
}
}
```
Every field map must declare all four required canonical keys (`external_id`, `amount`, `currency`, `date`) explicitly — there is no automatic mapping. See [Field mapping](/en/matcher/configuration/matcher-field-mapping) for the full canonical vocabulary.
## Shared platform foundations
***
Although there's no live integration, Matcher is built for the same platform as Midaz and mirrors several of its patterns:
* **Authentication.** Matcher uses the shared Lerian auth stack, so the same identity provider and tokens used across the platform apply.
* **Multi-tenancy.** Matcher follows a pool-per-tenant isolation model (a dedicated database per tenant) in line with the rest of the stack, keeping each tenant's data separate.
These are platform-level commonalities, not a Matcher-to-Midaz data channel.
## What this integration is *not*
***
To avoid confusion with earlier drafts of this page:
* Matcher has **no** `MIDAZ_BASE_URL` or `MIDAZ_GRPC_ADDRESS` setting.
* There is **no** real-time sync mode and **no** `account_filter` for a Midaz source.
* Matcher **doesn't** subscribe to Midaz events or open a gRPC/HTTP connection to Midaz.
Reconciling Midaz data means exporting it and importing it into Matcher like any other source.
## Best practices
***
Export only the ledger accounts and period you actually need to reconcile. Smaller, targeted exports keep contexts fast and matching precise.
If you reconcile daily, export daily. Match the export schedule to the counterparty data (bank statement) availability.
Field maps rename columns but do not transform values. Produce exports whose amounts, dates, and currency codes are already in the shapes your match rules expect.
Include stable references (invoice numbers, external IDs) as columns in the export so match rules can key on them for higher match rates.
## Next steps
***
Connect the bank or gateway side of the reconciliation.
Map exported ledger columns to Matcher's canonical fields.
# Webhooks and callbacks
Source: https://docs.lerian.studio/en/matcher/integrations/matcher-webhooks-callbacks
Emit Matcher lifecycle events to external tools through outbound webhooks, and receive resolution callbacks so JIRA or ServiceNow keep exceptions in sync.
Webhooks enable real-time communication between Matcher and external systems. This guide covers outbound event notifications and inbound resolution callbacks.
## Overview
***
Matcher supports bidirectional webhook communication, keeping your operational tools in sync with every reconciliation event in real time. This reduces manual intervention, helps maintain SLA compliance, and ensures a continuous audit trail across all connected systems.
* **Outbound webhooks**: exception routing dispatches exceptions to external targets — JIRA, ServiceNow, or an HTTP webhook endpoint you configure
* **Inbound callbacks**: External systems notify Matcher when actions are taken
When an exception is routed to a webhook target, Matcher delivers a signed HTTP request to your endpoint. External systems like JIRA or ServiceNow can then send callbacks to update exception status or close items automatically. This two-way flow keeps your tools in sync without manual intervention.
Beyond exception dispatch, Matcher publishes its full lifecycle event catalog on the platform's streaming backbone — those events are consumed as a stream, not delivered as HTTP webhooks.
## Outbound events
***
Matcher emits events when significant actions occur in the reconciliation process. The catalog below is published on the streaming backbone; exception events additionally reach HTTP webhook endpoints through [exception routing](/en/matcher/configuration/matcher-exception-routing).
### Available events
Matcher's event catalog is defined centrally. The most commonly consumed events are grouped by domain below.
**Configuration**
| Event | Trigger | Typical Use |
| -------------------------------- | ---------------------------------- | ---------------------- |
| `reconciliation_context.created` | Reconciliation context created | Provisioning sync |
| `reconciliation_context.updated` | Context metadata or status changed | Config change tracking |
| `reconciliation_context.deleted` | Context deleted | Downstream teardown |
| `reconciliation_source.created` | Source created within a context | Source onboarding |
| `match_rule.created` | Match rule created | Rule change audit |
| `match_rule.reordered` | Rule priorities reordered | Rule change audit |
**Discovery (Fetcher)**
| Event | Trigger | Typical Use |
| ---------------------------------- | ------------------------------------------- | --------------------- |
| `fetcher_connection.synced` | Connection and schema snapshot synchronized | Discovery monitoring |
| `fetcher_connection.unreachable` | Connection marked unreachable | Connectivity alerting |
| `extraction_request.created` | Extraction request created | Extraction monitoring |
| `extraction_request.submitted` | Extraction accepted by Fetcher | Extraction monitoring |
| `extraction_request.completed` | Extraction completed with artifact | Data readiness |
| `extraction_request.failed` | Extraction failed | Error alerting |
| `extraction_request.cancelled` | Extraction cancelled | Pipeline monitoring |
| `extraction_request.bridged` | Extraction linked to an ingestion job | Pipeline monitoring |
| `extraction_request.bridge_failed` | Bridge to ingestion failed | Error alerting |
**Ingestion**
| Event | Trigger | Typical Use |
| --------------------- | ------------------------------------ | ------------------------ |
| `ingestion.completed` | File import finished | Data pipeline monitoring |
| `ingestion.failed` | File import failed | Error alerting |
| `transaction.ignored` | Unmatched transaction marked ignored | Audit trail |
**Matching**
| Event | Trigger | Typical Use |
| ---------------------------- | ------------------------------------ | ------------------------- |
| `match_run.completed` | Match job finished | Job monitoring, reporting |
| `match_run.failed` | Match job failed | Error alerting |
| `match_group.confirmed` | Match group confirmed | Downstream updates |
| `match_group.unmatched` | Confirmed match reverted | Correction tracking |
| `transaction.matched` | Transaction marked matched | Audit logging |
| `transaction.pending_review` | Non-automatic candidate needs review | Review queue triggers |
| `fee_variance.created` | Fee variance detected | Fee investigation |
**Exceptions and disputes**
| Event | Trigger | Typical Use |
| --------------------------------- | --------------------------------------- | ------------------ |
| `exception.assigned` | Exception assigned to an owner | User notification |
| `exception.resolved` | Exception resolved | Status sync |
| `exception.dispatched` | Exception sent to an external target | Ticket creation |
| `exception.callback_processed` | External callback processed | Status sync |
| `exception.force_match_resolved` | Exception resolved via force match | Approval workflows |
| `exception.adjust_entry_resolved` | Exception resolved via adjustment entry | Audit trail |
| `exception_comment.added` | Comment added to an exception | Collaboration sync |
| `exception_comment.deleted` | Exception comment deleted | Collaboration sync |
| `dispute.opened` | Dispute opened for an exception | Dispute tracking |
| `dispute.won` | Dispute closed as won | Status sync |
| `dispute.lost` | Dispute closed as lost | Status sync |
| `evidence.submitted` | Evidence submitted to a dispute | Dispute tracking |
**Governance and reporting**
| Event | Trigger | Typical Use |
| -------------------------- | ------------------------------ | --------------------- |
| `audit_log.created` | Audit log entry appended | Compliance monitoring |
| `archive_metadata.created` | Archive lifecycle started | Archival monitoring |
| `archive.uploaded` | Archive object uploaded | Archival monitoring |
| `archive.completed` | Archive verified and completed | Archival monitoring |
| `actor.pseudonymized` | Actor mapping pseudonymized | Compliance monitoring |
| `export_job.created` | Export job queued | Export monitoring |
| `export_job.succeeded` | Export job completed | Download readiness |
| `export_job.failed` | Export job failed | Error alerting |
| `export_job.expired` | Export artifact expired | Export lifecycle |
### Webhook delivery payload
Exception dispatches to a webhook target carry a consistent payload — `eventId`, `eventType`, `timestamp`, the exception snapshot under `data`, and routing/tracing information under `metadata`:
```json theme={null}
{
"eventId": "0e8f1c2a-5b6d-4f3e-9a7b-1c2d3e4f5a6b",
"eventType": "exception.dispatched",
"timestamp": "2026-01-20T10:30:00Z",
"data": {
"exceptionId": "9b2f4e6a-1c3d-4e5f-8a9b-0c1d2e3f4a5b",
"transactionId": "7a1b3c5d-9e8f-4a2b-b6c7-d8e9f0a1b2c3",
"severity": "HIGH",
"status": "PENDING",
"amount": "15000.00",
"currency": "USD",
"reason": "No matching ledger entry found",
"sourceType": "LEFT",
"createdAt": "2026-01-20T10:29:15Z",
"dueAt": "2026-01-23T10:29:15Z"
},
"metadata": {
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"target": "WEBHOOK",
"queue": "ops-review",
"ruleName": "high-value-unmatched",
"assignee": "ops-team"
}
}
```
`data.dueAt` and the `metadata` fields `traceId`, `queue`, `ruleName`, and `assignee` are omitted when not set. Streaming catalog events (the tables above) follow their own per-event schemas on the event stream and are not delivered in this HTTP shape.
## Inbound callbacks
***
External systems send callbacks to Matcher to update exception status after processing. The callback endpoint accepts status updates, resolution notes, and assignee changes from any external system.
### Process a callback
The callback endpoint is authenticated by the `X-Callback-Token` header — an opaque token minted through the [callback credentials](#callback-credentials) surface — not by an operator JWT. All fields shown below are required; `dueAt` and `updatedAt` accept `null`, and `payload` can be an empty object:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/callback" \
-H "X-Callback-Token: ***" \
-H "X-Idempotency-Key: callback-jira-1234" \
-H "Content-Type: application/json" \
-d '{
"callbackType": "status_update",
"externalSystem": "JIRA",
"externalIssueId": "RECON-1234",
"status": "RESOLVED",
"resolutionNotes": "Verified: amount difference is expected bank wire fee",
"assignee": "john.doe@company.com",
"dueAt": null,
"updatedAt": "2026-01-20T14:30:00Z",
"payload": {}
}'
```
The `externalSystem` field identifies the external system that processed the exception. Common values include `"JIRA"`, `"SERVICENOW"`, or `"WEBHOOK"`, but callbacks can report any system identifier. Omitting any of the nine required fields returns a `422`.
#### Response
```json theme={null}
{
"status": "accepted"
}
```
API Reference: [Process callback](/en/reference/matcher/process-exception-callback)
When Matcher processes a callback, it updates the exception status and records the resolution in the audit trail. Use the `X-Idempotency-Key` header to prevent duplicate processing.
### Automatic retry for failed callbacks
If a previous callback for the same idempotency key failed during processing, Matcher automatically attempts to reacquire the idempotency lock and reprocess the callback. This means you don't need to generate a new idempotency key when retrying a failed callback — simply resend the same request and Matcher handles the recovery.
The retry behavior applies only to callbacks that were marked as `failed` internally. Callbacks that completed successfully are still deduplicated as expected.
## Callback credentials
***
Inbound callbacks are authenticated with an opaque bearer token that the external system sends in the `X-Callback-Token` header. These **callback credentials** are minted, listed, rotated, and revoked through a dedicated CRUD surface under `/v1/exceptions/callbacks/credentials`. Each credential is bound to the caller's tenant, and only the token's SHA-256 hash is stored server-side — the raw token is returned **exactly once** at mint/rotate time.
| Action | Method & path | Notes |
| ----------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Mint credential | `POST /v1/exceptions/callbacks/credentials` | Creates a credential and returns the raw token once (`201`). |
| List credentials | `GET /v1/exceptions/callbacks/credentials` | Lists credential metadata for the tenant (never the raw tokens). |
| Rotate credential | `POST /v1/exceptions/callbacks/credentials/{credentialId}/rotate` | Atomically supersedes a live credential with a freshly minted one (same label) and returns the new raw token once; the old one is revoked in the same transaction. |
| Revoke credential | `DELETE /v1/exceptions/callbacks/credentials/{credentialId}` | Terminally revokes a credential (`204`); append-only audited. |
API Reference:
* [Mint callback credential](/en/reference/matcher/mint-callback-credential)
* [List callback credentials](/en/reference/matcher/list-callback-credentials)
* [Rotate callback credential](/en/reference/matcher/rotate-callback-credential)
* [Revoke callback credential](/en/reference/matcher/revoke-callback-credential)
### Mint a credential
The request body is optional; provide `externalSystem` as an operator-legible label for the system this token authenticates.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/callbacks/credentials" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "externalSystem": "stripe" }'
```
The `201` response (`CredentialSecretResponse`) returns:
| Field | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `token` | Raw bearer token, surfaced **once**. Configure it as the `X-Callback-Token` header value in the external system. |
| `credentialId` | Surrogate id of the minted credential (used for rotate/revoke). |
| `createdAt` | Mint time (RFC 3339, UTC). |
| `externalSystem` | The label echoed back for confirmation. |
| `webhookUrlHint` | Informational inbound-callback URL shape to configure externally; the `{exceptionId}` placeholder is filled per callback. |
The raw `token` is shown only in the mint and rotate responses. Store it securely on receipt — it cannot be retrieved again. If it is lost or leaked, rotate or revoke the credential.
### Rotate a credential
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/callbacks/credentials/{credentialId}/rotate" \
-H "Authorization: Bearer $TOKEN"
```
Rotation returns a new `CredentialSecretResponse` (new raw token) and revokes the previous credential atomically, so external callers experience no gap when you swap the token.
### Revoke a credential
```bash cURL theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/exceptions/callbacks/credentials/{credentialId}" \
-H "Authorization: Bearer $TOKEN"
```
Revocation is terminal: the credential can no longer authenticate inbound callbacks.
## Webhook security
***
### Signature verification
When a webhook shared secret is configured, Matcher signs each delivery with an HMAC-SHA256 over the **raw request body** and sends it in the `X-Signature-256` header, formatted as `sha256=`:
```
X-Signature-256: sha256=abc123...
```
Each delivery also carries an `X-Idempotency-Key` header so receivers can deduplicate retries.
**Verification process:**
1. Compute HMAC-SHA256 of the raw request body using the webhook shared secret
2. Prefix the hex digest with `sha256=`
3. Compare (constant-time) with the `X-Signature-256` header
**Example (Node.js):**
```javascript theme={null}
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
```
**Example (Python):**
```python theme={null}
import hmac
import hashlib
def verify_webhook(payload, signature, secret):
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return f"sha256={expected}" == signature
```
### Network posture
Matcher deploys in your own infrastructure, so webhook deliveries originate from your deployment's egress — there is no fixed Lerian IP range to allowlist. Serve webhook endpoints over HTTPS with a valid certificate. As an SSRF guard, Matcher refuses to deliver to private or loopback IP addresses unless the deployment explicitly allows them (development only).
## Retry logic
***
Failed webhook deliveries are retried with exponential backoff.
### Default retry policy
A failed delivery is retried up to **3 times** by default. Delays follow exponential backoff from a **1-second** base, with jitter added to spread retries — so the exact spacing varies from attempt to attempt rather than following a fixed ladder.
### Retry conditions
Retries occur for:
* HTTP 429 responses
* HTTP 5xx responses
* Transport errors (connection failures, timeouts)
No retry for:
* Other HTTP 4xx responses
## Best practices
***
Always verify the HMAC signature before processing webhook payloads. This prevents spoofed requests.
Return a 2xx response within 5 seconds. Process the event asynchronously if needed.
Deliveries may arrive more than once. Deduplicate on the `X-Idempotency-Key` header or the payload's `eventId`.
Set up alerts for webhook failure rates. Investigate persistent failures promptly.
## Next steps
***
Configure how exceptions trigger webhook events.
Set up data sources that can push via webhooks.
# Architecture
Source: https://docs.lerian.studio/en/matcher/matcher-architecture
Explore Matcher's modular monolith built on DDD, hexagonal architecture, and CQRS, with seven bounded contexts that evolve independently.
Matcher is built as a **modular monolith** using Domain-Driven Design (DDD) and hexagonal architecture. CQRS separates commands (writes) from queries (reads).
This keeps operations simple while maintaining clear boundaries. Each module can evolve independently without the complexity of microservices.
## Architecture overview
***
## Bounded contexts
***
Matcher has seven modules. Each owns its data and exposes clean interfaces to the others.
* **Configuration**: What you're reconciling (contexts, sources, field maps, rules)
* **Discovery**: External data source connections, schema detection, and extraction orchestration with the embedded Fetcher engine
* **Ingestion**: Getting data in (parsing, validation, normalization)
* **Matching**: The engine (rule execution, confidence scoring)
* **Exception**: Handling unmatched items (workflow, routing, resolution)
* **Governance**: Audit trails (immutable logs for compliance)
* **Reporting**: Visibility (reports, exports, dashboards)
### Configuration
Defines **what** you're reconciling and **how**.
**Handles:**
* Contexts (what's being reconciled)
* Sources (where data comes from)
* Field maps (translating external fields)
* Rules (how to match)
**Key models:**
* `ReconciliationContext`: The reconciliation scope
* `ReconciliationSource`: Source configuration
* `FieldMap`: Field translation rules
* `MatchRule`: Matching logic
### Discovery
The Discovery bounded context manages external data source connectivity, schema detection, and extraction orchestration with Fetcher's embedded engine. Matcher hosts that engine in-process; Fetcher is not a remote service.
**Responsibilities:**
* Manage external data source connections
* Detect and cache source schemas
* Run in-process extractions and hand results directly to Ingestion
* Track connection and extraction lifecycles
**Key entities:**
* `FetcherConnection`: External source connection managed locally by the embedded engine
* `ExtractionRequest`: Tracks an extraction lifecycle run by the embedded engine
See [Discovery](/en/matcher/integrations/matcher-discovery) for how Discovery connects to external databases with the embedded Fetcher engine.
### Ingestion
The Ingestion bounded context handles data intake and normalization.
**Responsibilities:**
* Parse uploaded files (CSV, JSON, XML)
* Validate incoming data against configured schemas
* Normalize external data into a canonical representation
* Detect and handle duplicate records
* Emit domain events when ingestion completes
**Key entities:**
* `IngestionJob`: Tracks ingestion lifecycle and status
* `Transaction`: Normalized canonical transaction record
**Events published:**
* `ingestion.completed`: Indicates data readiness for matching
### Matching
The Matching bounded context contains the reconciliation engine.
**Responsibilities:**
* Load applicable rules for a reconciliation context
* Execute matching strategies (exact, tolerance, date-based)
* Calculate confidence scores
* Create match groups and allocate transactions
* Identify unmatched transactions
**Key entities:**
* `MatchRun`: Execution of a matching job
* `MatchGroup`: Group of reconciled transactions
* `MatchItem`: Individual transaction allocation
**Events published:**
* `match_group.confirmed`: A match group has been finalized
* `match_group.unmatched`: A previously confirmed match was reverted
* `transaction.pending_review`: A non-automatic candidate needs review
### Exception management
The Exception bounded context manages unresolved transactions.
**Responsibilities:**
* Classify exceptions by severity
* Route exceptions to internal teams or external systems
* Support manual overrides and adjustments
* Track resolution status and SLAs
* Integrate with external workflow tools
**Key entities:**
* `Exception`: An unresolved transaction
* `Resolution`: Outcome of exception handling
* `RoutingRule`: Routing and escalation logic
**Integrations:**
* JIRA for issue tracking
* Webhooks for custom workflows
ServiceNow is an accepted routing target, but its HTTP connector is not implemented.
### Governance
The Governance bounded context preserves reconciliation traceability.
**Responsibilities:**
* Record instrumented auditable mutation workflows in immutable audit logs
* Provide queryable audit history
* Support regulatory and compliance reporting
**Key entities:**
* `AuditLog`: Append-only record of instrumented auditable mutation workflows
Audit logs are append-only by design. Entries cannot be modified or removed to preserve compliance integrity.
### Reporting
The Reporting bounded context provides operational visibility.
**Responsibilities:**
* Generate reconciliation reports
* Expose dashboard metrics
* Export reconciliation data in multiple formats
**Key entities:**
* `Report`: Reconciliation summary
* `Dashboard`: Aggregated operational metrics
* `ExportJob`: Asynchronous export execution
## Data flow
***
Reconciliation follows a deterministic pipeline across bounded contexts:
Reconciliation contexts, sources, field mappings, and rules are defined through the API.
Discovery connects to external sources, detects their schemas, and runs extractions in-process with the embedded Fetcher engine. Extracted results are handed directly to Ingestion.
Uploaded files and data extracted by Discovery are parsed, validated, normalized, and deduplicated. An `ingestion.completed` event is emitted.
Matching rules are applied to eligible transactions, producing match groups with confidence scores on an integer scale of 0 to 100. EXACT and TOLERANCE groups with a confidence of at least 90 out of 100 can auto-confirm; FUZZY and DATE\_LAG groups always require manual review. Unmatched items become exceptions.
Exceptions are classified, routed, and resolved either manually or via external systems. Resolution updates are propagated back to Matcher.
Instrumented auditable mutation workflows across the pipeline are recorded in immutable audit logs.
Users access reports and dashboards showing reconciliation status, match rates, and exception aging.
## Infrastructure components
***
Matcher relies on the following infrastructure services:
| Component | Purpose | Usage |
| ----------------------------- | -------------------------- | ------------------------------------------------------------------------------- |
| **PostgreSQL** | Primary data store | Domain data; configured multi-tenant deployments resolve a pool for each tenant |
| **Valkey (Redis-compatible)** | Cache and coordination | Deduplication, locks, idempotency keys |
| **Streaming backbone** | Business-event publication | Domain events published via lib-streaming |
| **RabbitMQ** | Infrastructure queues | Internal work queues and dead-letter handling |
| **Systemplane** | Runtime configuration | Hot-reload settings without restart via `/system/matcher/:key` admin API |
### Database architecture
* **Tenant-specific pool resolution** in configured multi-tenant deployments for data separation
* **Strong consistency** for matching and exception state
* **Eventual consistency** for reporting views
### Multi-tenancy
Matcher enforces strict tenant isolation:
* With `AUTH_PROVIDER=plugin-auth`, tenant identity is extracted from `tenant_id` or `tenantId` JWT claims
* `workos`, single-tenant, and authentication-disabled deployments use the configured default tenant
* Tenant identifiers are never accepted via request parameters
* Database access is scoped through the active tenant's connection pool
* All queries are automatically constrained to the active tenant
This model prevents cross-tenant data access and supports regulatory and audit requirements.
## Design patterns
***
### Hexagonal architecture
Each bounded context follows the ports-and-adapters pattern:
```
context/
├── adapters/
│ ├── http/
│ ├── postgres/
│ └── redis/
├── ports/
├── services/
│ ├── command/
│ ├── query/
│ └── worker/
└── domain/
├── entities/
└── errors/
```
### Cqrs-light
Write and read paths are separated at the service level:
* `*_commands.go` for state mutations
* `*_queries.go` for read operations
This improves code organization and allows independent optimization of query paths.
### Outbox pattern
Matcher uses per-event delivery policies. Outbox-backed events persist an outbox record and are dispatched asynchronously; other events can use direct delivery with an outbox fallback when the circuit is open.
## Next steps
***
Explore the architecture through a guided example.
Review authentication, authorization, and tenant isolation mechanisms.
# Matcher concepts
Source: https://docs.lerian.studio/en/matcher/matcher-concepts
Learn Matcher's five core concepts — contexts, sources, field maps, match rules, and matches — that shape every reconciliation you build.
The five core concepts in Matcher: **contexts**, **sources**, **field maps**, **rules**, and **matches**. Understand these and you'll understand how the entire system works.
## Context
***
A **context** defines what you reconcile. It is the configuration container for sources and rules. Matcher creates a new context in `DRAFT`, and its inline sources and rules are optional.
A context answers: *what am I matching against what?*
You can start with an empty draft. To activate it, configure at least one `LEFT` source and one `RIGHT` source, map every source (or declare valid `camt053` options, which self-map), and add a match rule. If fee normalization is enabled, add a fee rule too.
### Context types
| Type | Description | Example |
| ------- | --------------------------- | -------------------------------------- |
| **1:1** | One-to-one reconciliation | Bank statement vs ERP records |
| **1:N** | One-to-many reconciliation | One payment covering multiple invoices |
| **N:M** | Many-to-many reconciliation | Netting or aggregation scenarios |
### Example
A context named **"Chase Bank vs ERP System"** would:
* Define Chase Bank as one reconciliation source
* Define your ERP system as another source
* Specify the rules used to reconcile transactions between them
## Source
***
A **source** is where transactions come from. A draft context can start without sources; an activatable context needs at least one source on each matching side.
### Source types
* **LEDGER**: Ledger source category
* **BANK**: Bank source category
* **GATEWAY**: Payment-gateway source category
* **CUSTOM**: Custom source category
* **FETCHER**: A Fetcher source category
### Source setup
Each source requires:
* **Name**: Label it (e.g., "Chase Checking")
* **Type**: Category (`LEDGER`, `BANK`, `GATEWAY`, `CUSTOM`, or `FETCHER`)
* **Side**: Which matching side it feeds (`LEFT` or `RIGHT`)
**Config** is optional. If you omit it, Matcher stores an empty config and uses parser defaults for absent policy keys.
Field maps translate each source's fields into Matcher's standard schema.
## Field map
***
A **field map** translates external field names into Matcher's standard schema. Every system calls things differently—field maps normalize that.
### Standard fields
| Field | Required | Type | Description |
| -------------- | -------- | -------- | ----------------------------------------- |
| `external_id` | Yes | String | Source-system transaction identifier |
| `amount` | Yes | Decimal | Transaction amount (positive or negative) |
| `currency` | Yes | String | ISO 4217 currency code |
| `date` | Yes | DateTime | Transaction date |
| `description` | No | String | External reference or description |
| `fee_amount` | No | Decimal | Optional fee amount column |
| `fee_currency` | No | String | Optional fee currency column |
The canonical vocabulary is closed — a field map that declares any other key is rejected.
When a source config declares `camt053` options, Matcher uses its built-in ISO 20022 mapping and ignores a field map; activation treats that source as mapped.
### Example mapping
A bank statement exposing `TXN_ID`, `VALUE`, `CCY`, and `POST_DATE` would be mapped as:
```json theme={null}
{
"external_id": "TXN_ID",
"amount": "VALUE",
"currency": "CCY",
"date": "POST_DATE"
}
```
## Match rule
***
A **match rule** tells Matcher how to compare transactions. Rules run in ascending priority; a transaction claimed by an earlier rule is unavailable to later rules, which still evaluate the remaining transactions.
### Rule types
* **EXACT**: Compares configured fields exactly. Amount, currency, date (by day), and reference are enabled by default.
* **TOLERANCE**: Matches amounts inside the configured absolute and/or percentage tolerance. Omitted amount tolerances and `dateWindowDays` default to `0`, so no drift or date window is allowed until you configure one.
* **DATE\_LAG**: Matches within a configured day-difference band. `minDays` and `maxDays` both default to `0` (same day), not ±3. Like FUZZY, DATE\_LAG matches never auto-confirm — they always go to manual review.
* **FUZZY**: Grades normalized transaction references. It uses `Reference`, populated from the transaction's `ExternalID`; a field-map `description` is not a FUZZY input. FUZZY only proposes—it never auto-confirms, so a human reviews every fuzzy link.
### Priority order
Lower numbers run first. A rule claims its matching transactions; later rules continue with the remaining transactions.
| Priority | Rule | Description |
| -------- | ------------------ | -------------------------------------- |
| 1 | Exact match | Amount, date, and reference must match |
| 2 | Same-day tolerance | Same date, amount within 0.5% |
| 3 | Week tolerance | Within 7 days, amount within 1% |
These priorities and values are illustrative rules, not engine defaults. Configure the values for your reconciliation policy.
### Rule parameters
| Rule type | Parameters |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EXACT | `matchAmount`, `matchCurrency`, `matchDate`, `matchReference`: which fields must match exactly |
| TOLERANCE | Top-level non-negative `percentTolerance` and/or `absTolerance` values (numbers or decimal strings); optionally set `dateWindowDays` (`0` by default; maximum `3650`). Do not wrap these values in a `tolerance` object. |
| DATE\_LAG | `minDays`, `maxDays`: allowed day-difference band. Both default to `0`, must be from `0` to `3650`, and `maxDays` must be at least `minDays`; `inclusive` defaults to `true` and controls the upper bound. |
| FUZZY | `minSimilarity`: normalized-reference threshold from `0` to `1` (defaults to `0.80`), plus optional amount, currency, and date axes. |
## Match
***
A **match** is when transactions from different sources are reconciled together. It's the end goal.
### Match status
| Status | Description |
| ----------- | -------------------------------------------------------------------------------- |
| `PROPOSED` | Matcher found it, waiting for confirmation |
| `CONFIRMED` | Auto-approved or manually approved |
| `REJECTED` | Manually rejected |
| `REVOKED` | A previously confirmed match was unmatched, returning its transactions to review |
### Match patterns
#### 1:1 match
One transaction from each source is reconciled.
```
Bank: $100.00 on Jan 15 → ERP: $100.00 on Jan 15
```
#### 1:N match
One transaction is reconciled against multiple transactions.
```
Bank: $300.00 → ERP: $100.00 + $100.00 + $100.00
```
#### N:1 match
Multiple transactions are reconciled against a single transaction.
```
Bank: $50.00 + $50.00 + $50.00 → ERP: $150.00
```
#### N:M match
Multiple transactions on each side are reconciled together. N:M evaluation only executes `EXACT` and `TOLERANCE` rules; it considers up to four transactions per side in a group and caps each identity bucket at 40 candidates.
```
Bank: $100.00 + $200.00 → ERP: $150.00 + $150.00
```
### Match items
Each match group contains **match items**, which record transaction participation and allocation.
This enables partial reconciliation in split and aggregation scenarios.
## Exception
***
An **exception** records a transaction that needs review, including unmatched transactions and matched transactions with residual conditions such as FX-rate variance.
### Exception status
| Status | Description |
| -------------------- | ------------------------------------------------ |
| `OPEN` | Waiting for assignment |
| `ASSIGNED` | Someone's investigating |
| `PENDING_RESOLUTION` | A resolution is in progress, awaiting completion |
| `RESOLVED` | Handled |
### Severity
Matcher auto-classifies exceptions so you know what to prioritize.
| Severity | Default criteria |
| ------------ | --------------------------------------------------------------------------------------- |
| **Critical** | Absolute base amount ≥ 100,000, age ≥ 120 hours, or a configured regulatory source type |
| **High** | Absolute base amount ≥ 10,000 or age ≥ 72 hours |
| **Medium** | Absolute base amount ≥ 1,000 or age ≥ 24 hours |
| **Low** | All other cases |
The classifier evaluates the criteria from top to bottom. When an exception meets the criteria of more than one severity, the highest matching severity applies.
### Resolution workflows
* **Resolve**: Record a resolution label and optional reason to close an exception.
* **Force match**: Resolve an exception by forcing a match with an override reason after manual review.
* **Adjust entry**: Resolve an exception by creating an adjustment entry with a reason, notes, positive amount, currency, and effective time.
## Confidence score
***
A **confidence score** indicates the reliability of an automated match on a 0–100 scale.
Higher scores represent stronger alignment between transactions.
### Score calculation
| Component | Weight | Description |
| -------------- | ------ | ----------------------------------------------------------- |
| Amount match | 40% | Degree of amount alignment |
| Currency match | 30% | Currency consistency |
| Date tolerance | 20% | Proximity of transaction dates |
| Reference | 10% | Normalized reference alignment (graded 0–1 for FUZZY rules) |
### Confidence tiers
| Tier | Score range | System behavior |
| ----------------- | ----------- | -------------------------------------------------------------------------------------------------- |
| **Auto-approved** | ≥ 90 | Confirmed automatically (EXACT and TOLERANCE rules only — FUZZY and DATE\_LAG always go to review) |
| **Needs review** | 60–89 | Flagged for manual review |
| **No match** | \< 60 | Does not create a match proposal |
Confidence weights and tier thresholds are fixed by the engine and are not configurable.
## Audit log
***
An **audit log** is an immutable, append-only record created by an instrumented workflow.
It provides traceability for the actions Matcher records.
### Logged events
Only workflows instrumented to emit an audit event create entries. When audit publishing is configured, verified producers include:
* Context, source, field-map, and rule mutations
* Exception workflows, including force match and adjust entry
### Audit entry contents
| Field | Description |
| ------------------------- | ----------------------------------------------------------------------------------------- |
| `createdAt` | Record creation timestamp (UTC), which can differ from the audited action time |
| `actorId` | Actor identifier, when supplied |
| `action` | Action performed |
| `entityType` | Affected entity type |
| `entityId` | Identifier of the affected entity |
| `changes` | Structured JSON event data; emitted events include `occurred_at` and any supplied changes |
| `tenantSeq` | Per-tenant sequence number |
| `prevHash` / `recordHash` | Hash chain linking each entry to the previous one, making tampering detectable |
Audit logs are append-only. Entries cannot be modified or removed.
## Next steps
***
See how these concepts are implemented across bounded contexts.
Apply these concepts in a guided, hands-on flow.
# Rule suggestions
Source: https://docs.lerian.studio/en/matcher/matching/matcher-rule-suggestions
Produce AI-suggested match rules, review them in a human-in-the-loop queue, and approve or reject each candidate before any rule is created.
Matcher can propose config-only match rules from a context's history using AI — but **AI output is never authoritative**. Producing a suggestion creates no rule; a rule is created only when a human approves a suggestion. This guide covers the human-in-the-loop (HITL) rule-suggestion queue.
The lane is gated by a global advisor kill-switch **and** a per-tenant opt-in (fail-closed: a non-opted-in tenant gets `403`). The egress payload is **aggregates only** — no raw transaction, money, or PII leaves your deployment.
## Produce suggestions
***
Build aggregate, privacy-safe history features for a context, ask the AI advisor for candidate rules, and enqueue each surviving candidate in the review queue.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions" \
-H "Authorization: Bearer $TOKEN"
```
The response returns the newly created `PENDING_REVIEW` reviews — producing them creates no rule:
```json theme={null}
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"contextId": "550e8400-e29b-41d4-a716-446655440000",
"candidate": {
"type": "TOLERANCE",
"priority": 10,
"config": { "tolerance": { "percentTolerance": "0.01" } },
"rationale": "near-miss amounts cluster under 1%",
"expectedImprovement": "+8% auto-match",
"confidence": 0.82
},
"status": "PENDING_REVIEW",
"version": 1,
"createdAt": "2026-06-16T10:30:00Z",
"updatedAt": "2026-06-16T10:30:00Z"
}
],
"count": 1
}
```
Candidate types are drawn from the closed vocabulary: `EXACT` (strict equality), `TOLERANCE` (within an amount band), or `DATE_LAG` (allowing a settlement-date offset). The candidate is **config only** — it never carries a monetary value or a transaction.
## List suggestions
***
Cursor-paginated list of rule suggestions for a context, optionally filtered by status.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions?status=PENDING_REVIEW&limit=20" \
-H "Authorization: Bearer $TOKEN"
```
Query parameters: `status` (`PENDING_REVIEW`, `APPROVED`, `REJECTED`), `limit` (1–200), and `cursor`. Reading the queue egresses nothing.
## Approve a suggestion
***
Approving a `PENDING_REVIEW` suggestion creates the match rule through the deterministic configuration write path. This is the **only** path from an AI suggestion to an active match rule, and it runs only on explicit human approval.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions/{suggestionId}/approve" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"reviewId": "550e8400-e29b-41d4-a716-446655440000",
"createdRuleId": "550e8400-e29b-41d4-a716-446655440000"
}
```
## Reject a suggestion
***
Rejecting a `PENDING_REVIEW` suggestion discards it — nothing is created. The request body is required, but its `reason` field is optional (send `{}` to reject without a reason).
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/rule-suggestions/{suggestionId}/reject" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "too aggressive" }'
```
The approving/rejecting principal is recorded for audit.
## Lifecycle after approval
***
Once a suggestion is `APPROVED`, its `createdRuleId` links to a real match rule that participates in match runs exactly like a hand-authored rule. A review is a one-way state machine: a `PENDING_REVIEW` suggestion transitions to `APPROVED` or `REJECTED` once and cannot be re-decided. Attempting to re-decide, or to approve an already-linked review, returns `409`. An approved candidate that fails validation returns `422`.
Preview how a candidate rule would behave before you approve it with the read-only simulate endpoint — see [Simulation](/en/matcher/matching/matcher-simulate).
## Response codes
***
| Status | Meaning |
| ------ | ---------------------------------------------------------------- |
| `200` | Suggestions produced, listed, approved, or rejected |
| `400` | Invalid status filter or suggestion id |
| `403` | Tenant not opted into rule suggestions |
| `404` | Rule suggestion not found |
| `409` | Invalid state transition / already linked |
| `422` | Approved suggestion failed validation |
| `503` | Rule suggestion or advisor not available (author rules manually) |
# Simulation
Source: https://docs.lerian.studio/en/matcher/matching/matcher-simulate
Dry-run a match rule and preview fee-schedule calculations in Matcher — both endpoints are read-only, so you can validate config before committing anything.
Matcher exposes two read-only simulation endpoints so you can answer "what would happen?" before you commit configuration: **match simulation** (will this rule actually match?) and **fee simulation** (what fees would this schedule charge?). Neither persists anything.
## Match simulation
***
Preview how a single rule — an existing configured rule (`ruleId`) **or** an inline candidate rule (`rule`) — would match a context's unmatched transactions, without committing anything.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/simulate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contextId": "550e8400-e29b-41d4-a716-446655440000",
"rule": {
"type": "TOLERANCE",
"config": { "tolerance": { "percentTolerance": "0.01" } }
},
"sampleLimit": 25
}'
```
Supply **exactly one** of:
* `ruleId` — preview an existing configured rule of the context.
* `rule` — preview an un-persisted candidate definition (`type` is one of `EXACT`, `TOLERANCE`, `DATE_LAG`, `FUZZY`, plus its `config`).
Supplying both, or neither, returns `400`. `sampleLimit` (1–200, default 25) caps the returned would-match pairs.
The response reports how many 1:1 groups the rule would form, a bounded sample of would-match pairs (each with a confidence score and per-component rationale), and the per-side unmatched counts:
```json theme={null}
{
"ruleType": "TOLERANCE",
"matchedGroups": 12,
"unmatchedLeft": 3,
"unmatchedRight": 5,
"sampleTruncated": false,
"sample": [
{
"left": { "id": "...", "amount": "100.00", "currency": "BRL", "date": "2025-06-01T00:00:00Z" },
"right": { "id": "...", "amount": "100.00", "currency": "BRL", "date": "2025-06-01T00:00:00Z" },
"score": 90,
"why": { "amountMatch": true, "currencyMatch": true, "dateMatch": true, "referenceScore": 0 },
"amountDelta": "0.00",
"dateDeltaDays": 0
}
]
}
```
Scope: the simulation scores by the deterministic rule engine over the **raw** transaction amounts. It does **not** apply run-time fee normalization or the FX-variance band, and it previews only 1:1 pairwise grouping (no 1:N/N:M allocation). A pair that only matches after fee normalization, inside the FX band, or via allocation is not counted here.
## Fee simulation
***
Calculate fees for a given gross amount using a specific fee schedule. Use it to validate a schedule's rules before you attach it to a context.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/fee-schedules/{scheduleId}/simulate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "grossAmount": "100.00", "currency": "USD" }'
```
The response returns the net amount, the total fee, and a per-item breakdown:
```json theme={null}
{
"grossAmount": "100.00",
"netAmount": "97.70",
"totalFee": "2.30",
"currency": "USD",
"items": [
{ "name": "interchange", "fee": "1.50", "baseUsed": "100.00" }
]
}
```
Fee simulation has no transaction metadata, so expression-fee items that require a transaction identifier surface their missing-identifier error as a `4xx`. A missing schedule returns `404`.
## When to use each
***
| Use | When |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Match simulation** (`/matching/simulate`) | You are authoring or reviewing a match rule and want to see how many transactions it would group, and which pairs, before committing it. |
| **Fee simulation** (`/fee-schedules/{scheduleId}/simulate`) | You are configuring a fee schedule and want to verify the net/fee breakdown it would produce for a representative gross amount. |
Both are strictly read-only: they never persist a run, group, item, rule, or transaction, and the tenant is always taken from the JWT.
## Response codes
***
| Status | Meaning |
| ------ | ------------------------------------------------------------------------- |
| `200` | Simulation returned |
| `400` | Invalid input (ambiguous/missing rule, invalid ids, invalid gross amount) |
| `404` | Context, rule, or fee schedule not found |
| `503` | Match simulation not available |
# Confidence scoring
Source: https://docs.lerian.studio/en/matcher/reference/matcher-confidence-scoring
See how Matcher computes a 0–100 confidence score from amount, currency, date, and reference checks, and how EXACT, TOLERANCE, DATE_LAG, and FUZZY rules score.
Confidence scores indicate the reliability of an automated match on a scale of 0-100. Higher scores mean greater certainty that two transactions represent the same financial event.
## Overview
***
When Matcher identifies a potential match, it assigns a confidence score based on multiple factors.
This score defines how the match is handled:
* High scores (90+) are auto-approved
* Mid-range (60 – 89) scores require review
* Low scores (\<60) are treated as exceptions
## Score components
***
Matcher uses a **binary weighted scoring system** with four components. Each component evaluates to either a full match (1.0) or no match (0.0) — there are no partial scores within a component.
| Component | Weight | Points (match / no match) |
| --------------- | ------ | ------------------------- |
| Amount match | 40% | 40 / 0 |
| Currency match | 30% | 30 / 0 |
| Date proximity | 20% | 20 / 0 |
| Reference match | 10% | 10 / 0 |
### Amount match (40%)
The amount component has the highest weight because amount discrepancies often indicate different transactions.
| Condition | Score |
| ------------------------------------- | --------- |
| Amounts match (within rule tolerance) | 40 points |
| Amounts do not match | 0 points |
Amount matching depends on the active rule type. An EXACT rule requires identical amounts; a TOLERANCE rule allows variance within the configured `percentTolerance` and `absTolerance`.
### Currency match (30%)
Currency verification is binary — currencies either match or they don't.
| Condition | Score |
| ------------------ | --------- |
| Same currency | 30 points |
| Different currency | 0 points |
### Date proximity (20%)
Date scoring checks whether the transaction dates fall within the configured window.
| Condition | Score |
| ---------------------------- | --------- |
| Dates within allowed window | 20 points |
| Dates outside allowed window | 0 points |
The allowed window depends on the rule: an EXACT rule requires the same date (respecting `datePrecision`), while a DATE\_LAG rule accepts a day difference inside its `[minDays, maxDays]` band (the `inclusive` flag controls whether `maxDays` itself counts).
### Reference match (10%)
For EXACT and TOLERANCE rules, reference comparison is binary.
| Condition | Score |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------- |
| References match (exact or case-insensitive) | 10 points |
| Both references absent | 10 points (unless the rule sets `referenceMustSet`, which scores any absent reference as 0) |
| References do not match, or only one is absent | 0 points |
**FUZZY rules score the reference on a continuous scale.** For a FUZZY rule, the 10% reference slot carries a graded `ReferenceScore` between `0.0` and `1.0` (a similarity measure) rather than a strict `0`/`1`. A near-identical reference contributes close to the full 10 points while one that barely clears the fuzzy gate contributes proportionally less. As a result, FUZZY matches can produce non-multiple-of-10 scores such as 97 or 99. See [Possible scores](#possible-scores) below.
**DATE\_LAG rules do not score references.** For DATE\_LAG rules the `ReferenceScore` is always `0.0`, so the 10% reference component contributes `0` points regardless of the reference values. The maximum achievable DATE\_LAG score is therefore 90 (40 + 30 + 20 + 0), which by design keeps date-lag matches in the manual-review path.
## Calculation formula
***
The confidence score formula:
```
confidence = round(
(amountMatch × 0.40 +
currencyMatch × 0.30 +
dateMatch × 0.20 +
referenceScore × 0.10) × 100
)
```
Where each factor is either `1.0` (match) or `0.0` (no match).
The weights are hardcoded constants and are not configurable per context.
### Possible scores
For **EXACT**, **TOLERANCE**, and **DATE\_LAG** rules, every component is binary, so the confidence score is always one of these values:
**0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100**
For these rule types, intermediate values (e.g. 87, 72, 55) never occur. (For DATE\_LAG the reference component is always `0`, so its scores never include the final 10 points.)
For **FUZZY** rules this does not hold. Because the reference component is a continuous `ReferenceScore` (0.0–1.0), FUZZY matches can produce intermediate scores such as **97** or **99**. Note that regardless of the resulting score, a FUZZY match is **never auto-confirmed** — see [FUZZY matches never auto-confirm](#fuzzy-matches-never-auto-confirm).
## Calculation examples
***
### Exact match (score: 100)
Two transactions with identical values on the same day:
| Component | Comparison | Points |
| --------- | -------------------------- | ------ |
| Amount | $1,000.00 vs $1,000.00 ✓ | 40 |
| Currency | USD vs USD ✓ | 30 |
| Date | 2024-01-15 vs 2024-01-15 ✓ | 20 |
| Reference | PAY-001 vs PAY-001 ✓ | 10 |
**Final Score: 100** → Auto-confirmed
### High confidence match (score: 90)
All fields match except reference:
| Component | Comparison | Points |
| --------- | -------------------------- | ------ |
| Amount | $1,000.00 vs $1,000.00 ✓ | 40 |
| Currency | USD vs USD ✓ | 30 |
| Date | 2024-01-15 vs 2024-01-15 ✓ | 20 |
| Reference | PAY-001 vs — ✗ | 0 |
**Final Score: 90** → Auto-confirmed
### Medium confidence (score: 70)
Amount and currency match, but date and reference do not:
| Component | Comparison | Points |
| --------- | -------------------------- | ------ |
| Amount | $1,000.00 vs $1,000.00 ✓ | 40 |
| Currency | USD vs USD ✓ | 30 |
| Date | 2024-01-15 vs 2024-01-25 ✗ | 0 |
| Reference | PAY-001 vs REC-999 ✗ | 0 |
**Final Score: 70** → Needs review
### Low confidence (score: 40)
Only amount matches:
| Component | Comparison | Points |
| --------- | -------------------------- | ------ |
| Amount | $1,000.00 vs $1,000.00 ✓ | 40 |
| Currency | USD vs EUR ✗ | 0 |
| Date | 2024-01-15 vs 2024-01-25 ✗ | 0 |
| Reference | PAY-001 vs REC-999 ✗ | 0 |
**Final Score: 40** → Exception (below 60)
## Confidence tiers
***
Matcher categorizes matches into tiers based on score:
| Tier (score) | System behavior | Typical volume (illustrative) |
| ------------------------ | ------------------------ | ----------------------------- |
| **Auto-Approved** (≥ 90) | Automatically confirmed | 70-80% |
| **Needs Review** (60-89) | Queued for manual review | 15-25% |
| **Exception** (\< 60) | Treated as unmatched | 5-10% |
### How confidence tiers are applied
When Matcher proposes a match, it evaluates the confidence score and applies the following steps:
1. If the score is **90 or higher**, the match is automatically confirmed for EXACT and TOLERANCE rules; FUZZY and DATE\_LAG matches always require manual review.
2. If the score is **between 60 and 89**, the match is queued for manual review.
3. If the score is **below 60**, no match is created and the transaction becomes an exception.
4. Reviewed matches can be either confirmed or rejected, updating their final status.
This ensures high-confidence matches flow automatically while keeping human control where it matters.
### FUZZY matches never auto-confirm
The auto-confirm behavior above applies to EXACT and TOLERANCE rules; FUZZY and DATE\_LAG matches always require manual review. Matches produced by **FUZZY** rules are **never auto-confirmed**, regardless of their confidence score — even a FUZZY match scoring 90 or above is always queued for manual review.
This is by design: a fuzzy reference only contributes the 10% reference slot, so the financial fields (amount + currency + date) alone can already reach the 90 threshold. Capping FUZZY below auto-confirm guarantees that a human reviews the fuzzed reference before the match is committed — "fuzzy proposes, never commits."
## Confidence thresholds
***
Matcher uses fixed thresholds to determine how matches are handled:
| Threshold | Score | Behavior |
| ------------ | ----- | -------------------------------------------------- |
| Auto-confirm | >= 90 | Match is automatically confirmed |
| Match | >= 60 | Match is proposed for manual review |
| Exception | \< 60 | No match created; transaction becomes an exception |
These thresholds are not configurable per context.
## Weights
***
The component weights (40/30/20/10) are hardcoded constants. They cannot be adjusted per context or per rule.
| Component | Weight | Rationale |
| --------- | ------ | --------------------------------------------------------------- |
| Amount | 40% | Amount is the strongest indicator of a valid match |
| Currency | 30% | Currency mismatch usually means different transactions |
| Date | 20% | Date proximity adds confidence but allows for settlement delays |
| Reference | 10% | References are helpful but often missing or inconsistent |
## Best practices
***
Track the percentage of transactions in each tier. Unusual shifts may indicate data quality issues or rule misconfiguration.
Since confidence depends on which rules match, always dry-run rule changes before committing. This prevents unexpected increases in manual review volume.
Periodically review matches just above the exception threshold. These often reveal opportunities for rule improvements.
For EXACT, TOLERANCE, and DATE\_LAG rules, scoring is binary, so a score of 70 means exactly "amount + currency matched, date + reference did not." Use this to diagnose matching issues. FUZZY rules are the exception: their graded reference component can yield intermediate scores (e.g. 97), so a FUZZY score should be read as "financial fields matched plus a partial reference similarity."
## Next steps
***
Configure rules that influence scoring.
How FX affects confidence scoring.
# Exports & disputes
Source: https://docs.lerian.studio/en/matcher/reference/matcher-export-disputes
Run asynchronous export jobs to generate downloadable reconciliation reports, and open, evidence, and resolve disputes against exceptions.
This guide covers two operator workflows that both end in a downloadable or resolved artifact: **export jobs** (queue a report, poll until it succeeds, download the file) and **disputes** (open a dispute against an exception, attach evidence, then close it won or lost). Both are tenant-scoped from the JWT.
## Export jobs
***
Exports are asynchronous. You create a job scoped to a context, poll its status by ID, and download the file once it reaches `SUCCEEDED`. Statuses are `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `EXPIRED`, and `CANCELED`.
### Create an export job
`POST` to the context's export-jobs collection. Responds `202 Accepted` with the job ID and a poll URL.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/export-jobs" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reportType": "MATCHED",
"format": "CSV",
"dateFrom": "2025-01-01",
"dateTo": "2025-01-31",
"sourceId": "550e8400-e29b-41d4-a716-446655440000"
}'
```
```json theme={null}
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "QUEUED",
"statusUrl": "/v1/export-jobs/550e8400-e29b-41d4-a716-446655440001"
}
```
* `reportType` — one of `MATCHED`, `UNMATCHED`, `VARIANCE`, `EXCEPTIONS` (aliases `MATCHES` and `UNMATCHED_TRANSACTIONS` are normalized).
* `format` — `CSV`, `JSON`, or `XML` (normalized to uppercase).
* `dateFrom` / `dateTo` — optional `YYYY-MM-DD`; `dateFrom` defaults to 30 days before `dateTo`, and `dateTo` defaults to tomorrow (UTC).
* `sourceId` — optional source filter.
`SUMMARY` and `PDF` are **not** supported for async export jobs and are rejected with `400`. The date window is also capped at a maximum span (an over-range request is rejected rather than silently clamped).
### Poll job status
Read the top-level job route (the `statusUrl` from creation).
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/export-jobs/{jobId}" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reportType": "MATCHED",
"format": "CSV",
"status": "SUCCEEDED",
"recordsWritten": 4250,
"bytesWritten": 524288,
"fileName": "matched_report_2025-01-31.csv",
"createdAt": "2025-01-15T10:30:00Z",
"startedAt": "2025-01-15T10:30:05Z",
"finishedAt": "2025-01-15T10:35:00Z",
"expiresAt": "2025-01-16T10:30:00Z",
"downloadUrl": "https://storage.example.com/exports/matched_report.csv?token=abc"
}
```
`error` is present only when `status` is `FAILED`; `downloadUrl` appears only once the job has `SUCCEEDED` and the file is still available. You can list a context's jobs with `GET /v1/contexts/{contextId}/export-jobs`, list all jobs with `GET /v1/export-jobs`, and cancel a queued/running job with `POST /v1/export-jobs/{jobId}/cancel`.
### Download the file
Returns a presigned URL, the original file name, a SHA-256 checksum, and the URL's remaining lifetime in seconds.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/export-jobs/{jobId}/download" \
-H "Authorization: Bearer $TOKEN"
```
```json theme={null}
{
"downloadUrl": "https://storage.example.com/exports/report.csv?token=abc",
"fileName": "matched_report.csv",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"expiresIn": 3600
}
```
Export files are purged after `expiresAt` (default 7 days). Once a job is `EXPIRED` the file is no longer downloadable — re-run the export to regenerate it.
## Disputes
***
A dispute is opened against a specific **exception** when a reconciliation discrepancy needs to be contested. Its lifecycle starts at `DRAFT` → `OPEN`. From `OPEN` a dispute can move to `PENDING_EVIDENCE` (and back to `OPEN`) or close directly as `WON` / `LOST`. Only `WON` is terminal — a `LOST` dispute can be reopened to `OPEN`.
### Open a dispute
`POST` to the exception's disputes collection.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/disputes" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"category": "BANK_FEE_ERROR",
"description": "Transaction amount differs from invoice"
}'
```
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"exceptionId": "550e8400-e29b-41d4-a716-446655440001",
"category": "BANK_FEE_ERROR",
"state": "OPEN",
"description": "Transaction amount differs from invoice",
"openedBy": "user@example.com",
"evidence": [],
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
```
`category` is one of `BANK_FEE_ERROR`, `UNRECOGNIZED_CHARGE`, `DUPLICATE_TRANSACTION`, or `OTHER`. The opening principal is recorded in `openedBy`.
### Submit evidence by URL
Attach a reference to an already-hosted evidence file plus a describing comment.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/evidence" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"comment": "Attached bank statement showing correct amount",
"fileUrl": "https://storage.example.com/evidence/doc123.pdf"
}'
```
### Upload an evidence file
Stream the raw file bytes directly to tenant-scoped object storage — the comment travels as a query parameter and the file as the request body. Responds `201 Created` with the updated dispute. Allowed content types are `application/pdf`, `image/png`, `image/jpeg`, and `text/csv`; the body is capped at 10 MiB.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/evidence/upload?comment=Bank%20statement%20showing%20correct%20amount" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/pdf" \
--data-binary @statement.pdf
```
Each stored evidence item is returned on the dispute's `evidence` array:
```json theme={null}
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"disputeId": "550e8400-e29b-41d4-a716-446655440001",
"comment": "Bank statement showing correct amount",
"submittedBy": "user@example.com",
"fileUrl": "https://storage.example.com/evidence/doc123.pdf",
"submittedAt": "2025-01-15T10:30:00Z"
}
```
The upload endpoint fails closed with `503` when object storage is not configured, rejects oversize bodies with `413`, and rejects content types outside the allowlist with `415`. The tenant and dispute are always resolved from the JWT and path — never from the body.
### Close a dispute
Record the outcome. `won` sets the state to `WON` (terminal) or `LOST` (reopenable), with a required `resolution` note.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/disputes/{disputeId}/close" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"won": true,
"resolution": "Counterparty acknowledged the error and issued correction"
}'
```
You can list disputes with `GET /v1/disputes` (filter by `state`, `category`, date range; sort and cursor-paginate) and fetch one with `GET /v1/disputes/{disputeId}`.
## Response codes
***
| Status | Meaning |
| ------ | -------------------------------------------------------------------------------- |
| `200` | Export/dispute data returned |
| `201` | Evidence file uploaded |
| `202` | Export job accepted |
| `400` | Invalid input (unsupported report type/format, bad date range, invalid category) |
| `404` | Context, export job, exception, or dispute not found |
| `409` | Invalid dispute state transition |
| `413` | Evidence file exceeds the 10 MiB cap |
| `415` | Evidence content type not in the allowlist |
| `422` | Malformed field |
| `503` | Export or evidence storage not configured |
# Multi-currency matching
Source: https://docs.lerian.studio/en/matcher/reference/matcher-multi-currency
Reconcile transactions across currencies in Matcher by converting to a base amount using per-transaction FX hints, then apply your existing match rules.
Matcher lets you reconcile transactions in different currencies by converting amounts to a common base currency before comparison. This enables matching across international transactions, treasury operations, and multi-entity reconciliations.
## Overview
***
Multi-currency matching converts both transaction amounts to a base currency using the appropriate FX rate, then applies standard matching rules. If the converted amounts fall within tolerance, Matcher creates a match. Otherwise, it creates an exception for review.
## How it works
***
Multi-currency support is built into the existing context types (`1:1`, `1:N`, `N:M`) and match rules — there is no separate "multi-currency" context type.
When transactions have different currencies, Matcher uses the `amountBase` and `currencyBase` fields on each transaction to compare converted amounts. Today, these base fields are populated **at match time**: Matcher derives them from per-transaction FX hints carried in the transaction's own metadata (see [FX from transaction metadata](#fx-from-transaction-metadata) below).
You cannot supply a base amount directly at file upload — the field-map vocabulary has no base-amount columns. If a transaction already carries a base amount, Matcher honors it and never overwrites it, but the supported way to get base amounts onto your transactions is the FX metadata path.
There is no external FX provider or rate-lookup service: the rate always comes from the transaction row itself.
### Key components
| Component | Where it lives | Purpose |
| ------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------- |
| `amountBase` / `currencyBase` | Transaction fields | Base-currency amounts used for comparison, derived at match time from FX metadata |
| `matchBaseAmount` / `matchBaseCurrency` | Rule config | Tell a rule to compare base amounts instead of originals |
| `fx_rate`, `fx_base_currency`, `fx_notional_expr` | Transaction metadata | Per-transaction FX hints used to derive the base amount at match time |
## Configuring rules for multi-currency
***
Enable multi-currency comparison by setting `matchBaseAmount` and `matchBaseCurrency` to `true` in the rule config.
### Exact rule with base amount matching
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "EXACT",
"priority": 1,
"config": {
"matchBaseAmount": true,
"matchBaseCurrency": true,
"matchDate": true,
"matchReference": false,
"matchScore": 100,
"matchBaseScore": 90
}
}'
```
When `matchBaseAmount` is `true`, the rule compares `amountBase` fields instead of `amount`. When `matchBaseCurrency` is `true`, it compares `currencyBase` instead of `currency`.
### Tolerance rule with base amount matching
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "TOLERANCE",
"priority": 2,
"config": {
"matchBaseAmount": true,
"matchBaseCurrency": true,
"percentTolerance": 0.02,
"absTolerance": 10.0,
"matchScore": 85,
"matchBaseScore": 80
}
}'
```
### Confidence scoring
The `matchScore` and `matchBaseScore` fields are **accepted and validated** in the rule config, but they **do not influence the calculated confidence score**. The scoring engine always uses the fixed internal component weights (`DefaultConfidenceWeights`: amount 40, currency 30, date 20, reference 10) to produce a 0–100 score. Values like `matchScore: 100` or `matchBaseScore: 90` are **not** applied directly as the match output.
These fields are currently **reserved for future use** (retained for parity across rule configs and for metrics); setting them has no effect on how a match is scored or auto-confirmed today.
Do not rely on `matchScore` / `matchBaseScore` to control confidence. Whether a rule matches original or base amounts, the confidence score is computed from the same 40/30/20/10 component weights. To reflect FX uncertainty, tune the matching **rule** itself (for example, use a TOLERANCE rule or adjust date/reference requirements) rather than these score fields.
For the full scoring model, see [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring).
## FX from transaction metadata
***
When a transaction has no base amount yet, Matcher converts it at match time using FX hints carried in that transaction's `metadata`. Matcher does **not** call any external rate provider — the rate travels with the row.
Conversion only runs when `fx_base_currency` is present, and it never overwrites a base amount that is already set on the transaction. The original `amount` and `currency` are never mutated — conversion changes only the comparison.
### Metadata fields
| Metadata field | Required | Purpose |
| ------------------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `fx_base_currency` | Yes (to trigger) | The base currency the amount is converted **into**. Becomes `currencyBase`. |
| `fx_rate` | Yes, unless `fx_notional_expr` is set | Multiplicative rate. `amountBase = amount * fx_rate`. |
| `fx_notional_expr` | No | Expression evaluated against the transaction metadata to derive the base notional directly. When present, it takes precedence over `fx_rate`. |
| `fx_rate_source` | No | Optional label identifying where the rate came from, kept for validation and audit. Defaults to `metadata`. |
### Example transaction with FX metadata
```json theme={null}
{
"external_id": "txn_001",
"amount": 1000.00,
"currency": "EUR",
"date": "2024-01-15",
"metadata": {
"fx_base_currency": "USD",
"fx_rate": "1.085",
"fx_rate_source": "ecb"
}
}
```
With the metadata above, Matcher derives `amountBase = 1000.00 * 1.085 = 1085.00` and `currencyBase = USD`, then compares against the other side using the rule's `matchBaseAmount` / `matchBaseCurrency` settings.
If a transaction already carries a base amount, these metadata hints are ignored — Matcher never overwrites an existing base amount. If the hints are missing or malformed (unparseable rate, failed expression), the transaction simply does not participate in base-amount matching — the run is not aborted.
### When base fields are missing
When a rule requires base-amount matching (`matchBaseAmount` / `matchBaseCurrency`) and transactions lack a base amount or base currency, Matcher records the condition under the `FX_RATE_UNAVAILABLE` exception reason. You can filter the exceptions list by `reason=FX_RATE_UNAVAILABLE` (along with the related `MISSING_BASE_AMOUNT` and `MISSING_BASE_CURRENCY` reasons) to find transactions that could not join base-amount comparison.
## FX rate-variance band
***
Cross-currency amounts often disagree slightly because each side was converted at a different rate or on a different day. The `fxVarianceBand` key on TOLERANCE rules handles this: it defines a **second threshold stacked above the match tolerance**, expressed as a decimal fraction (`0.0001` = 1 basis point).
After the strict-tolerance pass, Matcher rescans unmatched cross-currency `1:1` pairs. A pair whose base-amount residual exceeds the match tolerance but stays within the band still **matches** — the pair becomes a proposed group with a fixed confidence of 75, below the auto-confirm threshold, so it always requires human review. Both transactions are flagged with the `FX_RATE_VARIANCE` exception reason so the residual is recorded as a typed exception instead of collapsing to `UNMATCHED`.
The band applies only when:
* both sides carry a base amount and the same base currency;
* the original currencies differ (same-currency drift is a plain mismatch, not an FX case);
* all of the rule's other gates (date window, reference, currency, composite fields) still pass.
A zero or absent `fxVarianceBand` disables the band.
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"type": "TOLERANCE",
"priority": 3,
"config": {
"matchBaseAmount": true,
"matchBaseCurrency": true,
"percentTolerance": 0.01,
"fxVarianceBand": "0.005"
}
}'
```
## Transaction fields
***
For multi-currency matching, each transaction carries both original and base currency fields. You supply `amount` and `currency` at upload; Matcher derives `amountBase` and `currencyBase` at match time from the FX metadata:
| Field | Type | Description |
| -------------- | ------- | ------------------------------------------------------------- |
| `amount` | Decimal | Original transaction amount (supplied at upload) |
| `currency` | String | Original ISO 4217 currency code (supplied at upload) |
| `amountBase` | Decimal | Amount converted to base currency (derived from FX metadata) |
| `currencyBase` | String | Base currency ISO 4217 code (derived from `fx_base_currency`) |
### Example transaction
After FX conversion, a transaction looks like this internally:
```json theme={null}
{
"external_id": "txn_001",
"amount": 1000.00,
"currency": "EUR",
"amountBase": 1085.00,
"currencyBase": "USD",
"date": "2024-01-15",
"description": "PAY-2024-001"
}
```
## Example: cross-currency reconciliation
***
**Source (EUR account):**
| ID | Amount | Base amount |
| -------- | ------------ | ------------ |
| txn\_001 | 1,000.00 EUR | 1,085.00 USD |
**Target (USD account):**
| ID | Amount | Base amount |
| -------- | ------------ | ------------ |
| txn\_002 | 1,095.00 USD | 1,095.00 USD |
With a TOLERANCE rule (`matchBaseAmount: true`, `percentTolerance: 0.02`):
* Base amounts: $1,085.00 vs $1,095.00
* Variance: \$10.00 (0.92%)
* Tolerance: 2%
* Result: **Match** (0.92% \< 2%)
## Best practices
***
Attach `fx_base_currency` and `fx_rate` (or `fx_notional_expr`) to each transaction's metadata at the source, using the rate that applied when the transaction settled. Because the rate travels with the row, results are reproducible across runs — no runtime rate lookups.
`matchBaseScore` and `matchScore` are reserved fields and do not change the calculated confidence score — the engine always uses the fixed 40/30/20/10 weights. To flag FX-converted matches for review, design the rule itself (e.g. tighter tolerances or required reference/date checks) instead of relying on these score fields.
FX conversions introduce small variances. Use TOLERANCE rules with matchBaseAmount to allow for rounding and rate timing differences.
Use a consistent base currency across all contexts. USD is common for international operations; use your reporting currency for domestic + international.
## Next steps
***
How match scores work and what thresholds apply.
Full reference for rule types and config fields.
# Split and aggregate matches
Source: https://docs.lerian.studio/en/matcher/reference/matcher-split-aggregate
Reconcile 1:1, 1:N (split and aggregate), and N:M transaction patterns using context types and rule allocation flags to control how amounts are distributed.
Real-world reconciliation often involves transactions that don't match 1:1. A single payment may cover multiple invoices, or several deposits may consolidate into one bank entry. Matcher handles these complex scenarios through split and aggregate matching.
## Overview
***
Matching cardinality is controlled by the **context type**. Matcher supports three context types:
| Context type | Description | Example |
| ----------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| **1:1** — one to one | One source to one target | Single invoice payment |
| **1:N** — one to many / many to one | One source to many targets (**split**) or many sources to one target (**aggregate**) | Bulk payment covering invoices; deposits consolidated at a bank |
| **N:M** — many to many | Any combination of sources and targets | Complex netting |
There is no separate `N:1` context type. Aggregate matching (many sources to one target) is simply the `1:N` context type applied in the aggregate direction — the same context type covers both split and aggregate.
## How it works
***
Split and aggregate behavior is controlled by two mechanisms:
1. **Context type** — determines the matching cardinality (`1:1`, `1:N`, or `N:M`).
2. **Rule allocation flags** — control how amounts are distributed within a match group.
There is no separate "split" or "aggregate" setting on the context. The context type defines what patterns are allowed, and the rule config controls allocation behavior.
### Context type mapping
| Context type | Allowed patterns |
| ------------ | ----------------------------------------------------------------------------- |
| `1:1` | One source to one target only |
| `1:N` | One source to many targets (split), or many sources to one target (aggregate) |
| `N:M` | Any combination of sources and targets |
### Rule allocation settings
All rule types accept allocation flags in their `config`:
| Field | Type | Description |
| -------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowPartial` | Boolean | Allow partial allocation of transaction amounts |
| `allocationDirection` | String | Allocation order: `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT` |
| `allocationToleranceMode` | String | How tolerance is measured: `ABS` (absolute) or `PERCENT` |
| `allocationToleranceValue` | Decimal | Tolerance threshold for allocation residuals |
| `allocationUseBaseAmount` | Boolean | Use base (converted) amount for allocation |
| `feeAware` | Boolean | Fee-aware 1:N allocation: consume each candidate's gross share (net + fee) instead of net only. Useful for marketplace splits where the payout is net of fees |
| `nmDeductionBand` | Decimal | TOLERANCE rules only. Short-pay band for the N:M solver, as a decimal fraction of the short invoice face (`0.05` = 5%). Lets a payment subset under-pay an invoice subset within the band. Zero or absent disables it |
### Example: tolerance rule with allocation
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "TOLERANCE",
"priority": 2,
"config": {
"percentTolerance": 0.01,
"absTolerance": 5.0,
"matchCurrency": true,
"allowPartial": true,
"allocationDirection": "LEFT_TO_RIGHT",
"allocationToleranceMode": "ABS",
"allocationToleranceValue": 10.0,
"matchScore": 85,
"matchBaseScore": 80
}
}'
```
`matchScore` and `matchBaseScore` are accepted and validated but **reserved/inert** — they do not change the calculated confidence score. Confidence is always computed from the fixed internal component weights (amount 40, currency 30, date 20, reference 10). See [Confidence scoring](/en/matcher/reference/matcher-confidence-scoring).
## Creating a 1:N context
***
To enable split or aggregate matching, create a context with type `1:N`:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Payment Reconciliation",
"type": "1:N",
"interval": "daily"
}'
```
API Reference: [Create context](/en/reference/matcher/create-context)
## 1:N split matching
***
One source transaction matches multiple target transactions.
### Common use cases
* **Bulk payment**: Single wire covering multiple invoices
* **Payroll**: One bank debit for multiple salary payments
* **Settlement**: One gateway payout for multiple orders
### Example: bulk invoice payment
**Source (Bank Statement):**
| ID | Amount | Reference |
| --------- | ----------- | ----------------- |
| bank\_001 | \$15,000.00 | BULK-PAY-2024-001 |
**Targets (Ledger Entries):**
| ID | Amount | Invoice |
| -------- | ---------- | ------------ |
| inv\_001 | \$5,000.00 | INV-2024-001 |
| inv\_002 | \$7,500.00 | INV-2024-002 |
| inv\_003 | \$2,500.00 | INV-2024-003 |
**Result:** 1:3 match with full allocation
## Aggregate matching (many-to-one)
***
Multiple source transactions match one target transaction. This is the aggregate direction of the `1:N` context type — it is not a separate `N:1` type.
### Common use cases
* **Bank deposits**: Multiple checks deposited as one credit
* **Card settlements**: Daily batch of transactions as one deposit
* **Cash consolidation**: Multiple register receipts to one deposit
### Example: consolidated deposit
**Sources (Point of Sale):**
| ID | Amount | Register |
| -------- | ---------- | -------- |
| pos\_001 | \$1,250.00 | REG-01 |
| pos\_002 | \$980.00 | REG-02 |
| pos\_003 | \$1,770.00 | REG-03 |
**Target (Bank Statement):**
| ID | Amount | Reference |
| --------- | ---------- | ---------------- |
| bank\_002 | \$4,000.00 | DEPOSIT-20240120 |
**Result:** 3:1 match with full allocation
## N:M many-to-many matching
***
Multiple source transactions match multiple target transactions. This is the most complex pattern.
### Common use cases
* **Intercompany netting**: Multiple invoices netted against multiple payments
* **Trade settlements**: Complex clearing with partial fills
* **Revenue recognition**: Multiple deliveries against multiple advances
### Example: intercompany netting
**Sources (Company A Payables):**
| ID | Amount | Reference |
| -------- | ----------- | ---------- |
| pay\_001 | \$10,000.00 | IC-PAY-001 |
| pay\_002 | \$8,000.00 | IC-PAY-002 |
**Targets (Company A Receivables):**
| ID | Amount | Reference |
| -------- | ----------- | ---------- |
| rec\_001 | \$12,000.00 | IC-REC-001 |
| rec\_002 | \$6,000.00 | IC-REC-002 |
**Result:** 2:2 match, \$18,000 total matched
To enable N:M matching, create a context with type `N:M`:
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Intercompany Netting",
"type": "N:M",
"interval": "weekly"
}'
```
## Running and reviewing matches
***
After configuring the context and rules, trigger a matching run and review the resulting groups.
### Run matching
```bash cURL theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "DRY_RUN"
}'
```
### View match run history
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/matching/contexts/{contextId}/runs" \
-H "Authorization: Bearer $TOKEN"
```
### View a run's match groups
The `contextId` query parameter is required. The response is a cursor-paginated list of match groups, each containing its matched transactions (across all cardinalities) and confidence scores.
```bash cURL theme={null}
curl -X GET "https://api.matcher.example.com/v1/matching/runs/{runId}/groups?contextId={contextId}" \
-H "Authorization: Bearer $TOKEN"
```
### Break (unmatch) a match group
To reverse an incorrect group, unmatch it. A `PROPOSED` group is rejected with a reason and its transactions return to `UNMATCHED`. For a `CONFIRMED` group, Matcher also reverses the residual/open-item effects that confirmation applied, atomically with revoking the group and returning its transactions. The `contextId` query parameter is required, and a `reason` is sent in the body.
```bash cURL theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/matching/groups/{matchGroupId}?contextId={contextId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "incorrect match - amounts do not match"
}'
```
If the confirmed group's reversal removes the final live contribution behind an obligation, that open item becomes terminal `WITHDRAWN`: it remains as history but is not nettable or carried into another run. Matcher checks reversibility before changing anything. If a later live entry still stands on the residual, or a newer live obligation would conflict with restoring a terminal item on the same identity, the endpoint returns `409 Conflict` and leaves the group, transactions, and open items unchanged.
## Matching algorithm
***
The algorithm depends on the context type.
### 1:N — deterministic sequential allocation
For split and aggregate (`1:N`) scenarios, Matcher uses deterministic sequential allocation:
1. **Sort**: Transactions are sorted deterministically to ensure reproducible results across runs.
2. **Iterate**: The engine walks through candidates in priority order.
3. **Allocate**: Amounts are distributed according to the `allocationDirection` setting (`LEFT_TO_RIGHT` or `RIGHT_TO_LEFT`).
4. **Track residuals**: Any remaining unallocated amounts are tracked. If `allowPartial` is `true`, an overshooting leg is capped to the remaining amount; an under-covered split still surfaces a diagnostic exception.
### N:M — set-matching solver
For `N:M` scenarios, Matcher does **not** allocate sequentially. It uses a bounded subset-selection solver: candidates are bucketed by the rule's match identity, and the solver searches for a subset of left transactions and a subset of right transactions that reconcile against each other, with cardinality capped per side. Selection is deterministic over the sorted input, each proposed group must clear the fixed confidence gate (minimum score 60), and no transaction lands in two proposed groups within a run. On TOLERANCE rules, the `nmDeductionBand` key lets the solver admit a payment subset that under-pays an invoice subset within the band.
### Exception reasons
Transactions that cannot be fully reconciled surface as typed exceptions:
* `SPLIT_INCOMPLETE` — allocations exist but do not fully cover the target amount, regardless of `allowPartial`.
* `OVER_SETTLED` — a leg over-shot what it was settling; the over-settled remainder is surfaced as a typed break.
You can filter the exceptions list by these `reason` values.
## Best practices
***
Many-to-many matching is complex. Start with simpler patterns and enable N:M only when necessary.
Small rounding differences are common in split payments. Set allocationToleranceValue to a few cents to avoid false exceptions.
Only set allowPartial to true when partial matches are expected. This prevents false matches from incomplete data.
Always test split and aggregate matching in DRY\_RUN mode first to verify allocation results.
Track residual amounts over time. Growing residuals may indicate systematic matching issues.
## Next steps
***
Configure rules and allocation settings.
Security and access control.
# Context rules
Source: https://docs.lerian.studio/en/matcher/ui/context-rules
Create and manage EXACT, TOLERANCE, DATE_LAG, and FUZZY match rules from the Rules tab of a context's setup page in the Matcher UI.
Use the **Rules** tab in a context's setup page to define and manage match rules that control how transactions are paired during reconciliation. Rules specify the criteria the match engine uses to identify corresponding transactions across data sources.
## Accessing the Rules tab
***
Navigate to **Configure → Contexts** in the left sidebar.
Select a context from the list to open its setup page.
Select the **Rules** tab.
The global **Context Switcher** in the sidebar lets you change the active reconciliation context at any time.
## Rules list
***
The Rules tab lists match rules in priority order. Rules evaluate top to bottom; the first match wins. Each visible row shows the rule's strategy and a short configuration summary, with **Move rule up** / **Move rule down** arrows, an **Edit rule** button, and a **Delete rule** button.
For contexts with 100 rules or fewer, priority is assigned automatically when you create a rule — a new rule is appended to the end of the chain. To change precedence, reorder the list with the up/down arrows. There is no priority field in the form.
The Rules tab loads only the first 100 rules by priority and provides no pagination. Rules after that cannot be viewed or managed here. **Add rule** derives the new priority from those 100 rules; once a rule at priority 101 is hidden, the next creation attempt conflicts with it and fails.
## Creating a rule
***
In the **Rules** tab, click the **Add rule** button.
A dialog opens. Select a **Strategy**:
| Strategy | Description |
| ------------- | -------------------------------- |
| **Exact** | Field-for-field equality |
| **Tolerance** | Amount/date within a band |
| **Date lag** | Settlement delay window |
| **Fuzzy** | Approximate reference similarity |
Depending on the strategy, different fields appear (see below).
Click **Create rule**.
Strategy is fixed after creation. To change a rule's strategy, delete the rule and recreate it.
## Rule types
***
### EXACT
Matches transactions by comparing fields for exact equality.
Top-level toggles:
| Field | Description |
| ------------------- | --------------------------- |
| **Match amount** | Compare transaction amounts |
| **Match currency** | Require currency equality |
| **Match date** | Compare transaction dates |
| **Match reference** | Compare reference fields |
The **Advanced** section adds:
| Field | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Date precision** | Precision for date comparison: `DAY` or `TIMESTAMP` |
| **Case-insensitive reference compare** | Ignore case when comparing references |
| **Reference must be present** | When **Match reference** is enabled, require a non-empty reference value |
| **Match base amount** | Also match on the base (pre-conversion) amount |
| **Match base currency** | Also match on the base currency |
| **Sign mode** | How amount signs compare: `same` matches like signs, `opposite` matches a refund against its charge, `ignore` compares magnitudes only |
| **Match score** | Accepted configuration value (0–100). It is stored but does not determine awarded confidence, which uses the engine's fixed weighted comparison components |
| **Base-match score** | Accepted configuration value (0–100). It is stored but does not determine awarded confidence |
| **Allocation (1:N / N:1)** | Partial allocation settings (see below) |
| **Match fields** | Composite-key editor for matching on additional named fields, combined with the toggles above |
### TOLERANCE
Matches transactions within a numeric or date tolerance band.
Top-level fields:
| Field | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Absolute tolerance** | Fixed amount tolerance. The effective amount band uses the larger of this and the percentage-derived amount tolerance |
| **Percent tolerance** | Percentage-derived amount tolerance (e.g., `0.005` = 0.5%). The effective amount band uses the larger of this and the absolute tolerance |
| **Date window (days)** | Allowed date drift between sides (0–3650) |
| **Match currency** | Require currency equality |
Zero tolerances are valid — both values default to `0`, which makes the amount band require equality after the default `HALF_UP` rounding at scale `2`.
The **Advanced** section adds:
| Field | Description |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Rounding mode** | How values round before comparison: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE` |
| **Rounding scale** | Decimal places for rounding (0–10) |
| **Percentage base** | Which side the percent tolerance is measured against: `MAX`, `MIN`, `AVERAGE`, `LEFT`, or `RIGHT` |
| **Match reference** | Compare reference fields |
| **Case-insensitive reference compare** | Ignore case when comparing references |
| **Reference must be present** | When **Match reference** is enabled, require a non-empty reference value |
| **Match base amount** / **Match base currency** | Also match on the base amount/currency |
| **Sign mode** | `same`, `opposite`, or `ignore` (as for EXACT) |
| **Match score** / **Base-match score** | Accepted configuration values (0–100). They are stored but do not determine awarded confidence, which uses the engine's fixed weighted comparison components |
| **FX variance band** | Extra cross-currency tolerance above the match band, as a decimal fraction (`0.0001` = 1 basis point). A residual within it still matches and records an FX rate-variance exception; `0` disables |
| **Lockbox deduction band** | Short-pay tolerance for N:M lockbox matching, as a decimal fraction of the invoice face (`0.05` = 5%); `0` disables |
| **Business-day & timezone** | Holiday calendar and timezone for date comparison |
| **Allocation (1:N / N:1)** | Partial allocation settings (see below) |
| **Match fields** | Composite-key editor, as for EXACT |
### DATE\_LAG
Matches transactions that occur within a configurable number of days of each other.
Top-level fields:
| Field | Description |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| **Min days** | Minimum day lag allowed (0–3650; defaults to `0`) |
| **Max days** | Maximum day lag allowed (0–3650; defaults to `0`) |
| **Direction** | Which side is expected to be earlier: `ABS` (absolute), `LEFT_BEFORE_RIGHT`, or `RIGHT_BEFORE_LEFT` |
| **Inclusive bounds** | Include the min/max day boundaries |
**Max days** must be greater than or equal to **Min days**. Exclusive bounds (`Inclusive bounds` off) with **Min days** set to `0` are rejected, because that would exclude same-day transactions.
The **Advanced** section adds:
| Field | Description |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Fee tolerance** | Maximum absolute difference between the transaction amounts (defaults to `0`) |
| **Match currency** | Require currency equality |
| **Match reference** | Off by default; enable to require reference equality |
| **Case-insensitive reference compare** | Ignore case when comparing references |
| **Reference must be present** | When **Match reference** is enabled, require a non-empty reference value |
| **Match score** | Accepted configuration value (0–100). It is stored but does not determine awarded confidence, which uses the engine's fixed weighted comparison components |
| **Business-day & timezone** | **Holiday calendar** (`US Federal` or `Brazil ANBIMA`), **Timezone** (IANA zone, defaults to UTC), and **Count business days only** — measure the lag in business days, skipping the calendar's weekends and holidays. Requires a calendar |
| **Allocation (1:N / N:1)** | Partial allocation settings (see below) |
DATE\_LAG rules have no base-amount options (`Match base amount` / `Match base currency` are only available on EXACT and TOLERANCE).
### FUZZY
Matches transactions by approximate reference similarity, with configurable financial checks on amount, currency, and date. Fuzzy matches always propose for review — they never auto-confirm.
Top-level fields:
| Field | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Minimum similarity** | Reference similarity threshold as a ratio from 0 to 1 (`0.80` = 80% similar). References below it are rejected; if they clear it and the enabled financial checks pass, the pair receives a graded confidence score. Default `0.80` |
| **Match amount** | Compare transaction amounts |
| **Match currency** | Require currency equality |
| **Match date** | Compare transaction dates |
The **Advanced** section adds:
| Field | Description |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Date precision** | `DAY` or `TIMESTAMP` |
| **Reference must be present** | Defaults to on — two blank references would otherwise compare as fully similar |
| **Sign mode** | `same`, `opposite`, or `ignore` |
| **Match score** | Accepted configuration value (0–100; default `70`). It is stored but does not determine awarded confidence; fuzzy confidence uses the engine's fixed weighted components and graded reference similarity |
| **Allocation (1:N / N:1)** | Partial allocation settings (see below) |
FUZZY rules have no reference-equality toggle and no base-amount options.
## Allocation settings
***
EXACT, TOLERANCE, DATE\_LAG, and FUZZY rules include an **Allocation (1:N / N:1)** block inside the **Advanced** section:
| Field | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Allow partial allocation** | Match a set of items against one counterpart, splitting the amount across them; an unallocated remainder carries forward as an open item |
| **Fee-aware split** | Consume each leg's gross share (net + fee) from the counterpart instead of its net amount alone. Off uses net only |
| **Allocation direction** | Order legs are consumed in; defaults to left-to-right |
| **Allocation tolerance mode** | How the residual is bounded: `ABS` (absolute amount) or `PERCENT` (fraction, `0.01` = 1%). Defaults to `ABS` |
| **Allocation tolerance value** | Residual the split may leave; defaults to `0` |
| **Allocate on base amount** | Use the base amount instead of the converted amount for allocation |
## Previewing a rule
***
The rule dialog includes a **Preview matches** panel. Click **Run preview** to test the rule before saving. The preview is read-only: it uses up to 5,000 extraction-complete unmatched transactions, forms only 1:1 pairs, and does not apply fee normalization, FX variance bands, or allocation. The panel shows how many pairs **Would match**, the unmatched left/right counts, and up to 25 highest-scoring would-match pairs. Nothing is saved.
## Editing a rule
***
Click the **Edit rule** button on a rule to open the dialog. The **Strategy** selector is disabled — strategy is fixed after creation. Update the remaining fields and click **Save changes**.
## Reordering rules
***
Use the **Move rule up** / **Move rule down** arrows on the list to change rule precedence. Rules evaluate top to bottom; the first match wins.
## Deleting a rule
***
Click the **Delete rule** button on a rule and confirm in the **Delete rule?** dialog. The rule is removed from the chain; remaining rules keep their order.
# Matcher UI overview
Source: https://docs.lerian.studio/en/matcher/ui/matcher-ui-overview
Meet Matcher's standalone web app — configure contexts, sources, and rules, upload files, run matches, and resolve exceptions without writing any API calls.
The **Matcher UI** is the standalone web application for operating Matcher, Lerian's reconciliation engine. It puts the full reconciliation workflow behind a visual interface: configure contexts, sources, and match rules, upload transaction data, run reconciliations, review matches, and resolve exceptions — all without writing API calls.
The Matcher UI is **not part of Lerian Console**. It ships as its own web application, deployed alongside the Matcher API, and does not require a Console installation. If you operate other Lerian products through the Console, Matcher is the exception: it brings its own work surface.
Matcher is only available if you've acquired the Matcher product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more.
## Accessing the Matcher UI
***
Your platform team deploys the Matcher UI alongside the Matcher API and shares the URL for your environment. Sign in with the credentials your identity provider issues; every action in the UI is authorized against the Matcher API with your identity and tenant — the UI has no separate permission model.
Once signed in, the **active context** shown in the sidebar controls which reconciliation context you're working with. The active context applies across Matcher's pages.
## Main work surfaces
***
Reconciliation health at a glance — match rate, cash exposure, SLA compliance, and setup progress for the active context.
Create contexts and manage their sources, match rules, and schedules from each context's detail page.
Import transaction files and start reconciliation runs, then track their progress.
Review confirmed match groups and confidence scores inside each run's detail page, or pair remaining transactions in the Unmatched workbench.
Work the queue of transactions that didn't match automatically — resolve, dispatch, or escalate them into formal disputes.
Operational history, immutable audit logs, and downloadable archives for compliance.
For a page-by-page directory of every screen in the UI, see [Matcher UI pages](/en/matcher/ui/matcher-module).
## Beyond the UI
***
Everything the Matcher UI does runs through the Matcher API, so you can mix surfaces freely:
* **API** — automate any workflow programmatically. Start with the [developer quick start](/en/reference/matcher/matcher-developer-quick-start).
* **MCP server** — let an AI assistant operate Matcher through the [Model Context Protocol](/en/matcher/mcp/matcher-mcp-overview).
# Pix reconciliation
Source: https://docs.lerian.studio/en/matcher/use-case-guides/matcher-pix-reconciliation
Configure Matcher to reconcile Pix cash-in and cash-out flows between Midaz Ledger and BACEN's SPI settlement extract using endToEndId as the matching key.
This guide walks through reconciling Pix transactions between Midaz Ledger and BACEN (Brazil's Central Bank) settlement data using Matcher. It covers both Pix sent (cash-out) and Pix received (cash-in), from configuration to daily operation and exception handling.
By the end of this guide, you will have a fully configured Matcher context that automatically reconciles your Pix transactions against BACEN's SPI settlement extracts on a daily basis.
## Pix transaction flows
***
Understanding how Pix transactions flow through the system is essential for configuring reconciliation correctly. The two flows below show what Matcher needs to reconcile on each side.
### Pix sent (cash-out)
1. **Client initiates Pix** — The end user triggers a Pix payment via the app or API.
2. **Plugin creates initiation** — The Pix plugin creates an initiation record and resolves the destination account via DICT lookup.
3. **Plugin processes payment** — The plugin debits the client account in Midaz (transaction in `pending` status) and sends the payment instruction to SPI.
4. **Settlement confirmed** — SPI sends a webhook confirming settlement. The Midaz transaction is committed.
5. **Matcher reconciles** — Matcher compares the committed Midaz transaction against the corresponding entry in the BACEN SPI settlement extract.
### Pix received (cash-in)
1. **Inbound Pix arrives** — SPI sends a synchronous webhook containing the inbound Pix data.
2. **Plugin validates** — The Pix plugin validates the payload and approves the transaction.
3. **Credit transaction created** — The plugin creates a CREDIT transaction in Midaz for the recipient account.
4. **Settlement confirmed** — The settlement webhook confirms the transaction is final.
5. **Matcher reconciles** — Matcher compares the Midaz credit transaction against the corresponding entry in the BACEN SPI settlement extract.
In both flows, the **endToEndId** is the unique identifier that links the Midaz transaction to the BACEN settlement record. This is the primary key for reconciliation.
## Configuration step-by-step
***
Create a reconciliation context for Pix transactions. Use type `1:1` because each Pix transaction has exactly one corresponding BACEN settlement entry.
```json theme={null}
{
"name": "Pix Daily Reconciliation",
"type": "1:1",
"interval": "daily",
"feeToleranceAbs": "0",
"feeTolerancePct": "0",
"autoMatchOnUpload": false
}
```
Pix transactions have no intermediate fees or partial settlements. A Pix of R$ 150.00 in Midaz must appear as exactly R$ 150.00 in the BACEN extract. Set both tolerance values to zero — they are decimal strings, so pass `"0"`.
Setting `autoMatchOnUpload` to `false` gives you control over when matching runs, which is important when you need both sources ingested before executing.
See the full request schema at [Create context](/en/reference/matcher/create-context).
Each context needs two sources: one for Midaz transactions and one for the BACEN settlement extract.
**Source A — Midaz (type `LEDGER`):**
```json theme={null}
{
"name": "Midaz - Pix Transactions",
"type": "LEDGER",
"side": "LEFT",
"config": {}
}
```
`LEDGER` is Matcher's category for internal ledger data — it is not a live Midaz connector. You export the day's Pix transactions from Midaz (including the `endToEndId` metadata as a flat column) and upload the export to this source, manually or via an automated pipeline. See [Matcher and Midaz](/en/matcher/integrations/matcher-midaz-integration) for the export/import flow.
**Source B — BACEN SPI extract (type `CUSTOM`):**
```json theme={null}
{
"name": "BACEN SPI Settlement Extract",
"type": "CUSTOM",
"side": "RIGHT",
"config": {
"description": "Daily SPI settlement file from BACEN"
}
}
```
The BACEN source uses type `CUSTOM` because the SPI settlement file is uploaded manually or via an automated pipeline each day.
Each source must declare a `side` (`LEFT` or `RIGHT`). A context reconciles its `LEFT` source against its `RIGHT` source, so assign one side to Midaz and the other to BACEN and keep the assignment consistent across both sources.
See the full request schema at [Create source](/en/reference/matcher/create-source).
Field maps tell Matcher how to translate fields from each source into the canonical fields used for matching.
A field map is a JSON object in the form `{ "": "" }`. The keys come from Matcher's closed canonical vocabulary (`external_id`, `amount`, `currency`, `date`, and optional `description`, `fee_amount`, `fee_currency`); the values are the raw column names in each source. Lookups are **flat** — a mapping value must name a top-level column in the row, so the Midaz export must carry `endToEndId` as its own flat column (see [Matcher and Midaz — custom field mapping](/en/matcher/integrations/matcher-midaz-integration#custom-field-mapping)).
`external_id` is the **cross-side matching reference** — the value the engine compares between the two sources when a rule sets `matchReference`. Map it to the `endToEndId` on **both** sides. Do not map side-local row IDs (the Midaz transaction `id`, the BACEN `id_liquidacao`) into `external_id`: those values never agree across sources, so reference matching would never find a counterpart.
The following table shows how each canonical field maps to the column in each source:
| Canonical field | Midaz export column | BACEN extract column |
| --------------- | ------------------- | -------------------- |
| `external_id` | `endToEndId` | `end_to_end_id` |
| `amount` | `amount` | `valor` |
| `currency` | `asset_code` | `moeda` |
| `date` | `created_at` | `data_liquidacao` |
**Midaz source — field map:**
```json theme={null}
{
"mapping": {
"external_id": "endToEndId",
"amount": "amount",
"currency": "asset_code",
"date": "created_at"
}
}
```
**BACEN source — field map:**
```json theme={null}
{
"mapping": {
"external_id": "end_to_end_id",
"amount": "valor",
"currency": "moeda",
"date": "data_liquidacao"
}
}
```
See [Create field map](/en/reference/matcher/create-field-map) for the full request schema and [Field mapping](/en/matcher/configuration/matcher-field-mapping) for the canonical vocabulary.
Match rules define how Matcher compares transactions across sources. For Pix reconciliation, two rules cover the vast majority of scenarios.
**Rule 1 — Exact match by endToEndId (priority 1):**
```json theme={null}
{
"type": "EXACT",
"priority": 1,
"config": {
"matchAmount": true,
"matchCurrency": true,
"matchDate": true,
"matchReference": true,
"datePrecision": "DAY",
"caseInsensitive": false,
"referenceMustSet": true,
"matchScore": 100
}
}
```
This rule resolves approximately 95% of cases. The `endToEndId` is unique per Pix transaction across the entire ecosystem. When the reference, amount, currency, and date all match, it is a confirmed reconciliation with maximum confidence. Note that `caseInsensitive` is set to `false` because `endToEndId` values are case-sensitive, and `referenceMustSet` is `true` to ensure both sides carry the `endToEndId` before comparing — this prevents false positives on amount and date alone.
**Rule 2 — Date tolerance fallback (priority 51):**
```json theme={null}
{
"type": "DATE_LAG",
"priority": 51,
"config": {
"maxDays": 1,
"minDays": 0,
"inclusive": true,
"direction": "ABS",
"feeTolerance": 0,
"matchScore": 85,
"matchCurrency": true
}
}
```
A Pix initiated at 23:58 may settle in BACEN on the following calendar day. This rule allows a 1-day window to cover D+1 settlement scenarios. Note that this rule relies on amount and currency matching only — the `endToEndId` comparison is handled by Rule 1.
See [Create match rule](/en/reference/matcher/create-match-rule) for the full request schema and all available rule types.
Once all configuration is in place, activate the context and create a daily schedule.
**Activate the context:**
```bash theme={null}
curl -X PATCH "https://api.matcher.example.com/v1/contexts/{contextId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "ACTIVE" }'
```
**Create a schedule to run daily at 07:00 UTC:**
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/contexts/{contextId}/schedules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cronExpression": "0 7 * * *",
"enabled": true
}'
```
Running at 07:00 UTC provides enough margin for D+1 settlements to appear in the BACEN extract and for the daily file to be uploaded before the matching run executes.
See [Update context](/en/reference/matcher/update-context) and [Create schedule](/en/reference/matcher/create-schedule) for the full request schemas.
## Daily operation
***
Once configured, the daily reconciliation workflow follows five steps.
Upload the previous day's SPI settlement file to the BACEN source. Matcher parses CSV, JSON, XML, and other formats from its [format catalog](/en/matcher/imports/matcher-import-formats).
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{sourceId}/upload" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "file=@bacen_spi_2026-03-17.csv" \
-F "format=csv"
```
This step can be automated via a pipeline that fetches the SPI file and uploads it before the scheduled matching run.
Export the previous day's committed Pix transactions from Midaz — including the `endToEndId` metadata as a flat column — and upload the export to the Midaz source the same way. This step is usually automated by the same pipeline. See [Matcher and Midaz](/en/matcher/integrations/matcher-midaz-integration) for the export/import flow.
The scheduled run executes automatically at 07:00 UTC. To run matching manually, use the run endpoint.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "mode": "DRY_RUN" }'
```
Use `DRY_RUN` first to preview results without committing them. When satisfied, run again with `COMMIT`:
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/matching/contexts/{contextId}/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "mode": "COMMIT" }'
```
After the run completes, retrieve the matched groups to see the results.
```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/matching/runs/{runId}/groups" \
-H "Authorization: Bearer $TOKEN"
```
Each group shows the matched Midaz transaction and its corresponding BACEN settlement entry, along with the rule that matched them and the confidence score.
Unmatched transactions appear as exceptions. These require investigation — a transaction present in one source but not the other, or a mismatch in amount or date beyond the configured tolerance.
Review exceptions, determine the root cause, and resolve them by force matching, ignoring, or correcting the underlying data.
Always run a `DRY_RUN` first when testing new rules or after configuration changes. This prevents unintended matches from being committed.
## Practical example — one day of data
***
The following example illustrates a complete reconciliation run for March 17, 2026.
### Midaz transactions (Source A)
| ID (type) | endToEndId | Amount · date |
| ----------------- | ---------------------- | -------------------------- |
| txn-001 (Pix OUT) | E123456789202603170001 | 150.00 · 2026-03-17 10:15 |
| txn-002 (Pix IN) | E987654321202603170042 | 3200.50 · 2026-03-17 11:30 |
| txn-003 (Pix OUT) | E555666777202603170099 | 89.90 · 2026-03-17 23:58 |
| txn-004 (Pix IN) | E111222333202603170007 | 500.00 · 2026-03-17 14:00 |
### BACEN SPI extract (Source B)
| id\_liquidacao | end\_to\_end\_id | valor / data\_liquidacao |
| -------------- | ---------------------- | ------------------------ |
| liq-8801 | E123456789202603170001 | 150.00 / 2026-03-17 |
| liq-8802 | E987654321202603170042 | 3200.50 / 2026-03-17 |
| liq-8803 | E555666777202603170099 | 89.90 / 2026-03-18 |
| liq-8804 | E444555666202603170055 | 750.00 / 2026-03-17 |
### Match results
| endToEndId | Midaz ↔ BACEN | Result (rule) |
| ------------- | ------------------ | ----------------------------------- |
| E12345...0001 | txn-001 ↔ liq-8801 | Match — EXACT (score: 100) |
| E98765...0042 | txn-002 ↔ liq-8802 | Match — EXACT (score: 100) |
| E55566...0099 | txn-003 ↔ liq-8803 | Match (D+1) — DATE\_LAG (score: 85) |
| E11122...0007 | txn-004 ↔ — | Exception — — |
| E44455...0055 | — ↔ liq-8804 | Exception — — |
### Analysis
* **txn-001 and txn-002:** Exact match on endToEndId, amount, currency, and date. Rule 1 resolved these with confidence score 100.
* **txn-003:** Pix initiated at 23:58, settled in BACEN on 2026-03-18. Rule 2 (DATE\_LAG with 1-day window) paired this with confidence score 85. Date-lag matches never auto-confirm — the pair lands in the review queue for a human to confirm.
* **txn-004:** Present in Midaz but absent from BACEN. Possible settlement failure or SPI timeout. Investigate the transaction status via the Pix plugin.
* **liq-8804:** Present in BACEN but absent from Midaz. An inbound Pix that was not processed. Check webhook delivery or reprocess the message.
## Handling Pix exceptions
***
The following table covers the most common Pix exception scenarios and recommended actions.
| Scenario | Probable cause | Recommended action |
| ---------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------- |
| In Midaz, not in BACEN | Settlement failure, SPI timeout, rejected transaction | Check transaction status in the Pix plugin. If rejected, revert in Midaz. |
| In BACEN, not in Midaz | Inbound Pix not processed, webhook failure | Reprocess the message. Create a manual transaction if needed. |
| Amount mismatch | Rare in Pix (no intermediate fees). Possible rounding error. | Investigate original records. Force match if the difference is acceptable. |
| Date mismatch (>1 day) | Held transaction, reprocessing | Verify it is the same Pix. Force match or ignore. |
### Force match
When you have confirmed that two records represent the same Pix transaction but Matcher could not match them automatically, use force match.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/exceptions/{exceptionId}/force-match" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"notes": "Confirmed same Pix via endToEndId lookup in SPI",
"overrideReason": "D+2 settlement delay confirmed with BACEN"
}'
```
### Ignore transaction
When a transaction should be excluded from reconciliation (for example, a duplicate entry or an already-reversed Pix), mark it as ignored.
```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/transactions/{transactionId}/ignore" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Pix reversal already processed — duplicate entry"
}'
```
See [Force match](/en/reference/matcher/force-match-exception) and [Ignore transaction](/en/reference/matcher/ignore-transaction) for the full request schemas.
## Pix refunds (devoluções)
***
Pix refunds generate reverse transactions that also need reconciliation. When a refund is processed, the Pix plugin creates a new transaction in Midaz with:
* The `originalEndToEndId` linking back to the original Pix transaction
* A new `returnIdentification` (rtrId) that uniquely identifies the refund in SPI
BACEN's settlement extract includes refund entries with both identifiers, allowing Matcher to reconcile them against the corresponding Midaz refund transactions.
For low refund volumes, these can be reconciled within the same Pix Daily Reconciliation context. For high volumes, create a separate context dedicated to refund reconciliation. This simplifies exception triage and keeps refund metrics isolated from standard Pix flow metrics.
Refund initiation via `POST /v1/transfers/{id}/refunds` is a **Pix plugin** endpoint, **not** a Matcher endpoint. Matcher does not initiate transfers or refunds — it only reconciles the resulting transactions. Each refund carries the `originalEndToEndId` and a new `returnIdentification` for end-to-end tracking, which Matcher then uses to match the refund against BACEN's settlement extract. For refund initiation, see the [Pix plugin documentation](/en/midaz/plugins/pix/pix-overview).
## Best practices
***
The `endToEndId` is the unique Pix identifier across the entire ecosystem — from the initiating institution through SPI to the receiving institution. Ensure it is stored in Midaz transaction metadata and present in the BACEN extract. Without it, reconciliation falls back to amount and date matching, which is far less reliable.
Pix transactions near end of day may settle in BACEN on D+1. Scheduling Matcher for 07:00 UTC ensures all settlements from the previous day are included in the BACEN extract before matching runs. This eliminates false exceptions caused by timing.
When processing high Pix volumes, create two separate contexts — one for cash-out and one for cash-in. This simplifies exception triage, provides more granular metrics per flow, and allows independent scheduling if needed.
A healthy Pix reconciliation achieves greater than 99% automatic match rate. If the rate drops below 95%, investigate systemic issues such as plugin failures, BACEN format changes, or missing metadata in Midaz transactions.
Pix has no intermediate fees, partial settlements, or processing charges. If amounts diverge between Midaz and BACEN, it indicates a real problem — not rounding. Keep both `feeToleranceAbs` and `feeTolerancePct` at zero.
Always run a `DRY_RUN` before `COMMIT`, especially after rule or field map changes. This lets you review match results and catch configuration errors before they affect production data.
## Key metrics
***
Track these metrics to monitor the health of your Pix reconciliation process.
| Metric | Healthy value | Alert threshold |
| -------------------------- | -------------------- | --------------- |
| Automatic match rate | Above 99% | Below 95% |
| Daily exceptions | Below 0.5% of volume | Above 2% |
| Average resolution time | Under 4 hours | Over 24 hours |
| Unresolved exceptions 48h+ | 0 | More than 5 |
Use Matcher's dashboard endpoints to monitor these metrics in real time. See [Dashboard metrics](/en/reference/matcher/get-dashboard-metrics).
## Next steps
***
Learn how to configure and manage reconciliation contexts and data sources.
Explore all available rule types and advanced matching configurations.
Deep dive into automatic field mapping and real-time sync with Midaz Ledger.
Detailed guide on investigating, force matching, and managing reconciliation exceptions.
# What is Matcher?
Source: https://docs.lerian.studio/en/matcher/what-is-matcher
Discover Matcher, Lerian's reconciliation engine that automates transaction matching, confidence scoring, exception routing, and audit trails for compliance.
**Matcher** is Lerian's reconciliation engine that automates transaction reconciliation between any data sources—internal systems, banks, payment processors, ERPs, or payment gateways. Matcher is available to licensed customers; its repository is maintained internally.
You configure matching rules once. Matcher handles the repetitive work—matching transactions, flagging discrepancies, and maintaining audit trails for compliance.
## Why use Matcher
***
Every financial institution reconciles transactions — comparing internal records against bank statements, payment processors, and ERPs. Most do it manually in spreadsheets. This does not scale: matching takes hours, errors go unnoticed, and month-end close becomes stressful.
Matcher solves this:
* **Automate matching**: Configure rules once, let the system handle routine matches
* **Know what's reliable**: Confidence scores tell you which matches are solid and which need review
* **Route exceptions**: Send unmatched items to JIRA, ServiceNow, or your own tools via webhooks
* **Stay compliant**: Every match and resolution is logged immutably for audits
## What Matcher does
***
* **Match automatically**: Apply exact or tolerance-based rules. Get confidence scores (0-100) for every match
* **Handle multiple sources**: Reconcile between banks, ERPs, payment processors, ledgers—all at once if needed
* **Manage exceptions**: Unmatched items get classified, routed to your team, and tracked until resolved
* **Audit everything**: Immutable logs and SOX-ready reports for compliance — a complete audit trail
### Matching features
* **Exact matching**: Match transactions by ID, amount, date, and custom attributes
* **Tolerance matching**: Allow controlled variance in amounts, defined as absolute values or percentages
* **Date range matching**: Handle timing differences using configurable date windows
* **Multi-currency support**: Reconcile transactions across currencies with FX rate handling
* **Split and aggregate matching**: Support 1:N, N:1, and N:M transaction relationships
* **Confidence scoring**: Assign a 0–100 score indicating match reliability
* **Dry run mode**: Preview matching results before committing changes
### Data ingestion
* Import data in **CSV**, **JSON**, **XML**, or **ISO 20022 camt.053** format — plus a catalog of regional layouts (CNAB, OFX, acquirer EDI files). See [Formats and templates](/en/matcher/imports/matcher-import-formats)
* Automatic validation and duplicate detection
* Flexible field mapping for heterogeneous data sources
* Optional: reconcile Midaz Ledger exports through the `LEDGER` source type
### Workflow integration
* Route exceptions to **JIRA** or **ServiceNow**
* Emit events through **webhooks** for custom integrations
* Receive resolution callbacks from external systems
* Track SLAs and automatically escalate breaches
## Common use cases
***
Match your internal records against daily bank statements. Upload CSVs, map fields, run matching. Review exceptions where the bank and your systems don't align.
Reconcile Stripe or Adyen settlements against your ledger or ERP. Handle splits (one payment, many invoices) and aggregates (many payments, one deposit) automatically.
Match Visa, Mastercard, or Elo settlement files against your transaction records. Tolerance rules handle fee variances and timing differences between authorization and settlement.
Reconcile acquiring transactions with complex rate structures. Handle MCC-based rates, transaction type variations, and Interchange Plus models where rates vary by card type, merchant segment, and transaction method.
Verify SAP, Oracle, or NetSuite transactions against your other systems. Catch discrepancies before month-end close or audits.
## How Matcher works
***
Define what you're reconciling (contexts), where data comes from (sources), and how to match (rules).
Upload files or pull data automatically. Matcher validates, normalizes, and deduplicates everything.
Run a matching job. Matcher applies your rules and assigns confidence scores to each proposed match.
High-confidence matches (score 90 or above) are confirmed automatically — except fuzzy and date-lag matches, which always go to review. Lower scores and unmatched items go to your review queue.
Handle exceptions: force a match, create an adjustment, split transactions, or route to JIRA/ServiceNow.
Export reconciliation reports. Every action is logged immutably for audits.
## Integrations with the lerian ecosystem
***
Matcher is a stand-alone product that can optionally integrate with other components of the Lerian ecosystem.
| Product | Integration |
| ----------------------------------------- | -------------------------------------------------------------------------------------------- |
| [Midaz](/en/midaz/about-midaz) | Optional: Reconcile exported Ledger data through the `LEDGER` source type |
| [Reporter](/en/reporter/what-is-reporter) | Optional: Reconciliation reports and audit summaries |
| [Flowker](/en/flowker/what-is-flowker) | Optional: Event-driven reconciliation workflows |
Matcher works independently or as part of the Lerian Studio ecosystem. Use it alone or connect it to Midaz and other financial components as needed.
All Matcher write endpoints (POST, PUT, PATCH) include built-in idempotency through the `X-Idempotency-Key` request header. For details, see [Retries and idempotency](/en/reference/retries-idempotency).
## Next steps
***
Understand the key concepts: contexts, sources, rules, matches, and exceptions.
Explore Matcher's architecture and data flow.
Run your first reconciliation, then review matches and exceptions with confidence.
Learn how to configure exact, tolerance, and date-based matching rules.
[Explore the Matcher API](/en/reference/matcher/matcher-developer-quick-start). Browse endpoints for reconciliation contexts, sources, and matching rules.
# APIX 001
Source: https://docs.lerian.studio/en/reporter/apix-001
Generate the APIX 001 XML report for BACEN — monthly Pix statistics including transactions, refunds, cautionary blocks, revenue, and availability metrics.
Reporter lets you generate XML-based reports that follow the official APIX structure, as required by the Brazilian Central Bank (BACEN).
This guide walks you through the structure and logic used to generate the **APIX 001** report (Document 1201) in XML.
APIX 001 reports must strictly follow the XSD schema defined by BACEN (version 2.5). Reporter automates the XML generation, but you remain responsible for validating the output and ensuring compliance with regulatory requirements.
## What is APIX 001?
***
The **APIX 001** is a monthly regulatory report that Pix participants — direct or indirect — must submit to the Brazilian Central Bank. It consolidates **operational statistics** for the institution's Pix ecosystem during a given month.
### What BACEN expects to receive
The report covers ten data sections:
| Section | Description |
| ----------------------- | ---------------------------------------------------------------------- |
| **Transacoes** | Transaction volumes and values, broken down by detail type and purpose |
| **Devolucoes** | Refund volumes and values (MED and other reasons) |
| **BloqueiosCautelares** | Cautionary block counts and values |
| **Receitas** | Revenue from Pix operations by source and person type |
| **TemposTransacoes** | Transaction processing time percentiles (P50, P99) |
| **TemposDict** | DICT operation time percentiles (queries, registrations, portability) |
| **ConsultasDict** | Total DICT queries in the period |
| **Disponibilidade** | System availability index (percentage) |
| **TempoAutorizacoes** | Authorization processing time (P95) |
| **Autorizacoes** | Pix Automático authorization counts and stock by payer type |
### Submission requirements
| Document | Frequency | Deadline | Reference |
| ------------ | --------- | ---------------------------------------- | ------------------------------ |
| **APIX 001** | Monthly | 15th business day of the following month | Document 1201, XSD version 2.5 |
### Header attributes
The root `` element requires these attributes:
| Attribute | Description | Format |
| ----------- | ------------------------------------------------ | ------------------------------------ |
| `DtArquivo` | File generation date | `YYYY-MM-DD` |
| `Ano` | Reference year | `YYYY` |
| `Mes` | Reference month | `1`–`12` |
| `ISPB` | Institution's ISPB code (first 8 digits of CNPJ) | 8 numeric digits |
| `NomeResp` | Responsible person's name | Text (max 200 chars) |
| `EmailResp` | Responsible person's email | Valid email |
| `TelResp` | Responsible person's phone | Text (max 14 chars) |
| `TipoEnvio` | Submission type | `I` (Inclusion) or `S` (Replacement) |
## Understanding the data structure
***
Before building the template, it's important to understand how the Pix plugin data maps to each APIX 001 section.
### Pix plugin data sources
The template queries data from the **Pix plugin** tables registered as Reporter data sources. The main entities used are:
| Data Source | Table | Content |
| ------------------- | ------------- | ------------------------------------------------------- |
| `payment.transfers` | Pix transfers | Transaction records with status, amount, type, and fees |
| `payment.refunds` | Pix refunds | Refund records with status, amount, and reason code |
| `dict.entries` | DICT entries | Pix key registrations |
| `dict.claims` | DICT claims | Portability and ownership claims |
The Pix plugin does not currently track DICT query events. The `dict.entries` table stores key registrations, not consultations. For `ConsultasDict`, source this metric from your infrastructure monitoring or API gateway logs. The template placeholder is included for structural reference only.
The data source prefix (e.g. `pix_btg`) depends on how you register the Pix plugin in Reporter. Replace it with your actual data source name.
### Transfers table structure
| Field | Type | Description |
| ------------------------- | --------- | ------------------------------------------------------ |
| `id` | UUID | Unique transfer identifier |
| `status` | String | Transaction status: `COMPLETED`, `REJECTED`, `PENDING` |
| `amount` | Decimal | Transaction value in BRL |
| `transfer_type` | String | `CASHIN` (receiving) or `CASHOUT` (sending) |
| `destination_person_type` | String | `NATURAL_PERSON` or `LEGAL_PERSON` |
| `fee_charge` | JSONB | Fee structure (populated only for CASHIN transactions) |
| `failed_reason` | String | Rejection reason code (when status is REJECTED) |
| `created_at` | Timestamp | Creation date and time |
### Refunds table structure
| Field | Type | Description |
| ------------ | --------- | ------------------------------------------- |
| `id` | UUID | Unique refund identifier |
| `status` | String | Refund status: `COMPLETED`, `PENDING` |
| `amount` | Decimal | Refund value in BRL |
| `reason` | String | Reason code: `FR01`, `BE08`, `MD06`, `SL02` |
| `created_at` | Timestamp | Creation date and time |
### Refund reason codes
| Code | Meaning | APIX detail type |
| ------ | --------------------------------------------- | ---------------- |
| `FR01` | Fraud (MED — Mecanismo Especial de Devolução) | 1 |
| `BE08` | Bank error | 2 |
| `MD06` | Customer request | 2 |
| `SL02` | Creditor-specific service (Pix Saque/Troco) | 2 |
### Fee charge structure (JSONB)
The `fee_charge` field is a JSONB column populated **only for CASHIN** (receiving) transactions:
```json theme={null}
{
"applied": true,
"calculationType": "FIXED",
"totalAmount": "1.50",
"netAmount": "98.50",
"fees": [
{
"type": "TRANSACTION_FEE",
"amount": "1.50"
}
]
}
```
CASHOUT transactions do not have fee charges in the current product model. Revenue from CASHOUT sources should be reported as zero unless your implementation charges fees on outgoing transfers.
## Data mapping
***
### Transaction detail types
| Code | Description | Mapping |
| ---- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 5 | Indirect participant (settled via direct participant) | All completed transactions via the settlement partner |
| 6 | Direct participant (own settlement) | Zero for indirect participants |
| 7 | Rejected due to fraud indication | Transactions with `status == "REJECTED"` filtered by fraud-indication reason (see warning below) |
Detail type 7 should only count rejections due to fraud indication — not all rejected transactions. The current template uses `status == "REJECTED"` as a simplified filter. In production, cross-reference with `dict.infraction_reports` (filtering by fraud-related `situation_type`) or apply specific `failed_reason` codes. Validate this logic with your engineering team before submitting to BACEN.
### Transaction purposes
| Code | Description | Mapping |
| ---- | ------------------- | ------------------------------------------------ |
| 1 | Transfer / Purchase | Completed transactions (`status == "COMPLETED"`) |
| 2 | Pix Saque | Zero if not implemented |
| 3 | Pix Troco | Zero if not implemented |
| 4 | Pix Automático | Zero if not implemented |
The report requires exactly *12 transaction entries* — one for each combination of 3 detail types × 4 purposes. Entries with no matching data must still be present with zero values.
### Revenue sources
| Source | Description | Mapping |
| ------ | -------------------------------------- | -------------------------------------------------------- |
| 1 | Initiation by legal person (CASHOUT) | `fee_charge.totalAmount` from CASHOUT + `LEGAL_PERSON` |
| 2 | Receiving by legal person (CASHIN) | `fee_charge.totalAmount` from CASHIN + `LEGAL_PERSON` |
| 3 | Receiving by natural person (CASHIN) | `fee_charge.totalAmount` from CASHIN + `NATURAL_PERSON` |
| 4 | Initiation by natural person (CASHOUT) | `fee_charge.totalAmount` from CASHOUT + `NATURAL_PERSON` |
### Refund detail types
| Code | Description | Mapping |
| ---- | ------------------ | ------------------------------------- |
| 1 | Fraud refund (MED) | `reason == "FR01"` |
| 2 | Other reasons | `reason != "FR01"` (BE08, MD06, SL02) |
### Cautionary block detail types
| Code | Description |
| ---- | --------------------- |
| 1 | Requested |
| 2 | Received |
| 3 | Canceled by requester |
| 4 | Canceled by recipient |
### Authorization payer types (Pix Automático)
| Code | Description |
| ---- | -------------- |
| 1 | Natural person |
| 2 | Legal person |
## Using Reporter
***
Here is the complete template for generating APIX 001 in Reporter. This example uses `pix_btg` as the data source prefix — replace it with the name configured in your Reporter data source setup.
Reporter's report request (`POST /v1/reports`) accepts only `templateId` and `filters` — there is no input for arbitrary template parameters. Set the reference year and month (`Ano`, `Mes`) and the responsible person's attributes (`NomeResp`, `EmailResp`, `TelResp`) directly in the template before each submission, the same way `TipoEnvio` is set.
```tpl Template theme={null}
{% count_by pix_btg:payment.transfers if status == "COMPLETED" %}{% sum_by pix_btg:payment.transfers by "amount" if status == "COMPLETED" %}0.005100.000.005200.000.005300.000.005400.000.006100.000.006200.000.006300.000.0064{% count_by pix_btg:payment.transfers if status == "REJECTED" %}{% sum_by pix_btg:payment.transfers by "amount" if status == "REJECTED" %}0.007100.000.007200.000.007300.000.0074{% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason == "FR01" %}{% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason == "FR01" %}1{% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason != "FR01" %}{% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason != "FR01" %}200.00100.00200.00300.0040.001{% sum_by pix_btg:payment.transfers by "fee_charge.totalAmount" if transfer_type == "CASHIN" and destination_person_type == "LEGAL_PERSON" and status == "COMPLETED" %}2{% sum_by pix_btg:payment.transfers by "fee_charge.totalAmount" if transfer_type == "CASHIN" and destination_person_type == "NATURAL_PERSON" and status == "COMPLETED" %}30.004001002
```
## Code breakdown
***
### Root element
```tpl theme={null}
```
* `DtArquivo`: File generation date, dynamically inserted via `date_time`
* `Ano` and `Mes`: Reference year and month — static values you update in the template for each submission (the report request carries no template parameters)
* `ISPB`: First 8 digits of the institution's CNPJ, extracted using the `slice` filter from Midaz onboarding data
* `TipoEnvio`: `I` for inclusion, `S` for replacement of previously approved data
### Transactions section
The template explicitly declares all 12 required entries (3 detail types × 4 purposes). Dynamic queries are used where data exists:
```tpl theme={null}
{% count_by pix_btg:payment.transfers if status == "COMPLETED" %}{% sum_by pix_btg:payment.transfers by "amount" if status == "COMPLETED" %}0.0051
```
* `count_by` counts records matching the filter condition
* `sum_by ... by "field"` sums a specific field across matching records
* `ValorEspecie` is `0.00` for standard transfers (only non-zero for Pix Saque/Troco)
Entries for purposes 2, 3, and 4 (Pix Saque, Troco, Automático) use hardcoded zeros when those features are not implemented. Detail type 6 (direct participant settlement) also uses zeros for indirect participants.
### Refunds section
Refunds are split by reason code using the `reason` field:
```tpl theme={null}
{% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason == "FR01" %}{% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason == "FR01" %}1{% count_by pix_btg:payment.refunds if status == "COMPLETED" and reason != "FR01" %}{% sum_by pix_btg:payment.refunds by "amount" if status == "COMPLETED" and reason != "FR01" %}2
```
* `FR01` maps to BACEN detail type 1 (fraud via MED)
* All other reason codes (`BE08`, `MD06`, `SL02`) map to detail type 2
### Revenue section
Revenue is extracted from the `fee_charge.totalAmount` JSONB field, filtered by transfer type and person type:
```tpl theme={null}
{% sum_by pix_btg:payment.transfers by "fee_charge.totalAmount" if transfer_type == "CASHIN" and destination_person_type == "LEGAL_PERSON" and status == "COMPLETED" %}2
```
The `fee_charge` JSONB field uses nested field path syntax (`fee_charge.totalAmount`). Reporter's Pongo2 engine navigates the JSON structure to access the nested value.
### Time metrics and availability
Time metrics (transaction processing times, DICT operation times) and availability index must be sourced from your **infrastructure monitoring system** — they are not derived from transactional data. Populate these values from your SPI logs and uptime monitoring.
### DICT queries
```tpl theme={null}
```
This placeholder counts DICT entries (key registrations) as a structural reference. In production, replace `dict.entries` with your actual DICT query log data source or populate `QtdConsultas` manually from infrastructure metrics.
## Rendered example
***
```xml theme={null}
15000075000000.000.005100.000.005200.000.005300.000.005400.000.006100.000.006200.000.006300.000.0064320160000.000.007100.000.007200.000.007300.000.00744522500.001230115000.00200.00100.00200.00300.0040.00185000.00212000.0030.004001002
```
## Request example with date filter
***
To generate the APIX 001 for a specific month, send a `POST /v1/reports` request with the following body:
```json Request theme={null}
{
"templateId": "APIX_001_TEMPLATE_ID",
"filters": {
"pix_btg": {
"payment.transfers": {
"created_at": {
"between": ["2026-03-01T00:00:00Z", "2026-03-31T23:59:59Z"]
}
},
"payment.refunds": {
"created_at": {
"between": ["2026-03-01T00:00:00Z", "2026-03-31T23:59:59Z"]
}
}
}
}
}
```
| Field | Description |
| ----------------------------------- | --------------------------------------------------- |
| `templateId` | APIX 001 template identifier registered in Reporter |
| `filters.pix_btg.payment.transfers` | Date filter for the transfers collection |
| `filters.pix_btg.payment.refunds` | Date filter for the refunds collection |
| `created_at.between` | Filters records created within the specified month |
Dates must be in ISO 8601 format with UTC timezone (`Z`). Make sure to cover the entire reference month — from the first second to the last.
## XSD validation rules
***
Key constraints from the official APIX 001 XSD (version 2.5) that your data must satisfy:
| Field | Constraint |
| ------------------------ | --------------------------------------------------------- |
| Monetary values | Up to 15 digits total, 2 decimal places, minimum 0 |
| Quantities | Integer, minimum 0, max 999,999,999,999 |
| Time values | Up to 8 digits total, 2 decimal places, 0–999,999 seconds |
| Availability | Percentage, 0.00–100.00 |
| ISPB | Exactly 8 numeric digits |
| Transaction entries | Exactly 12 (3 detail types × 4 purposes) |
| Refund entries | Exactly 2 |
| Cautionary block entries | Exactly 4 |
| Revenue entries | Exactly 4 |
| Authorization entries | Exactly 2 |
BACEN validates both structure and cardinality. If your report has fewer or more entries than expected in any section, the submission will be rejected.
## Best practices
***
### Zero-value entries
Even when there are no transactions for a given combination, the entry must still appear in the report with zero values. BACEN requires all 12 transaction entries, all 4 revenue entries, all 4 cautionary block entries, and all 2 authorization entries — regardless of whether data exists.
### Replacement submissions
Use `TipoEnvio="S"` only to replace a previously **approved** submission. If your first submission was rejected, resubmit with `TipoEnvio="I"`.
### Value precision
All monetary values must have exactly 2 decimal places. Use Reporter's formatting capabilities or ensure your data source provides pre-formatted values.
### Time metric sourcing
Transaction time percentiles and DICT operation times must be sourced from your **infrastructure monitoring** — not from transactional data. These metrics reflect the actual user experience from payment initiation to settlement confirmation. BACEN may audit these values against SPI logs.
### JSONB nested fields
Revenue calculations use nested field paths (e.g. `fee_charge.totalAmount`) to access values inside JSONB columns. Make sure your Reporter version supports nested field parsing in aggregation functions.
Always validate the rendered XML against BACEN's official XSD before submission. You can download the schema from BACEN's regulatory documents portal.
# APIX 002
Source: https://docs.lerian.studio/en/reporter/apix-002
On-demand Pix data submission to BACEN (Document 1202) — 12 months of transaction dimensions kept on hand, with format defined per request.
Pix participants must deliver detailed transaction data to BACEN whenever the regulator requests it. Reporter helps you keep that data structured and ready for ad-hoc extraction.
This guide covers the **APIX 002** report (Document 1202) — what to store, how to organize it, and how to respond when BACEN sends a request.
Unlike APIX 001, APIX 002 has no fixed XML format or XSD schema. Each request defines its own format, dimensions, and deadline. The institution remains responsible for extracting accurate data and meeting the deadline set by the regulator.
## What is APIX 002?
***
The **APIX 002** is an on-demand regulatory submission to the Brazilian Central Bank, defined by Normative Instruction BCB nº 32/2020. Unlike the monthly APIX 001, it is sent only when BACEN explicitly requests it, with the format specified per request.
### APIX 001 vs APIX 002
| Aspect | APIX 001 | APIX 002 |
| ----------------- | ---------------------------------------- | -------------------- |
| **Frequency** | Monthly (mandatory) | On-demand |
| **Deadline** | 15th business day of the following month | Defined per request |
| **Format** | XML (fixed XSD) | Defined per request |
| **Layout / XSD** | Available (version 2.5) | Not applicable |
| **Validation** | Pre + post submission | Post-submission only |
| **Document code** | 1201 | 1202 |
### Who must report
| Participant type | Must report? |
| ------------------------------------------------------- | ------------------------------------------- |
| Direct participant (transactional account provider) | Yes |
| Special settler | Yes |
| Indirect participant (settled via BTG, Itaú, or others) | No — consolidated by the direct participant |
Indirect participants do not report APIX 002 directly. The direct participant consolidates the data on their behalf. However, indirect participants must still *keep the underlying data available* for the direct participant to aggregate when BACEN makes a request.
### Submission attributes
| Field | Value |
| ------------------ | ---------------------------------------------------------- |
| Document name | On-demand Pix participant data |
| Document code | 1202 |
| STA code | APIX002 |
| Frequency | Non-periodic (on-demand) |
| Format | Defined per request |
| Validation | Post-submission |
| Autran transaction | SPIXP001 |
| Reference window | Monthly data covering up to 12 months prior to the request |
## Why APIX 002 has no fixed format
***
The normative explicitly defines the submission format as *"to be defined per demand"* ("Formato para Remessa: a definir para cada demanda"). This has four direct implications:
1. **No standard XSD** is published for APIX 002 — you cannot pre-build a single template.
2. **BACEN sets the format** at the moment of the request, including dimensions, granularity, and file structure.
3. **The institution extracts data** matching whatever dimensions are specified.
4. **The deadline is set per request**, with no standard turnaround time.
To stay ready, your data store needs to support flexible queries across all dimensions BACEN may ask for.
## Data dimensions to store
***
Participants must keep the data described in **Annex II of IN 32/2020** for at least 12 months. The data is organized across three groups: internally settled transactions, all transactions (internal and interbank), and cross-dimensional combinations.
### Internally settled transactions
These dimensions apply to transactions where both payer and receiver belong to the same institution.
| Field | Description | Values |
| ------------------------ | --------------------------------------------------------- | ------------------------------------------------------------- |
| **Scheduled** | Whether the transaction was scheduled | Yes / No |
| **Suspected fraud** | Transactions analyzed past the fraud detection time limit | Yes / No |
| **Not settled in time** | Transactions that exceeded 40 seconds | Yes / No |
| **Rejected** | Whether the transaction was rejected | Yes / No |
| **Rejection reason** | Per arts. 38 and 39 of Resolution BCB nº 1/2020 | Specific codes |
| **Initiation procedure** | How the transaction was initiated | 1) Manual; 2) Pre-stored values |
| **Initiation mechanism** | Mechanism used to start the transaction | 1) Pix key; 2) Dynamic QR; 3) Static QR; 4) Payment initiator |
| **Payer nature** | Person type | 1) Natural person; 2) Legal person |
| **Receiver nature** | Person type | 1) Natural person; 2) Legal person |
| **Initiator** | ISPB of the payment initiator | ISPB code (8 digits) |
### All transactions (internal and interbank)
These dimensions apply to the full transaction set, including interbank settlements via SPI.
| Field | Description | Example |
| ---------------------- | ----------------------------- | -------------------------------------------------------------- |
| **Year** | Reference year | 2026 |
| **Period** | Reference month | 01–12 |
| **Transaction count** | Total count | 1,250,000 |
| **Financial amount** | Sum of values | R\$ 985,000,000.00 |
| **Initiation channel** | Where the transaction started | 1) App; 2) Internet banking; 3) Call center; 4) ATM; 5) Branch |
### Cross-dimensional queries
BACEN may request combinations of multiple dimensions. Examples:
* Count of transactions that are **scheduled** AND **suspected fraud**
* Value of **rejected** transactions by **specific reason** AND **legal person**
* Transactions by **initiation channel** AND **initiation mechanism**
Structure your storage so any combination of these dimensions can be aggregated efficiently. A wide, denormalized table or a columnar store works better than highly normalized models for this type of regulatory query.
## Submission process
***
### System and transaction
| Item | Value |
| ------------------ | ------------------------------------------ |
| System | STA (Sistema de Transferência de Arquivos) |
| File code | APIX002 |
| Autran transaction | SPIXP001 |
| Document number | 1202 |
### Six-step flow
1. **Wait for the BACEN request** — there is no proactive submission.
2. **Review the format and deadline** specified by the regulator.
3. **Extract the data** matching the requested dimensions.
4. **Generate the file** in the specified format.
5. **Submit through STA** using the `APIX002` file code.
6. **BACEN runs post-submission validation** on the received data.
There is no pre-submission validation for APIX 002. BACEN validates the data only after you submit it. Make sure your extraction is accurate before sending.
## Implementation recommendations
***
### Data model
A flat, dimension-rich table simplifies on-demand extraction. The minimum recommended schema:
```sql theme={null}
CREATE TABLE pix_transactions (
id UUID PRIMARY KEY,
occurred_at TIMESTAMP NOT NULL,
year SMALLINT NOT NULL,
month SMALLINT NOT NULL,
amount NUMERIC(18, 2) NOT NULL,
settlement_type VARCHAR(16) NOT NULL, -- INTERNAL / SPI
scheduled BOOLEAN NOT NULL DEFAULT FALSE,
suspected_fraud BOOLEAN NOT NULL DEFAULT FALSE,
not_settled_in_time BOOLEAN NOT NULL DEFAULT FALSE,
rejected BOOLEAN NOT NULL DEFAULT FALSE,
rejection_reason VARCHAR(8),
initiation_procedure SMALLINT NOT NULL, -- 1=Manual, 2=Pre-stored
initiation_mechanism SMALLINT NOT NULL, -- 1=Key, 2=Dynamic QR, 3=Static QR, 4=Initiator
payer_nature SMALLINT NOT NULL, -- 1=Natural, 2=Legal
receiver_nature SMALLINT NOT NULL, -- 1=Natural, 2=Legal
initiation_channel SMALLINT NOT NULL, -- 1=App, 2=Web, 3=Call, 4=ATM, 5=Branch
initiator_ispb VARCHAR(8),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
```
The `settlement_type` column distinguishes internally settled transactions (both parties at the same institution) from interbank transactions settled via SPI. This distinction matters because some dimensions only apply to internal settlements.
### Required capabilities
* [ ] Store transaction data for at least 12 months prior to any request
* [ ] Query by any single dimension or combination of dimensions
* [ ] Aggregate count and financial amount over arbitrary periods
* [ ] Export to multiple formats (CSV, XML, JSON) as BACEN may specify any
* [ ] Generate cross-dimensional reports efficiently
## Regulatory references
***
### Normatives
| Document | Description |
| ------------------------ | ------------------------------------------------- |
| IN BCB nº 32/2020 | Main normative establishing APIX 001 and APIX 002 |
| IN BCB nº 174/2021 | Updates to Annexes I and III of IN 32 |
| IN BCB nº 126/2021 | Earlier updates to IN 32 |
| IN BCB nº 383/2023 | Updates effective from 2024-02-01 |
| Resolution BCB nº 1/2020 | Pix regulation (arts. 38, 39, and 99) |
### Supporting documents
| Document | Description |
| --------------------------- | ------------------------------------------------------- |
| APIX 002 instructions (PDF) | Guidance on preparing and submitting on-demand requests |
| Pix Time Manual | Reference for Pix time limits |
| BACEN submission portal | Instructions and XSD for APIX 001 |
| STA system | File Transfer System access |
| STA instructions | How to use the system |
| Sisbacen / Autran | Transaction lookup |
### BACEN contacts
| Topic | Email |
| ------------------- | ----------------------------------------------------------------- |
| Submission delivery | [suporte.ti@bcb.gov.br](mailto:suporte.ti@bcb.gov.br) |
| Document content | [estatisticas.spb@bcb.gov.br](mailto:estatisticas.spb@bcb.gov.br) |
This page is based on regulation in force as of January 2026. Always check the latest BACEN publications for current requirements.
Unlike APIX 001 which can be fully automated with a Reporter template, APIX 002 requires manual intervention — you must wait for BACEN's specific request, understand the format they define, and build the extraction accordingly. Reporter can help generate the output file, but the template must be created per request.
# CADOC 4010 and 4016
Source: https://docs.lerian.studio/en/reporter/cadoc-4010-and-4016
Generate CADOC 4010 and 4016 XML reports for BACEN with Reporter, following the COSIF standard, required regulatory layout, and validation rules.
Reporter lets you generate XML-based reports that follow the official CADOC structure, exactly as required by the Brazilian Central Bank (BACEN).
This guide walks you through the structure and logic used to generate **CADOC 4010** and **CADOC 4016** reports in XML.
These reports follow the **COSIF** standard and must match the XML structure defined by BACEN. You can adapt the logic to your own data model, but the XML format must be respected.
## What are CADOC 4010 and 4016?
***
The **4010** and **4016** are Analytical Balance Sheet reports used to submit financial reports to the **BACEN** (Central Bank of Brazil).
### What BACEN expects to receive
* Closing balances by COSIF code
* Report base date (reference month)
* Institution's CNPJ (first 8 digits)
* Submission type (`I` = Inclusion, `S` = Replacement)
### Difference between CADOC 4010 and 4016
| Document | Periodicity | Base Date | Expected Accounts |
| -------- | -------------------- | --------- | ------------------------------------------ |
| **4010** | Monthly | YYYY-MM | All accounts |
| **4016** | Semiannual (Jun/Dec) | YYYY-MM | Without groups 7 and 8 (Revenues/Expenses) |
The **4016** represents the entity's accounting position after the income statement closing. At this point, Revenue accounts (group 7) and Expense accounts (group 8) have already been closed and their balances transferred to Equity.
### Submission requirements
| Document | Deadline | STA Code |
| -------- | -------------------------------------------------------- | -------- |
| **4010** | Day 18 of the following month (or the next business day) | ACOS010 |
| **4016** | Last business day of the following month | ACOS016 |
The STA code identifies the document type in BACEN’s transmission system. Use `ACOS010` when submitting CADOC 4010 and `ACOS016` when submitting CADOC 4016.
## Understanding the data structure
***
### Operation routes
Operation routes work as accounting classifiers. Each route has:
* **Unique identifier (`id`)**: Used internally to relate operations
* **COSIF code (`code`)**: The 10-digit accounting code that will be reported to BACEN
Think of routes as "accounting labels" attached to each operation, indicating which chart of accounts item that movement should be classified under.
### Operations
Operations represent financial movements in the ledger. Each operation contains:
* **Associated account (`account_id`)**: Which account was moved
* **Route (`route`)**: ID of the applied accounting route/classification
* **Balance after operation (`available_balance_after`)**: The account balance immediately after this operation
* **Date and time (`created_at`)**: When the operation occurred
### Relationship between entities
The `balance` entity represents the **current** account balance, not the history. For regulatory reports that need balances from a specific period, use the `operation` entity with date filters.
## CADOC structure
***
### Base format
The CADOC report **must** be an XML file and **must** follow the structure defined by **BACEN**:
```xml XML theme={null}
```
### Mandatory fields
**``**
Always starts the file. It defines the XML version and encoding so the system knows how to read the content.
**`` tag**
Wraps the entire CADOC structure and includes:
| Field | Description | Format |
| ----------------- | ---------------------- | -------------------- |
| `codigoDocumento` | Fixed identifier | `"4010"` or `"4016"` |
| `cnpj` | First 8 digits of CNPJ | Numeric, 8 positions |
| `dataBase` | Reference month | YYYY-MM |
| `tipoRemessa` | Submission type | `"I"` or `"S"` |
If your first submission was rejected due to errors, you still need to use `"I"` on your next attempt. Only use `"S"` for replacing previously approved data.
**`` tag**
Groups all account entries for the reporting period.
**`` tag**
* `codigoConta`: Account code, following COSIF format (10 numeric digits).
* `saldo`: Account balance in decimal format (two decimal places).
## Template construction logic
***
### General structure
The template follows a two-level aggregation logic:
1. **First level**: Iterate through all available operation routes
2. **Second level**: For each route, aggregate the balances of linked operations
### Iterating over routes
The template must iterate through all registered operation routes. For each route:
1. **Check if it has a COSIF code**: Only routes with a filled code generate lines
2. **Filter operations**: Select operations that belong to that route
3. **Calculate the balance**: Aggregate the balances of filtered operations
### Variable naming caution
When building the template, avoid using the same name for the iteration variable and the filter field:
| Approach | Example | Result |
| ------------- | --------------------------------------------- | --------------- |
| **Incorrect** | `for route in ... if route == route.id` | Name conflict |
| **Correct** | `for op_route in ... if route == op_route.id` | Works correctly |
## CADOC 4010 template
***
Here's the complete template for generating CADOC 4010 in Reporter:
```xml tpl theme={null}
{%- for op_route in midaz_transaction.operation_route %}
{%- if op_route.code %}
{%- endif %}
{%- endfor %}
```
### Line-by-line explanation
**Line 1 - XML declaration**
Standard XML header with UTF-8 encoding.
**Line 2 - Root element ``**
* `codigoDocumento="4010"`: Fixed identifier for the Analytical Balance Sheet
* `cnpj`: Extracts the first 8 digits from the organization's legal document
* `dataBase`: Generates the date in YYYY-MM format (reference month)
* `tipoRemessa="I"`: Indicates data inclusion
**Line 4 - Start of `for` loop**
* `op_route`: Variable that receives each route during iteration
* `midaz_transaction.operation_route`: Collection of all operation routes
**Line 5 - `if` condition**
Checks if the route has a filled COSIF code.
**Line 6 - `` element**
* `codigoConta`: Displays the current route's COSIF code
* `saldo`: Uses the `sum_by` tag to aggregate balances, filtering operations whose route matches the current route's identifier (`op_route.id`)
**Lines 7-8 - Block closing**
Close the condition and loop respectively.
### Tags and filters used
| Element | Type | Function |
| ---------------------- | ---------- | --------------------------------------------- |
| `{{ variable }}` | Expression | Displays a variable's value |
| `{% tag %}` | Tag | Executes logic (loop, condition, aggregation) |
| `\|slice:':8'` | Filter | Cuts the first 8 characters |
| `for ... in` | Tag | Iterates through a collection |
| `if` | Tag | Condition for execution |
| `sum_by ... by ... if` | Tag | Sums values with conditional filter |
| `date_time` | Tag | Generates formatted date |
## CADOC 4016 template
***
The CADOC 4016 is the Analytical Balance Sheet, similar to 4010, but with semiannual periodicity and an important restriction: **it must not contain accounts from groups 7 (Revenues) and 8 (Expenses)**.
### Why exclude groups 7 and 8?
The 4016 document represents the entity's accounting position **after the income statement closing**. At this point, Revenue accounts (group 7) and Expense accounts (group 8) have already been closed and their balances transferred to Equity.
### Template example
```xml tpl theme={null}
{%- for op_route in midaz_transaction.operation_route %}
{%- if op_route.code and op_route.code|slice:":1" != "7" and op_route.code|slice:":1" != "8" %}
{%- endif %}
{%- endfor %}
```
### Difference from 4010
The only difference in the template is the additional condition in the `if`:
| Template | Condition |
| -------- | -------------------------------------------------------------------------------------------- |
| **4010** | `if op_route.code` |
| **4016** | `if op_route.code and op_route.code\|slice:":1" != "7" and op_route.code\|slice:":1" != "8"` |
### How the exclusion works
The `slice:":1"` filter extracts the first character of the COSIF code:
| COSIF Code | First Digit | Group | Included in 4016? |
| ---------- | ----------- | ----------- | ----------------- |
| 1000000009 | 1 | Assets | Yes |
| 2100000003 | 2 | Liabilities | Yes |
| 7100000001 | 7 | Revenues | **No** |
| 8200000005 | 8 | Expenses | **No** |
## Generating the report with date filter
***
### Request example for 4010
To generate the CADOC 4010 for a specific month, send a `POST /v1/reports` request with the following body:
```json JSON theme={null}
{
"templateId": "CADOC_4010_TEMPLATE_ID",
"filters": {
"midaz_transaction": {
"operation": {
"created_at": {
"between": ["2025-11-01T00:00:00Z", "2025-11-30T23:59:59Z"]
}
}
}
}
}
```
### Field explanation
| Field | Description |
| ------------------------------------- | ----------------------------------------------------------------- |
| `templateId` | Identifier of the CADOC 4010 template registered in Reporter |
| `filters.midaz_transaction.operation` | Indicates the filter will be applied to the operations collection |
| `created_at.between` | Filters operations created within the specified interval |
| `between[0]` | First day of the month at 00:00:00 UTC |
| `between[1]` | Last day of the month at 23:59:59 UTC |
**Date format**: ISO 8601 with UTC timezone (`Z`).
Adjust the last day according to the month (28, 29, 30, or 31 days).
### Request example for 4016
To generate the CADOC 4016 for the first semester:
```json JSON theme={null}
{
"templateId": "CADOC_4016_TEMPLATE_ID",
"filters": {
"midaz_transaction": {
"operation": {
"created_at": {
"between": ["2025-01-01T00:00:00Z", "2025-06-30T23:59:59Z"]
}
}
}
}
}
```
For the second semester, adjust the dates to July through December.
## Template evolution and balance extraction
***
We are working on evolving our main template to support balance aggregation in compliance with BACEN CADOC 4010/4016, which requires the final balance from the last business day of the month.
While this feature is being developed, we provide an alternative version for extracting these balances using the following template.
### Extraction template
This auxiliary template is designed to correctly calculate balances, ensuring compliance of your reports through the following steps:
1. Group operations by account
2. Sort entries by date and time
3. Select the last record from each account to obtain the final balance
4. Sum final balances by COSIF code
With this option, you can extract the information required for CADOC 4010/4016 and transfer it to the layout format required by BACEN. This template can help if you already have a provider that builds CADOC files.
### Extraction template example
```xml tpl theme={null}
account_id;account_alias;codigo_cosif;created_at;saldo_disponivel
{%- for op_route in midaz_transaction.operation_route %}
{%- if op_route.code %}
{%- for op in filter(midaz_transaction.operation, "route", op_route.id) %}
{{ op.account_id }};{{ op.account_alias }};{{ op_route.code }};{{ op.created_at }};{{ op.available_balance_after }}
{%- endfor %}
{%- endif %}
{%- endfor %}
```
### Extraction output
The template exports all operations in CSV format, containing:
* Account identifier
* Account alias
* COSIF code
* Operation date and time
* Available balance
You can import this CSV into a spreadsheet or your existing reconciliation system to process the final balances.
## Comparison with CADOC 4111
***
CADOC 4010 and CADOC 4111 share the same template structure, with differences only in the parameters:
| Aspect | CADOC 4111 | CADOC 4010 |
| ----------------- | --------------- | --------------------- |
| Document | Daily Balances | Monthly Balance Sheet |
| Periodicity | Daily | Monthly |
| `codigoDocumento` | `"4111"` | `"4010"` |
| `dataBase` | YYYY-MM | YYYY-MM |
| Date filter | 1 day | 1 month |
| Expected balance | Last of the day | Last of the month |
**Identical template**: The logic for iterating over routes and aggregating balances is the same.
## Best practices for building templates
***
### Variable naming
Use descriptive and unique names for iteration variables, avoiding conflicts with entity field names.
### Field validation
Always check if optional fields have values before using them. Empty fields can generate unwanted lines in the report.
### Date format
BACEN requires dates in YYYY-MM format for 4010. Make sure to configure the format correctly in the template.
### CNPJ handling
The CNPJ must be presented with only the first 8 digits, without formatting (dots, slashes, or dashes).
### COSIF account code
The COSIF chart of accounts is structured with 6 hierarchical levels and a check digit. The `codigoConta` field must have 10 numeric digits, without dots or dashes.
## Component summary
***
| Component | Data Source | Usage in Template |
| ---------- | ----------------------------- | ---------------------- |
| CNPJ | Organization (Onboarding) | Document header |
| COSIF Code | Operation Route (Transaction) | Account identifier |
| Balance | Operation (Transaction) | Value to be aggregated |
| Base Date | Current date function | Document header |
Always validate the rendered XML against BACEN's schema before submitting. The structure alone isn't enough — the data must reflect your institution's actual ledger.
# CADOC 4111
Source: https://docs.lerian.studio/en/reporter/cadoc-4111
Generate the CADOC 4111 XML report for BACEN with Reporter — daily account balances grouped by COSIF code, in the required regulatory layout.
Reporter lets you generate XML-based reports that follow the official CADOC structure, exactly as required by the Brazilian Central Bank (BACEN).
This guide walks you through the structure and logic used to generate the **CADOC 4111** report in XML.
These reports follow the **COSIF** standard and must match the XML structure defined by BACEN. You can adapt the logic to your own data model, but the XML format must be respected.
## What is CADOC 4111?
***
The **CADOC 4111** is a regulatory document required by the Brazilian Central Bank (BACEN) that reports daily balances of accounting accounts grouped by COSIF code (Chart of Accounts for Institutions of the National Financial System).
### What BACEN expects to receive
* Consolidated balances by COSIF code
* Report base date
* Institution's CNPJ (first 8 digits)
* Submission type (`I` = Inclusion, `S` = Replacement)
### Submission requirements
| Document | Deadline | STA Code |
| -------- | ------------------------------------------------------- | -------- |
| **4111** | Day following the reference date (or next business day) | ACOS011 |
The STA code identifies the document type in BACEN's transmission system. Use `ACOS011` when submitting CADOC 4111.
## Understanding the data structure
***
Before building the template, it's essential to understand how data is organized in the Midaz ledger.
### Operation routes
Operation routes function as accounting classifiers. Each route has:
* **Unique identifier (`id`)**: Used internally to relate operations
* **COSIF code (`code`)**: The 10-digit accounting code that will be reported to BACEN
When an operation is recorded in Midaz, it's associated with a route. This route carries the corresponding COSIF code.
Think of routes as "accounting labels" attached to each operation, indicating which chart of accounts category that movement should be classified under.
### Operations
Operations represent financial movements in the ledger. Each operation contains:
* **Associated account (`account_id`)**: Which account was affected
* **Route (`route`)**: ID of the applied accounting route/classification
* **Balance after operation (`available_balance_after`)**: The account balance immediately after this operation
* **Date and time (`created_at`)**: When the operation occurred
The balance after operation field represents the accumulated account balance at that moment, not the value of the operation itself.
### Relationship between routes and operations
Each route can have multiple operations linked to it throughout the day.
## CADOC structure
***
### Base format
The CADOC report **must** be an XML file and **must** follow the structure defined by **BACEN**:
```xml XML theme={null}
```
### Mandatory fields
These fields are required and must be included:
**``**
Always starts the file. It defines the XML version and encoding so the system knows how to read the content.
**`` tag**
Wraps the entire CADOC structure and includes:
| Field | Description | Format |
| ----------------- | ---------------------- | -------------------- |
| `codigoDocumento` | Fixed identifier | `"4111"` |
| `cnpj` | First 8 digits of CNPJ | Numeric, 8 positions |
| `dataBase` | Reference date | YYYY-MM |
| `tipoRemessa` | Submission type | `"I"` or `"S"` |
If your first submission was rejected due to errors, you still need to use `"I"` on your next attempt. Only use `"S"` for replacing previously approved data.
**`` tag**
Groups all account entries for the reporting period.
**`` tag**
* `codigoConta`: COSIF code from the operation route (10 numeric digits)
* `saldoDia`: Consolidated balance in decimal format (two decimal places)
## Template construction logic
***
### General structure
The template follows an aggregation logic at two levels:
1. **First level**: Iterate through all available operation routes
2. **Second level**: For each route, sum the balances of linked operations
### Document header
The XML header must contain:
* **Document code**: Fixed identifier `4111`
* **CNPJ**: Extracted from organization data, limited to first 8 digits
* **Base date**: Report generation date in `YYYY-MM` format
* **Submission type**: `I` for inclusion
Organization data is obtained from the Midaz onboarding data source, specifically from the organization entity.
### Iterating over routes
The template must iterate through all registered operation routes. For each route:
1. **Check for COSIF code**: Only routes with a valid COSIF code should generate lines in the report
2. **Filter operations**: Select only operations belonging to that specific route
3. **Calculate balance**: Sum the balances of filtered operations
Not all routes have a COSIF code filled in. Routes without a code are used for internal controls and should not appear in the regulatory report.
### Filtering operations by route
The association between operations and routes is made through the route identifier. The template uses this identifier to:
1. Access a specific route
2. Find all operations that reference this route
3. Process only these operations in the aggregation
When building the template, avoid using the same name for the iteration variable and the filter field, as this can cause conflicts in template interpretation.
### Summing balances
For each set of operations from a route, the template sums the available balances. The field used is the available balance after each operation.
The aggregation function iterates through all operations that meet the filter criteria (same route) and accumulates the balance field values.
## Using Reporter
***
Here's the complete template for generating CADOC 4111 in Reporter:
```tpl Template theme={null}
{%- for op_route in midaz_transaction.operation_route %}
{%- if op_route.code %}
{%- endif %}
{%- endfor %}
```
## Code breakdown
***
### Line 1 - XML declaration
```xml theme={null}
```
Standard XML header with UTF-8 encoding.
### Line 2 - Root element ``
```tpl theme={null}
```
* `codigoDocumento="4111"`: Fixed document type identifier
* `cnpj`: Accesses the organization's legal document and extracts the first 8 characters using the `slice` filter
* `dataBase`: Generates the current date in BACEN's required format using the `date_time` tag
* `tipoRemessa="I"`: Indicates data inclusion
### Line 4 - Start of `for` loop
```tpl theme={null}
{%- for op_route in midaz_transaction.operation_route %}
```
* `op_route`: Variable that receives each route during iteration (name chosen to avoid conflict with the `route` field in operations)
* `midaz_transaction.operation_route`: Collection of all operation routes
### Line 5 - `if` condition
```tpl theme={null}
{%- if op_route.code %}
```
Checks if the route has a COSIF code filled in. Routes without a code are ignored.
### Line 6 - `` element
```tpl theme={null}
```
* `codigoConta`: Displays the current route's COSIF code
* `saldoDia`: Uses the `sum_by` tag to sum balances, filtering only operations whose route matches the current route identifier (`op_route.id`)
### Lines 7-8 - Closing blocks
```tpl theme={null}
{%- endif %}
{%- endfor %}
```
Close the condition and loop respectively.
### Tags and filters reference
| Element | Type | Function |
| ---------------------- | ---------- | --------------------------------------------- |
| `{{ variable }}` | Expression | Displays a variable's value |
| `{% tag %}` | Tag | Executes logic (loop, condition, aggregation) |
| `\|slice:":8"` | Filter | Extracts the first 8 characters |
| `for ... in` | Tag | Iterates through a collection |
| `if` | Tag | Execution condition |
| `sum_by ... by ... if` | Tag | Sums values with conditional filter |
| `date_time` | Tag | Generates formatted date |
## Date filter considerations
***
### Request example with date filter
To generate CADOC 4111 for a specific day, send a `POST /v1/reports` request with the following body:
```json Request theme={null}
{
"templateId": "CADOC_4111_TEMPLATE_ID",
"filters": {
"midaz_transaction": {
"operation": {
"created_at": {
"between": ["2025-11-11T00:00:00Z", "2025-11-11T23:59:59Z"]
}
}
}
}
}
```
### Field explanation
| Field | Description |
| ------------------------------------- | ----------------------------------------------------------------- |
| `templateId` | CADOC 4111 template identifier registered in Reporter |
| `filters.midaz_transaction.operation` | Indicates the filter will be applied to the operations collection |
| `created_at.between` | Filters operations created within the specified interval |
| `between[0]` | Start date and time (midnight of desired day) |
| `between[1]` | End date and time (last second of desired day) |
Dates must be in ISO 8601 format with UTC timezone (`Z`).
## Template evolution and balance extraction
***
We are working on evolving our main template to support balance aggregation in compliance with BACEN CADOC 4111, which requires the final balance from the last business day.
While this feature is being developed, we provide an alternative version for extracting these balances using the following template.
### Extraction template
This auxiliary template is designed to correctly calculate balances, ensuring compliance of your reports through the following steps:
1. Group operations by account
2. Sort entries by date and time
3. Select the last record from each account to obtain the final balance
4. Sum final balances by COSIF code
With this option, you can extract the information required for CADOC 4111 and transfer it to the layout format required by BACEN. This template can help if you already have a provider that builds CADOC files.
### Extraction template example
```tpl CSV Extraction theme={null}
account_id;account_alias;codigo_cosif;created_at;saldo_disponivel
{%- for op_route in midaz_transaction.operation_route %}
{%- if op_route.code %}
{%- for op in filter(midaz_transaction.operation, "route", op_route.id) %}
{{ op.account_id }};{{ op.account_alias }};{{ op_route.code }};{{ op.created_at }};{{ op.available_balance_after }}
{%- endfor %}
{%- endif %}
{%- endfor %}
```
### Extraction output
The template exports all operations in CSV format, containing:
* Account identifier
* Account alias
* COSIF code
* Operation date and time
* Available balance
You can import this CSV into a spreadsheet or your existing reconciliation system to process the final balances.
## Best practices for template construction
***
### Variable naming
Use descriptive and unique names for iteration variables, avoiding conflicts with entity field names.
### Field validation
Always check if optional fields have values before using them. Empty fields can generate unwanted lines in the report.
### Date format
BACEN requires dates in `YYYY-MM` format. Make sure to configure the format correctly in the template.
### CNPJ handling
The CNPJ must be presented with only the first 8 digits, without formatting (dots, slashes, or dashes).
## Component summary
***
| Component | Data Source | Usage in Template |
| ---------- | ----------------------------- | ---------------------- |
| CNPJ | Organization (Onboarding) | Document header |
| COSIF Code | Operation Route (Transaction) | Account identifier |
| Balance | Operation (Transaction) | Value to be aggregated |
| Base Date | Current date function | Document header |
Always validate the rendered XML against BACEN's schema before submitting. The structure alone isn't enough — the data must reflect your institution's actual ledger.
# CCS Reports
Source: https://docs.lerian.studio/en/reporter/ccs-reports
Build CCS XML files (ACCS001, ACCS005, ACCS010) for BACEN by combining Lerian CRM and Midaz data through Reporter templates and scheduled generation.
Reporter enables you to generate XML-based CCS (Cadastro de Clientes do Sistema Financeiro Nacional) files as defined by the Central Bank of Brazil (BACEN).
This guide shows how to create automated CCS reports using Reporter — including ACCS001, ACCS005, and ACCS010 — by combining CRM data (holders and aliases) with Midaz structures.
CCS reports must strictly follow the layouts defined by BACEN. Reporter lets you automate the XML generation, but you remain responsible for validating the output and ensuring compliance with regulatory requirements.
## What is CCS?
***
The **CCS (Cadastro de Clientes do Sistema Financeiro Nacional)** is a registry that maps **relationships between customers and financial institutions**.
Its purpose is to allow regulators and other authorized entities to identify **where a customer holds financial relationships**.
Institutions must submit CCS reports in XML format, following BACEN’s ACCS schemas. Each file type has a specific function — from daily updates to relationship transfers.
Reporter makes this process automated and standardized, ensuring that CCS submissions are both **technically valid** and **business-aligned**.
## ACCS001 — Daily update file
***
### What it is
The ACCS001 is the **daily update file**. It reports inclusions, changes, or exclusions of customer relationships (individual or corporate) with the institution. This is the most frequent CCS file, keeping the registry in sync with daily operations.
### Full template
```
1234567800000000ACCS00112233444
{%- for alias in plugin_crm.aliases -%}
{%- for holder in plugin_crm.holders -%}
{%- if holder.document == alias.document %}
IN{%- if holder.type == "NATURAL_PERSON" -%}F{%- else -%}J{%- endif -%}{{holder.document}}{{alias.banking_details.opening_date}}
{%- endif -%}
{%- endfor %}
{%- endfor %}
{% count_by plugin_crm.aliases %}{% date_time "YYYY-MM-dd" %}
```
### Applied logic
Nested loops cross-reference plugin\_crm.aliases (accounts) with plugin\_crm.holders (holders), enabling accurate association between account data and their respective owners.
* The `` field is conditional:
* F for **natural persons**
* J for **legal entities**
* **Fixed values in this example:**
* `` = I (Inclusion)
Although set to I in this scenario, this field must reflect the actual operation being reported.\
Valid values according to BACEN are:
* I = **Inclusion** (new accounts in the institution's database)
* A = **Update** (changes to existing account information)
* E = **Exclusion** (the original record must be disregarded)
* `` = N (Normal)
Similarly, this field should represent the correct qualifier for the account.\
Valid values include:
* N = **Normal**
* P = **Popular**
* C = **Conglomerate**
* **Functions used:**
* count\_by plugin\_crm.aliases counts alias records per account.
* date\_time "YYYY-MM-dd" dynamically inserts the current date in the specified format.
### Rendered example
```
1234567800000000ACCS00112233444INF123456789002025-02-0112025-02-01
```
## ACCS005 — Relationship detail file
***
### What it is
The ACCS005 is generated when BACEN requests **detailed information on client relationships**. It lists all the links between a customer and financial assets (BDVs — Bens, Direitos e Valores), and may include **related persons** such as partners or legal representatives.
### Full template
```
1234567800000001ACCS005{% date_time "YYYYMMddHHmm" %}{{plugin_crm.holders.0.document|slice:':8'}}000000
{%- for account in midaz_onboarding.account %}
{%- for alias in plugin_crm.aliases %}
{%- if alias.account_id == account.id %}
{%- for holder in plugin_crm.holders %}
{%- if holder.document == alias.document %}
{{holder.document|slice:"0:8"}}{% if account.type == "deposit" %}1{% elif account.type == "savings" %}2{% elif account.type == "payment" %}3{% else %}6{% endif %}
{%- if alias.banking_details.branch %}
{{alias.banking_details.branch}}
{%- endif %}
{%- if alias.banking_details.type == "CACC" %}
{{alias.banking_details.account}}
{%- elif alias.banking_details.type == "payment" %}
{{alias.banking_details.account}}
{%- endif %}
1{{holder.name}}{{alias.banking_details.opening_date}}
{%- if holder.type == "NATURAL_PERSON" and holder.natural_person %}
3{{holder.document}}{{holder.natural_person.mother_name}}{{alias.banking_details.opening_date}}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endfor %}
{%- endfor %}
{% date_time "YYYY-MM-dd" %}
```
### Applied logic
The logic applies nested loops to iterate through **accounts**, **aliases**, and **holders**, enabling structured extraction of all relevant entities.
* The `` field uses conditional logic to determine the account type:
* 1 = deposit
* 2 = savings
* 3 = payment
* 6 = others
* For CNPJ extraction, slice:"0:8" is applied to retrieve only the first 8 digits (base of the CNPJ).
* The `` tag is rendered only when the account holder is a natural person **and** has associated individuals (e.g., an attorney or legal representative).
To illustrate this, the example bellow includes the **mother’s name** as a placeholder for a vinculated person — not because that tag refers to the mother per se, but to demonstrate the feature in action.
* The `date_time` function dynamically generates timestamps at runtime, ensuring the report reflects the current execution context.
### Rendered example
```
1234567800000001ACCS0052025020112301234567800000012345678100112345-61João Silva2020-01-01312345678900Maria Silva2020-01-012025-02-01
```
## ACCS010 — Relationship transfer file
***
### What it is
The ACCS010 is generated when **client relationships are transferred** between institutions — common in portfolio sales or mergers. It lists the relationships being transferred and identifies the **new responsible institution**.
### Full template
```
1234567800000000ACCS010{% date_time "YYYYMMdd" %}000112345679
{%- for alias in plugin_crm.aliases %}
{%- for holder in plugin_crm.holders %}
{%- if alias.holder_id == holder._id and (holder.type == "LEGAL_PERSON" or holder.type == "NATURAL_PERSON") %}
{% if holder.type == "NATURAL_PERSON" %}F{% else %}J{% endif %}{{holder.document}}{{alias.banking_details.opening_date}}
{%- endif %}
{%- endfor %}
{%- endfor %}
{% count_by plugin_crm.aliases %}
```
### Applied logic
* **Dual loops** iterate through aliases and holders.
* `` defines whether the holder is natural (**F**) or legal (**J**).
* `` identifies the **new responsible institution**.
* `` uses count\_by to ensure the number of reported relationships matches the dataset.
* Dates (`` and ``) track relationship validity.
### Rendered example
```
1234567800000000ACCS0102025020100018765432112345678F123456789002019-05-101
```
While Reporter provides the technical automation, institutions must ensure the **accuracy and consistency of the data** submitted to BACEN. Always validate rendered XML against the official schema before submission, and involve accounting or compliance teams in the review process.
# Connect Reporter to Midaz
Source: https://docs.lerian.studio/en/reporter/connecting-reporter-to-midaz
Connect Reporter to Midaz through a named data source, protect the source database with least-privilege credentials, and verify the connection end to end.
Reporter reads Midaz data through a named **data source**. A report template references that name, and Reporter turns its placeholders and filters into queries against the source database.
For example, a template can reference an account from the `midaz_onboarding` data source:
```text theme={null}
{{ midaz_onboarding.account.name }}
```
Reporter builds `SELECT` queries for PostgreSQL sources and `Find` operations for MongoDB sources. It then renders the returned data through the template. Reporter does not write back to the source databases or persist report results in Midaz.
## Before you start
***
You need:
* Network access from Reporter to the Midaz database.
* A database user with read-only permissions.
* Preferably, a read replica so report workloads do not compete with ledger workloads. The reference setup uses the host `midaz-postgres-replica` as an example.
* The database, schemas, and tables that your templates need.
Reporter generates read operations, but this behavior does not enforce database permissions. Use read-only database credentials. Do not give Reporter a user that can insert, update, delete, or change schemas.
## Choose who manages the connection
***
Your deployment model determines who configures the data source:
* **Managed (Lerian-hosted):** the connection to your Lerian product data is set up during onboarding. Confirm the data source name and availability with your Lerian contact before you build templates.
* **Self-hosted:** your operator configures the connection at deployment time and owns its network access, credentials, certificates, and database permissions.
In both models, report authors reference the configured data source by its `configName`.
## Configure a named data source
***
In a self-hosted deployment, define the connection with `DATASOURCE__*` environment variables. Set `DATASOURCE__CONFIG_NAME` to the stable name that templates and filters will use.
For example, if the `configName` is `midaz_onboarding`, templates reference `midaz_onboarding` even if the database host or credentials change later.
For PostgreSQL sources that expose more than one schema, derive the schema variable from the `configName`. A `configName` of `external_db` uses:
```env theme={null}
DATASOURCE_EXTERNAL_DB_SCHEMAS=sales,inventory,reporting
```
See [Configure external data sources](/en/reporter/using-reporter#configure-external-data-sources) for the connection variables and complete examples.
## Protect the Midaz database
***
Use a dedicated database role that can connect and select only the required tables and schemas. Point Reporter to a read replica when your topology supports one. This keeps report queries away from the primary database, but the replica is not a substitute for read-only credentials: apply both controls.
Rotate the credential through your normal secret-management process. Keep the `configName` stable so existing templates continue to resolve the same data source.
## Verify the connection
***
1. [List data sources](/en/reference/reporter/list-data-sources) and confirm that the expected `configName` appears.
2. Copy the data source ID from the response.
3. [Retrieve the data source](/en/reference/reporter/retrieve-data-source) and confirm that Reporter discovers the expected schemas, tables, and fields.
4. Create a small template that references one known field, then generate a report to verify the complete query and rendering path.
The connection is ready when the named source is available, its discovered structure is correct, and the test report returns the expected value.
## Troubleshooting
***
### The data source is unavailable
Check network routing, DNS, TLS settings, credentials, database permissions, and replica health. During template validation, Reporter can return a `DATA_SOURCE_UNAVAILABLE` warning instead of blocking template creation. Fix the connection before relying on the report output.
### The source name is ambiguous
Use one descriptive `configName` per database role, such as `midaz_onboarding` and `midaz_transaction`. Avoid generic names such as `database` or `postgres`, especially when different sources contain tables with the same names. Update the template to reference the intended source explicitly.
## Next steps
***
* [Create your first report](/en/reporter/reporter-quick-start)
* [Build templates and filters](/en/reporter/using-reporter)
* [See Reporter in a complete core banking architecture](/en/building-a-complete-core-banking)
# Add a Data Source
Source: https://docs.lerian.studio/en/reporter/console/adding-a-data-source
Configure an external database connection when Reporter needs a data source that is not already available in the Lerian Console for report generation.
Use this page to create a Data Source when Reporter needs to read from an external database or from a connection that is not already configured. If the report uses internal Lerian product data, such as Midaz or CRM, the Data Source may already exist.
## Objective
***
Configure an external connection that Templates and Reports can use.
For example, create a Data Source named `production-postgres` that points to an external reporting database, or `compliance-readonly` for a read-only database user used by reporting operators.
## When to use
***
Use this task when:
* the required Data Source is not already available in Reporter;
* a new database must feed reports;
* an existing report process needs a separate read-only connection;
* operators need schemas and fields available in Template Builder or report filters.
Before adding one, check whether the required internal Data Source already exists. Creating a duplicate external connection is unnecessary when Reporter already has the source configured.
Do not use this task to fix an existing connection. Use [Edit a Data Source](/en/reporter/console/editing-a-data-source) and [Test a Data Source Connection](/en/reporter/console/testing-a-data-source).
## Before you start
***
Have these details ready:
* host, port, and database name;
* username and password;
* schema names, when the database uses schemas;
* SSL requirements and CA certificate, when required by infrastructure;
* a clear connection name, such as `production-postgres` or `compliance-readonly`.
Use credentials intended for reporting access. A Data Source gives Reporter access to read data used in Templates and Reports.
## Step by step
***
Go to **Data Sources** and click **Add Data Source**.
In **Add Data Source**, enter **Connection Name**, **Database Type**, **Host**, **Port**, **Database Name**, **Username**, and **Password**.
For database types where the Console shows **Schemas** (currently PostgreSQL in the selector), add the schemas that Reporter should inspect. Type a schema name and click **Add**, or press **Enter**.
Expand **SSL Configuration**, turn on **Enable SSL**, select **SSL Mode**, and paste the **CA Certificate** if your infrastructure requires it.
Expand **Metadata** to add key-value fields that help operators classify the connection.
Click **Create**.
The new Data Source appears with **Disconnected** status. Run **Test Connection** before using it in Templates or Reports.
## Field guide
***
| Field | What to enter | Example |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| **Connection Name** | Required operator-facing name. Technical field: `configName`. Console validation: 3 to 100 characters. | `production-postgres` |
| **Database Type** | Required database engine. Console currently offers `POSTGRESQL` and `MONGODB`. Technical field: `type`. | `POSTGRESQL` |
| **Host** | Required database server address, not an application URL. Technical field: `host`. | `db.example.com` |
| **Port** | Required database port. Technical field: `port`; valid range is 1 to 65535. | `5432` |
| **Database Name** | Required database name Reporter will query. Technical field: `databaseName`. | `ledger` |
| **Username** | Required database user. Technical field: `username`. | `reporter_readonly` |
| **Password** | Required secret when creating a Data Source. Technical field: `password`. | Stored through the form |
| **Schemas** | Optional schema list for supported database types. Stored as `metadata.schemas`; entries are trimmed, lowercased, and deduplicated. | `public`, `pix`, `transactions` |
| **Enable SSL** | Optional SSL toggle. When enabled, the connection payload includes `ssl`. | Enabled |
| **SSL Mode** | Required when SSL is enabled. Use `disable`, `require`, `verify-ca`, or `verify-full`. Technical field: `ssl.mode`. | `verify-ca` |
| **CA Certificate** | Required for `verify-ca` and `verify-full`. Technical field: `ssl.ca`. | PEM CA certificate |
| **Metadata** key/value fields | Optional operator classification data. Only rows with a nonblank key are submitted. Technical field: `metadata`. | `environment = production` |
## Expected result
***
The new external Data Source appears on the **Data Sources** page with **Disconnected** status until tested. After a successful test, operators can use its fields in Templates and Reports.
## Common errors and care points
***
**Host** should be the database server address, such as `db.example.com`, not the URL of an application or admin panel.
If the database uses schemas and the required schema is not listed, operators may not see the expected tables and fields.
If infrastructure requires SSL or a CA certificate, the connection can fail until **SSL Configuration** matches that requirement.
Names like `db1` are hard to use safely. Prefer names that show environment and purpose, such as `production-compliance-readonly`.
## Next steps
***
* Use [Test a Data Source Connection](/en/reporter/console/testing-a-data-source) immediately after creation.
* Use [Template Builder](/en/reporter/console/template-builder) to use fields from the Data Source in a Template.
* Use [Generate a Report](/en/reporter/console/generating-a-report) after the Template and Data Source are ready.
[Create connection endpoint](/en/reference/reporter/connections/create-connection)
# Add a Template
Source: https://docs.lerian.studio/en/reporter/console/adding-template
Create a Template in Reporter using Template Builder or by uploading a prepared .tpl file, then map its data sources for regulatory report generation.
Use this page to create the Template that Reporter will use to generate a Report. A Template is the reusable structure of the output file: it defines the layout, fields, conditions, loops, calculations, and final format.
## Objective
***
Add a Template that operators can select when generating Reports.
For example, a monthly tax Template can define a CSV header, one row per transaction, and a total amount at the end of the file. A regulatory XML Template can define the required XML tags and fill them with values from a Data Source.
## When to use
***
Use this task when:
* you are setting up the first Template for a report whose Data Source already exists;
* a new report obligation requires a new output structure;
* your team already has a reviewed `.tpl` file and needs to make it available in the Console;
* an operator needs to build the Template visually instead of editing `.tpl` code.
Do not use this task to fix a Template that already exists. Use [Update a Template](/en/reporter/console/updating-a-template) instead.
## Before you start
***
Confirm:
* the Data Source that will feed the Template is available when the Template needs database fields. It may be an internal source already configured for the environment or an external source added from **Data Sources**;
* the expected output format is known: CSV, XML, HTML, TXT, or PDF;
* the Template will be created in **Template Builder** or uploaded as a `.tpl` file;
* the report owner has confirmed the fields that must appear in the output.
A Template is not a generated Report. It is the reusable definition that Reporter applies later during report generation.
## Option 1: Build in Template Builder
***
Use Template Builder when an operator needs to assemble the Template visually from blocks.
Go to the **Templates** page and click **Template Builder**.
Use a name operators can recognize later, such as `Monthly tax CSV` or `Daily transaction PDF`.
Select the format that the generated Report must produce: XML, HTML, CSV, TXT, or PDF.
Add blocks such as **Text**, **Variable**, **Loop**, **Conditional**, or **Aggregation**. In practice, blocks represent the pieces of the final file: fixed text, database values, repeated rows, conditional sections, or totals.
Use the Data Sources sidebar to select fields from the configured database connection. For example, use a **Variable** block to place `transactions.amount` in the output.
Click **Save** to validate the blocks and save the generated `.tpl` file.
## Option 2: Upload a `.tpl` file
***
Use upload when a technical team already maintains the Template outside the Console.
Go to the **Templates** page and click **New Template**.
In the **File** step, drag the `.tpl` file to **Drag file here**, or click **to select** to browse your computer.
In the **Config** step, fill in **Template Name** and **Output Format**.
In the **Review** step, confirm the file, name, and output format. Use the edit buttons if any field is wrong.
Click **Save Template**.
**API equivalent**: [Upload template endpoint](/en/reference/reporter/upload-template)
## Field guide
***
### Upload wizard fields
| Field | What to enter | Example |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| **Template File (.tpl)** | Required `.tpl` file used as the Template source. It must be larger than 0 bytes and no larger than 5 MB. API field: `template`. | `monthly-tax.tpl` |
| **Template Name** | Required operator-facing name. It maps to `name` in the Console and to `description` in the Reporter API. | `Monthly tax CSV` |
| **Output Format** | Required output type. Use one of `csv`, `xml`, `html`, `txt`, or `pdf`. | `csv` |
### Template Builder fields
| Field | What to enter | Example |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **Template name** | Required name in the builder header. It becomes the saved Template name and generated `.tpl` filename prefix. | `Daily transaction PDF` |
| Block toolbar | Add at least one block before saving. Blocks become the generated `.tpl` code. | **Text** header + **Loop** over transactions + **Aggregation** total |
| Data Sources sidebar | Use configured Data Sources to insert fields into **Variable** blocks. Technical fields include `dataSource`, `table`, and `field`. | `production-postgres.transactions.amount` |
## Expected result
***
After saving, the Template appears on the **Templates** page and can be selected in the **Generate Report** wizard.
## Common errors and care points
***
Upload only accepts `.tpl` files. If the Template is still being designed, use **Template Builder** first or prepare the `.tpl` file outside the Console.
The output format controls what kind of file operators will download after generating a Report. Confirm the required format before saving.
Templates can reference fields from Data Sources. If the required source is missing or untested, operators may not know which fields are safe to use.
## Next steps
***
* Use [Generate a Report](/en/reporter/console/generating-a-report) to create a Report from the Template.
* Use [Update a Template](/en/reporter/console/updating-a-template) if the structure, file, name, or output format needs correction.
* Use [Template Builder](/en/reporter/console/template-builder) when operators need to maintain the Template visually.
# BC Correios in Lerian Console
Source: https://docs.lerian.studio/en/reporter/console/bc-correios
Operate the BC Correios provider in the Reporter Module — receive, send, search, and audit regulatory mail from the Banco Central do Brasil (BCB).
**BC Correios** is a provider in the [Reporter Module](/en/reporter/console/reporter-module) that automates regulatory communication with the **Banco Central do Brasil (BCB)** through its electronic mail system, *BC Correio*.
Institutions supervised by the BCB must monitor this mailbox daily for official directives, circulars, and resolutions. BC Correios brings that workflow into Lerian Console: it receives incoming mail, lets you transmit replies and new communications to the BCB, and keeps an immutable record for compliance evidence — without anyone needing to handle the BCB's underlying SOAP/XML protocol.
BC Correios is only available if your organization has acquired the provider and it is enabled for your environment. If you don't see it, [get in touch with our team](https://lerian.studio/contact).
## What BC Correios covers
***
Once enabled, BC Correios adds a section to the Reporter sidebar and covers the following areas:
* **Dashboard** — KPIs for received and pending mail, a chart of mail categories, sync status, and recent activity.
* **Inbox** — searchable, filterable list of incoming mail, with a detail view that shows attachments and an optional **AI analysis** panel.
* **New message** — send a new communication to the BCB (destination, subject, message, and keywords) via the BCB `TransmitirCorreio` operation.
* **Sent** — communications transmitted to the BCB.
* **Cancelled** — mail in a cancelled state. This folder is **read-only** because the BCB reports the state upstream.
* **Archived** — mail you have archived locally for organization.
* **Audit Trail** — an immutable audit trail of events, with CSV export for compliance evidence.
* **Settings** — BCB credentials, sync interval, AI provider, webhook, and retention configuration.
Archiving and restoring are **local-only** organizational actions — they update the BC Correios database and never change anything on the BCB side. Regulatory mail is retained to meet the minimum retention requirement; there is no user-facing delete.
## Mail types
***
BC Correios categorizes incoming mail to help you triage regulatory communications:
| Type | Meaning |
| ------------- | ----------------------------------------------------------- |
| Circular | A BCB circular. |
| Resolution | A BCB resolution. |
| Communication | A general BCB communication. |
| Other | Any communication that does not match the categories above. |
## How availability is controlled
***
BC Correios has two levels of activation:
### 1. Infrastructure flag (set by your DevOps/deploy team)
The flag `NEXT_PUBLIC_BC_CORREIOS_ENABLED` controls whether BC Correios is visible at all in the Console. It is **disabled by default** and must be set at the Console container level — via Helm values or environment variables — before anything appears in the UI.
This is a **deploy-time setting**, not something changed inside the Console itself. See [Lerian Console via Helm](/en/platform/helm/console/console-helm) for where to set it.
Because the flag is read at runtime, toggling it requires restarting the Console container — no rebuild needed. When the flag is off, all BC Correios pages are hidden.
### 2. Organization toggle (set by a Console admin)
Once the infrastructure flag is `true`, a **BC Correios card** appears in **Reporter → Settings → Plugins**. An administrator can use the toggle on that card to enable or disable BC Correios for the organization without touching the deployment.
### Where BC Correios appears
When both levels are active, BC Correios shows up as a collapsible section under **PLUGINS** in the Reporter sidebar. The sidebar labels the items **Dashboard**, **Caixa de Entrada**, **Enviados**, **Arquivados**, **Cancelados**, **Trilha de Auditoria**, and **Configurações**.
If you expect BC Correios but do not see it in the Reporter sidebar, check in order: (1) confirm the infrastructure flag is `true` for your environment; (2) confirm the plugin is toggled on in Reporter → Settings → Plugins. If both are set and it still does not appear, contact the Lerian team.
## Console vs API
***
Use the **Console** when an operator needs to read, send, search, and audit BCB mail through the visual interface. Use the BCB integration's **API** when a system needs to automate reception or transmission programmatically. The same concepts apply in both places — the Console abstracts the BCB's SOAP/XML mailbox behind a clean operator workflow.
# Audit Trail
Source: https://docs.lerian.studio/en/reporter/console/bc-correios-audit
Review the immutable chronological record of every action performed on BCB mail, and export it as CSV for compliance evidence and BACEN audit reviews.
The **Audit Trail** is a chronological, immutable record of every action performed on Banco Central do Brasil (BCB) mail in BC Correios. It provides traceability and regulatory compliance evidence, and it can be exported as CSV.
## Accessing the Audit Trail
***
Open **Reporter** > **BC Correios** > **Trilha de Auditoria** from the left-side menu. The screen is headed **Audit Trail**.
The Audit Trail requires the `audit:get` permission, which also covers CSV export. Without it, you see a "not authorized" page. Audit events are retained for 5 years.
## Reading the audit table
***
Each row records one event:
| Column | Description |
| :------------ | :------------------------------------------------------------------- |
| **Action** | The type of event (see the list below). |
| **User** | The user who performed the action, or `System` for automated events. |
| **Mail** | The related mail, when applicable. Click to open its detail view. |
| **Date/Time** | When the event occurred. |
### Tracked actions
| Action | When it is recorded |
| :---------------------- | :----------------------------------------- |
| `Mail received` | New mail arrives from the BCB. |
| `Viewed mail` | A mail is opened. |
| `Archived mail` | A mail is archived locally. |
| `Restored mail` | An archived mail is restored. |
| `Mail cancelled` | The BCB reports a mail as cancelled. |
| `Sent mail` | A communication is transmitted to the BCB. |
| `Downloaded attachment` | An attachment is downloaded. |
| `Performed search` | An operator runs a search. |
| `Updated settings` | Configuration is changed. |
| `Tested BCB connection` | A connection test is run. |
## Filtering events
***
Use the filter bar to focus the trail:
* **From date** and **To date** — restrict to a date range. The default range is the last 7 days.
* **All actions** — the default; pick one action type to show only that one.
* **Items per page** — control how many rows are shown.
## Exporting to CSV
***
Click **Export CSV** to download the currently filtered events. The export honors the active action and date-range filters, so you can produce a targeted compliance report.
Export after setting a date range and action filter to generate evidence for a specific event type or reporting period.
# Composing a message
Source: https://docs.lerian.studio/en/reporter/console/bc-correios-compose
Send a new communication or reply to the Banco Central do Brasil through the BCB TransmitirCorreio operation, with attachments and metadata support.
The **New message** page lets you transmit a new communication — or a reply — to the Banco Central do Brasil (BCB). Behind the scenes, BC Correios sends the message through the BCB `TransmitirCorreio` operation, so you never need to handle the underlying SOAP/XML protocol.
## Accessing New message
***
Open **New message** from the [Inbox](/en/reporter/console/bc-correios-inbox) by clicking the **New message** button, or from **Reply** on a mail detail page.
The page requires the `mails:post` permission. With read-only access, you see the form with a read-only banner and a disabled **Send** button.
## Message fields
***
| Field | Required | Description |
| :-------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Subject** | Yes | The subject line of the communication. |
| **Destination** | No | The destination unit at the BCB. Leave blank to use the tenant default. Common units are suggested as you type (for example DEINF, SECRE, GABIN, DEPIN, DESIG, DEORF), but the field accepts free text. |
| **Message** | Yes | The message body (up to 10,000 characters). A live character counter is shown. |
| **Keywords** | No | Comma-separated tags to classify the message. Maximum of 10 keywords, 100 characters each. |
## Replying to a message
***
When you start **New message** from a mail's **Reply** action, the subject is pre-filled with `Re: ` and the page shows which message you are replying to. The reply is linked to the original mail so the conversation stays traceable.
## Sending
***
1. Fill in at least the **Subject** and **Message**.
2. Optionally set a **Destination** and **Keywords**.
3. Click **Send**.
On success, BC Correios confirms the transmission and returns you to the Inbox. The sent communication then appears in the [Sent](/en/reporter/console/bc-correios-folders) folder.
If you navigate away with unsaved changes, BC Correios asks you to confirm before discarding the draft.
Every transmission is recorded in the [Audit Trail](/en/reporter/console/bc-correios-audit) as a `Sent mail` event for compliance evidence.
# Dashboard
Source: https://docs.lerian.studio/en/reporter/console/bc-correios-dashboard
Monitor incoming BCB mail at a glance — KPIs, mail categories, sync status, and recent activity in the BC Correios dashboard for regulatory oversight.
The **Dashboard** is the landing page of the **BC Correios** plugin. It gives you a single view of the mailbox health: how much mail has arrived, what still needs attention, how it breaks down by category, whether synchronization with the Banco Central do Brasil (BCB) is healthy, and what happened most recently.
## Accessing the Dashboard
***
Open **Reporter** > **BC Correios** > **Dashboard** from the left-side menu. The Dashboard is at the root of the BC Correios section.
The Dashboard requires the `dashboard:get` permission. Without it, you see a "not authorized" page instead of a partial view.
## KPI cards
***
Four cards summarize the current state of the mailbox:
| KPI | Meaning |
| :---------------- | :------------------------------------------------------------------------------------------ |
| **Total mail** | All mail received from the BCB. |
| **Unread** | Mail not yet opened. Links directly to the [Inbox](/en/reporter/console/bc-correios-inbox). |
| **Urgent** | Mail flagged as requiring prompt attention. |
| **AI categories** | Number of AI-derived categories detected across the mailbox. |
## Sync status bar
***
The sync status bar shows the connection to the BCB and lets you trigger a manual fetch:
* **Status badge** — `Pending`, `Syncing`, `Synced`, or `Sync failed`.
* **Last sync** — relative time of the most recent synchronization (or "Never synced").
* **Interval** — how often BC Correios checks for new mail (configured in [Settings](/en/reporter/console/bc-correios-settings)).
* **Credentials** — whether the BCB (Sisbacen) credentials are valid. If invalid, the badge links to Settings.
* **Sync now** — triggers an immediate synchronization without waiting for the next polling cycle.
If consecutive synchronization failures occur, a counter appears in the bar so you can react before mail is missed.
### First-time setup prompt
If credentials have not been validated and no mail has been received yet, the Dashboard shows an onboarding alert prompting you to configure your **Sisbacen credentials** in Settings before mail can start arriving.
## Activity and category charts
***
* **Recent activity** — monthly volume of received mail, so you can spot spikes or quiet periods.
* **Distribution by type** — a proportional bar with a legend showing the share of each [mail type](/en/reporter/console/bc-correios): Circular, Resolution, Communication, and Other, each with a count and percentage.
## Recent mail
***
The recent mail table lists the latest items received, with their type, subject, sender, and received time. Click a subject to open the [mail detail](/en/reporter/console/bc-correios-inbox#mail-detail) view.
The Dashboard refreshes automatically while the tab is in focus — sync status roughly every 30 seconds and KPIs every minute. You don't need to reload the page.
# Mail folders
Source: https://docs.lerian.studio/en/reporter/console/bc-correios-folders
Understand the Sent, Archived, and Cancelled folders in BC Correios, including which actions sync with the BCB and which are local-only.
Besides the [Inbox](/en/reporter/console/bc-correios-inbox), BC Correios organizes mail into three additional folders: **Sent**, **Archived**, and **Cancelled**. They share the same list layout — type, subject, sender, received date, and a per-row actions menu — but differ in which actions are available and whether those actions affect the Banco Central do Brasil (BCB).
All folder views require the `mails:get` permission, the same as the Inbox.
## Sent
***
The **Sent** folder lists communications you have transmitted to the BCB through [New message](/en/reporter/console/bc-correios-compose). It is a reference view — there are no row actions other than opening the message detail.
## Archived
***
The **Archived** folder holds mail you have archived for organization. From the row actions menu you can **Restore** an item back to its previous folder.
**Archive and Restore are local-only.** They update the BC Correios database for organizational purposes and never change anything on the BCB side. Restore requires the `mails:put` permission; without it the action is hidden.
## Cancelled
***
The **Cancelled** folder mirrors mail that the BCB has marked as cancelled. This view is **read-only**: there are no restore, archive, or delete actions.
Cancellations always originate upstream at the BCB. The BCB integration does not expose a cancellation operation, so BC Correios only reflects the cancelled state — it cannot change it. To act on a cancellation, use the BCB web interface.
## Why there's no delete
***
Regulatory mail must be retained to meet the minimum retention requirement configured in [Settings](/en/reporter/console/bc-correios-settings).
For that reason there's no user-facing delete in any folder; archiving is the closest organizational action, and it keeps the mail fully retained.
# Inbox
Source: https://docs.lerian.studio/en/reporter/console/bc-correios-inbox
Browse, search, and read regulatory mail received from the Banco Central do Brasil, including attachments and AI analysis, from the BC Correios inbox.
The **Inbox** lists mail received from the Banco Central do Brasil (BCB). From here you can search and filter incoming regulatory communications, open a message to read its full content, download attachments, and review an optional AI analysis.
## Accessing the Inbox
***
Open **Reporter** > **BC Correios** > **Caixa de Entrada** from the left-side menu. The screen is headed **Inbox**.
The Inbox requires the `mails:get` permission. The same permission also covers the mail detail view, attachments, and the implicit mark-as-read action.
## The mail list
***
Each row shows:
| Column | Description |
| :-------------- | :----------------------------------------------------------------------------------------------------------- |
| **Type** | The [mail category](/en/reporter/console/bc-correios) badge — Circular, Resolution, Communication, or Other. |
| **Subject** | The message subject. Unread mail is shown in bold. Click to open the detail view. |
| **Sender** | The originating BCB unit or address. |
| **Received at** | When the mail arrived. |
| **Actions** | The menu with **View details**. |
## Searching and filtering
***
Use the toolbar above the list to narrow the results:
* **Search mail** — free-text search by subject and content.
* **All types** — filters by mail category: Circular, Resolution, Communication, or Other.
* **All statuses** — filters by read state: Read or Unread.
* **Items per page** — control how many rows are shown.
Use the button to clear all filters and the search box at once.
The Inbox refreshes automatically about once a minute, so new mail appears without reloading.
If you have permission to send messages, a **New message** button opens the [New message](/en/reporter/console/bc-correios-compose) page.
## Mail detail
***
Click a subject to open the full message. Opening a mail automatically marks it as read.
The detail view shows:
* **Header** — the subject, mail type badge, sender, and received date.
* **Content** — the rendered message content.
* **Attachments** — a list of files with name and size; use the button to download each one.
* **AI analysis** — an optional AI-generated summary of the regulatory mail (see below).
### Actions
| Action | Behavior |
| :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Back to inbox** | Returns to the list. |
| **Archive** | Moves the mail to the [Archived](/en/reporter/console/bc-correios-folders) folder. This is a **local-only** action and requires the `mails:put` permission. |
| **Reply** | Opens [New message](/en/reporter/console/bc-correios-compose) pre-filled with the subject, to send a reply to the BCB. Requires the `mails:post` permission. |
### AI analysis panel
When an AI provider is configured in [Settings](/en/reporter/console/bc-correios-settings), BC Correios can analyze each mail and present a summary. The panel reflects the analysis state:
| State | What you see |
| :---------- | :------------------------------------------------------------------------------------- |
| `None` | AI analysis not available. |
| `Pending` | Analysis is in progress. |
| `Completed` | The AI summary, rendered from sanitized Markdown. |
| `Failed` | An error message, with a **Retry AI analysis** option for users who can send messages. |
AI analysis content is sanitized before rendering, so only safe formatting is displayed — never executable markup.
# Settings
Source: https://docs.lerian.studio/en/reporter/console/bc-correios-settings
Configure BCB credentials, polling interval, retention, webhook notifications, and the AI provider for BC Correios inside the Reporter Console.
The **Settings** page is where an administrator configures how BC Correios connects to the Banco Central do Brasil (BCB) and how it processes mail. It groups configuration into collapsible sections: BCB credentials, polling, retention, webhook, and AI.
## Accessing Settings
***
Open **Reporter** > **BC Correios** > **Configurações** from the left-side menu. The screen is headed **Settings**.
Viewing Settings requires the `settings:get` permission. Saving changes and testing connections require write permissions (`settings:patch` / `settings:post`). With read-only access, you see the current configuration with a read-only banner.
## BCB credentials
***
The Sisbacen credentials used to access the BC Correio mailbox.
* **BCB username** — your Sisbacen username.
* **BCB password** — your Sisbacen password.
A badge shows whether the credentials are **Configured** or **Not configured**. For security, stored secrets are never displayed back — leave a field blank to keep the existing value.
Without valid BCB credentials, BC Correios cannot synchronize mail. If the [Dashboard](/en/reporter/console/bc-correios-dashboard) sync status shows credentials as invalid, update them here first.
## Polling
***
Controls how often BC Correios fetches new mail from the BCB.
* **Sync interval** — choose 5, 15, 30, or 60 minutes.
## Retention
***
The minimum period mail is kept.
* **Retention days** — minimum 5 years (1,825 days), maximum 100 years. The equivalent in years is shown next to the field.
Retention enforces the regulatory minimum. This is why BC Correios has no user-facing delete — see [mail folders](/en/reporter/console/bc-correios-folders).
## Webhook
***
Optional outbound notification when new mail is received.
* **Webhook URL** — the endpoint to notify.
* **HMAC secret** — used to sign webhook payloads so your receiver can verify authenticity.
A badge shows whether the webhook is **Enabled** or **Disabled** based on whether a URL is set.
## AI provider
***
Optional automatic analysis of regulatory mail, on a **bring-your-own-key** basis. When configured, summaries appear in the [mail detail](/en/reporter/console/bc-correios-inbox#mail-detail) **AI analysis** panel.
| Field | Description |
| :------------------ | :-------------------------------------------------------------- |
| **AI provider** | None, OpenAI, Anthropic, or Google Gemini. |
| **API key** | Your provider API key. |
| **Model** | The model to use (for example `gpt-4o-mini`). |
| **Custom endpoint** | Optional custom endpoint, for self-hosted or proxied providers. |
| **System prompt** | Optional instructions that shape the AI analysis. |
Use **Test connection** to verify the AI configuration before saving; on success the response latency is reported.
## Saving changes
***
When you edit any field, a save bar appears with **Discard** and **Save** actions. BC Correios sends only the fields you changed, and shows a confirmation when the save succeeds.
Configuration changes are recorded in the [Audit Trail](/en/reporter/console/bc-correios-audit) as `Updated settings` and `Tested BCB connection` events.
# Deadlines in the Console
Source: https://docs.lerian.studio/en/reporter/console/deadlines-overview
Track upcoming and overdue reporting deadlines from the Reporter Overview in Lerian Console — regulatory calendar, delivery status, and notifications.
In [Lerian Console](/en/platform/console/about-lerian-console), **Deadlines** are not a separate menu item. They live inside the [Reporter module](/en/reporter/console/reporter-module), on the **Overview** page. This keeps delivery tracking next to the reports and data sources it relates to, rather than in an isolated area.
Use this page to understand how reporting deadlines surface visually in the Console. For creating, updating, or delivering deadlines programmatically, see [Managing deadlines](/en/reporter/managing-deadlines).
Reporter is only available if you've acquired the Reporter product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more.
## Where to find deadlines
***
When you open Reporter, you land on **Overview**. Deadlines appear here as dedicated widgets alongside the module's KPIs:
* **Reporter** from the left-side menu opens the module.
* **Overview** is the default landing page and the home for all deadline tracking.
There is no separate "Deadlines" navigation entry — everything related to delivery tracking is consolidated on this dashboard.
## Overview widgets
***
Two widgets on the Overview page give operators a complete view of their reporting obligations:
* **Upcoming Deadlines**: Lists the next reporting reminders with their due dates and current status, so operators can see what's coming up at a glance.
* **Delivery Calendar**: Shows recurring and one-off reporting deadlines by date, giving a calendar view of the obligation landscape across the period.
Together, the `UpcomingDeadlines` and `RegulatoryCalendar` widgets turn the underlying deadline records into a visual delivery layer on top of the reporting engine.
## What you can see
***
From the Overview, operators can monitor each obligation without leaving the Console:
* **Deadline status** — whether each obligation is `pending`, `overdue`, or `delivered`.
* **Notifications** — reminders that begin a configured number of days before the due date, so a deadline doesn't slip unnoticed.
* **Delivery state** — when a report behind a deadline has been filed or sent, the deadline reflects that it was delivered.
Deadlines in the Console are a tracking view only. They help operators follow reporting routines — they do not generate or submit reports automatically. Report generation still happens from the [Reports](/en/reporter/console/managing-reports) area.
## Console vs API
***
Use the **Console Overview** when an operator needs to *see* upcoming and overdue deadlines, follow the regulatory calendar, and monitor delivery status visually.
Use the **API** when a system needs to create, update, delete, or mark deadlines delivered programmatically — for example, to sync obligations from another system or automate delivery tracking.
Full request and response reference for creating, listing, updating, deleting, and delivering deadlines.
# Delete a Data Source
Source: https://docs.lerian.studio/en/reporter/console/deleting-a-data-source
Permanently remove a Data Source from Reporter when no active reports depend on its database connection, keeping your Console configuration clean.
Use this page to remove a Data Source that should no longer be available for future Templates or Reports.
## Objective
***
Permanently delete a Data Source connection from Reporter.
For example, delete a Data Source when an external reporting database was retired, a temporary migration connection is no longer needed, or operators have moved to a replacement connection.
## When to use
***
Use this task when:
* the database connection is retired;
* no current Template or reporting process should use it;
* a replacement Data Source exists, if the process continues;
* the connection was created by mistake and should not remain available.
Do not delete a Data Source just because it is not part of the current operator journey. Internal product Data Sources may be preconfigured for other Templates or future Reports.
Do not delete a Data Source just because a connection test failed. If the connection is still needed, edit and test it instead.
## Before you start
***
Confirm:
* no active reporting process depends on the connection;
* current Templates do not rely on fields from this Data Source;
* operators know which replacement Data Source to use;
* the connection is not only failing temporarily because of credentials, network, schema, or SSL settings.
Deleting a Data Source is permanent. Future Report generation can fail if a Template or process still depends on this connection.
## Step by step
***
Go to **Data Sources**.
Locate the card for the Data Source you want to delete.
Click the three vertical dots () on the card and select **Delete**.
In the **Delete Connection** dialog, review the connection name and confirm the deletion.
Confirm that the Data Source no longer appears on the **Data Sources** page.
## Field guide
***
| Field or label | What to check | Example |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Connection name | Confirms the Data Source that will be removed. The technical field is `configName`; check that it is the retired connection, not a replacement. | `migration-postgres-temp` |
| Connection ID | Technical UUID used by the API as the `connectionId` path parameter. The Console selects it when the operator chooses **Delete**. | `018f2f4c-...` |
| **Delete** | Action in the Data Source card menu. It starts the destructive flow and opens the confirmation dialog. | Select **Delete** on `migration-postgres-temp`. |
| **Delete Connection** | Final confirmation action. After this, the Data Source is permanently removed from the **Data Sources** list. | Confirm the temporary connection removal. |
## Expected result
***
The Data Source is removed from the list and can no longer be selected or used by future Reporter workflows.
## Common errors and care points
***
Future Reports can fail if the Template expects data from a deleted connection. Confirm dependencies first.
If the connection is still operationally needed, edit credentials or SSL settings and run **Test Connection**.
If the reporting process continues, create and test the replacement Data Source before deleting the old one.
## Next steps
***
* Use [Add a Data Source](/en/reporter/console/adding-a-data-source) if a replacement connection is needed.
* Use [Test a Data Source Connection](/en/reporter/console/testing-a-data-source) to validate replacement connections.
* Use [Managing Data Sources](/en/reporter/console/managing-data-sources) to review the remaining active connections.
[Delete connection endpoint](/en/reference/reporter/connections/delete-connection)
# Delete a Template
Source: https://docs.lerian.studio/en/reporter/console/deleting-a-template
Remove a Template from Reporter when it is no longer needed for any regulatory report generation, keeping the Console library focused and easy to navigate.
Use this page to remove a Template that should no longer be used for future Report generation.
## Objective
***
Delete a retired Template from the **Templates** page.
For example, delete a Template when a reporting layout was replaced by a new one, the report obligation no longer exists, or the Template was created by mistake and has no operational use.
## When to use
***
Use this task when:
* the Template is retired;
* operators should no longer select it when generating Reports;
* a replacement Template already exists, if the reporting process continues;
* the Template was created incorrectly and should not remain available.
Do not delete a Template just because one generated Report was wrong. Update the Template or the Report inputs, then generate a new Report.
## Before you start
***
Confirm:
* no active reporting process depends on this Template;
* operators are not using it for the current reporting cycle;
* a replacement Template exists when the report is still required;
* any generated Reports that need external storage were downloaded or archived.
Deleting a Template is permanent. Reports already generated with that Template remain available on the **Reports** page, but new Reports cannot use the deleted Template.
## Step by step
***
Go to **Templates**.
Locate the Template you want to remove. Check its name carefully if multiple Templates have similar names.
Click the three dots () in the **Actions** column and select **Delete**.
In the **Confirm Deletion** dialog, review the Template name and confirm the deletion.
## Field guide
***
| Field or label | What to check | Example |
| -------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| Template name | Confirm this is the retired Template, not the replacement. API field: `description`; Console field: `name`. | `Monthly tax CSV - legacy` |
| Template ID | Technical identifier used by the delete API. The Console selects it when you choose **Delete**. | `018f2f4c-...` |
| **Delete** | Opens the destructive confirmation flow from the row **Actions** menu. | Select **Delete** for the retired Template |
| **Confirm Deletion** | Final confirmation before the Template is removed from future **Generate Report** selection. | Confirm `Monthly tax CSV - legacy` |
## Expected result
***
The Template is removed from the **Templates** page and is no longer available in the **Generate Report** wizard.
## Common errors and care points
***
Future Reports that depend on the deleted Template cannot be generated from it. Confirm the replacement workflow before deletion.
If the Template is still needed but has the wrong layout, output format, or name, update it instead of deleting it.
Generated Reports remain available on the **Reports** page. Deletion only removes the Template from future use.
## Next steps
***
* Use [Add a Template](/en/reporter/console/adding-template) if you need a replacement Template.
* Use [Update a Template](/en/reporter/console/updating-a-template) if the Template should be corrected instead of removed.
* Use [Managing Templates](/en/reporter/console/managing-templates) to review the active Template list.
[Delete template endpoint](/en/reference/reporter/delete-template)
# Download a Report
Source: https://docs.lerian.studio/en/reporter/console/downloading-a-report
Download a finished Report from the Console once Reporter has completed generation, ready for submission to regulators or long-term archiving.
Use this page to download a generated Report after Reporter finishes creating the file.
## Objective
***
Save the generated Report file outside the Console so it can be submitted, archived, reviewed, or shared through the appropriate internal workflow.
For example, download a `Finished` XML file for regulatory submission or a CSV file for operational reconciliation.
## When to use
***
Use this task when:
* the Report status is `Finished`;
* the operator needs the generated file outside the Console;
* the file must be stored, sent through another process, or checked locally.
Do not use download while the Report is still `Processing`. Wait for completion first.
## Before you start
***
Confirm:
* the Report status is `Finished`;
* the Template and filters used for generation are the intended ones;
* the output format is the one expected by the receiving process;
* `Error` Reports have been corrected and regenerated.
Preview is for checking the Report inside the Console. Download is the action that saves the generated file for use outside the Console.
## Step by step
***
Go to **Reports**.
Locate the Report you need to download. Check its name, Template, generation date, and status.
Make sure the status is `Finished`.
Click the three dots () in the **Actions** column.
Select **Download Report**.
## Field guide
***
| Field or label | What to check | Example |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Report status | Download only after the Report reaches `Finished`. Technical field: `status`. | `Finished` |
| Template | Check which Template and output format produced the file. Technical fields include `templateId`, `templateDescription`, and `templateOutputFormat`. | `Monthly tax CSV`, `csv` |
| Filters | Review the filters used during generation. Generated Reports cannot be edited before download. | `transactions.created_at between 2026-05-01 and 2026-05-31` |
| **Download Report** | Action that downloads the generated file for the selected `reportId`. | Download report `018f2f4c-...` |
## Expected result
***
The file downloads in the output format configured by the Template, such as CSV, XML, HTML, TXT, or PDF.
## Common errors and care points
***
Wait until the status changes to `Finished`. A Report in `Processing` is not ready yet.
Download does not change Report content. If the filters were wrong, generate a new Report with corrected inputs.
Preview helps operators inspect the Report in the Console. Use **Download Report** for the final file.
## Next steps
***
* Store or deliver the downloaded file through the required operational process.
* Use [Correct a Report](/en/reporter/console/editing-a-report) if the downloaded file has wrong data, layout, or format.
* Use [Generate a Report](/en/reporter/console/generating-a-report) to create a corrected file.
[Download report endpoint](/en/reference/reporter/download-report)
# Edit a Data Source
Source: https://docs.lerian.studio/en/reporter/console/editing-a-data-source
Update an existing Data Source — change credentials, switch the host, or adjust SSL settings — without recreating the connection or reconfiguring reports.
Use this page to update an existing editable Data Source when its database access details change.
## Objective
***
Keep a Data Source usable without deleting and recreating it.
For example, edit a Data Source when an external database password rotates, the host changes after an infrastructure migration, a new schema must be visible, or SSL settings are updated.
## When to use
***
Use this task when:
* credentials changed;
* host, port, or database name changed;
* schema access changed;
* SSL mode or CA certificate changed;
* metadata needs better operator context.
If the Data Source was automatically configured for an internal Lerian product, confirm whether it is meant to be edited from the Console before changing it.
If the connection is retired and should no longer be used, delete it instead.
## Before you start
***
Confirm what changed:
* credentials: update **Username** or **Password**;
* database location: update **Host**, **Port**, or **Database Name**;
* schema visibility: update the schema list;
* security: update **SSL Mode** or **CA Certificate**.
A Data Source update affects future Report generation. Reports already generated stay unchanged.
## Step by step
***
Go to **Data Sources**.
Locate the card for the Data Source you need to edit.
Click the three vertical dots () on the card and select **Edit**.
In **Edit Connection**, change the fields that need correction.
Click **Save Changes**.
Run **Test Connection** to confirm the updated settings work.
The **Password** field appears empty for security reasons. Enter a new value only when changing the password. Leaving it blank keeps the current password.
## Field guide
***
| Field | What to change | Example |
| ----------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------- |
| **Connection Name** | Required operator-facing name. Technical field: `configName`; Console validation is 3 to 100 characters. | `production-postgres-readonly` |
| **Database Type** | Database engine used by the connection. Console currently offers `POSTGRESQL` and `MONGODB`. | `POSTGRESQL` |
| **Host** | Database server address. Changing it changes where Reporter reads future data from. | `db-new.example.com` |
| **Port** | Database network port. Valid range is 1 to 65535. | `5432` |
| **Database Name** | Database Reporter queries through this Data Source. | `ledger_reporting` |
| **Username** | Database user used by Reporter. | `reporter_readonly` |
| **Password (optional)** | Leave blank to keep the current password. Enter a value only when replacing the stored credential. | New rotated password |
| **Schemas** | Optional schema list stored as `metadata.schemas`; entries are trimmed, lowercased, and deduplicated. | `public`, `pix` |
| **Enable SSL** | Toggle SSL settings. Disabling SSL sends `ssl.mode = disable`. | Disabled after network migration |
| **SSL Mode** | SSL behavior. Use `disable`, `require`, `verify-ca`, or `verify-full`. | `verify-full` |
| **CA Certificate** | Required when changing SSL to `verify-ca` or `verify-full`. Leave blank unless replacing SSL material. | PEM CA certificate |
| **Metadata** key/value fields | Optional classification data. Only rows with a nonblank key are submitted. | `owner = compliance` |
## Expected result
***
The Data Source keeps the updated configuration. After a successful test, operators can rely on it for Templates and Reports.
## Common errors and care points
***
Leaving **Password** blank preserves the existing password. Enter a new value only when the credential changed.
Host and port changes can point Reporter to a different database. Always run **Test Connection** after saving.
If a Template or filter depends on fields from a schema, removing that schema can break future generation.
If the database is no longer used by any reporting process, delete the Data Source instead of editing it.
## Next steps
***
* Use [Test a Data Source Connection](/en/reporter/console/testing-a-data-source) after every connection change.
* Use [Generate a Report](/en/reporter/console/generating-a-report) only after the connection is healthy.
* Use [Delete a Data Source](/en/reporter/console/deleting-a-data-source) when the connection is retired.
[Update connection endpoint](/en/reference/reporter/connections/update-connection)
# Correct a Report
Source: https://docs.lerian.studio/en/reporter/console/editing-a-report
Understand why generated Reports cannot be edited, and how to correct the source setup before generating a new Report from the same Reporter template.
A generated Report is a historical file. Reporter does not edit the generated output after generation finishes.
## Objective
***
Correct the source setup that produced a wrong Report, then generate a new Report.
For example, if a CSV file is missing rows, review the filters used during generation. If the layout is wrong, update the Template. If values are wrong, test the Data Source and confirm it points to the expected database and schema.
## When to use
***
Use this page when:
* an operator asks why a generated Report cannot be changed directly;
* a generated Report has wrong data, layout, or output format;
* an `Error` or `Partial` Report needs investigation before trying again;
* the team needs a corrected file while preserving the original generated Report for history.
Do not use this task to overwrite a generated file. Generate a new Report after correction.
## Before you start
***
Identify what is wrong:
| Problem | Likely source to check |
| ---------------------------------------- | ------------------------------------------------------ |
| File layout is wrong | Template structure or `.tpl` file |
| Output format is wrong | Template **Output Format** |
| Rows are missing | Report filters or Data Source schema |
| Values are wrong | Data Source connection, schema, fields, or source data |
| Generation returned `Error` or `Partial` | Report details, Template, Data Source, filters |
The original Report remains available for audit history. The corrected file is a new generated Report.
## Step by step
***
Go to **Reports** and open the generated Report details.
Check the Template, filters, output format, status, and any error details.
Update the Template, test or edit the Data Source, or adjust the filters you will use during the next generation.
Use the corrected Template, Data Source, and filters to generate a new Report.
Wait until the new Report reaches `Finished`, then download it.
## Field guide
***
| Field or status | What it helps diagnose | Example |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| Template | Whether the correct Template and output format were used. Technical fields include `templateId`, `templateDescription`, and `templateOutputFormat`. | `Monthly tax CSV`, `csv` |
| Filters | Whether the generated Report included the intended data subset. Technical field: `filters`. | `status eq active` |
| Status | Whether the Report finished, is still running, covers only some of its sections, or returned an error. Technical field: `status`. | `Error` |
| Error details | Message returned by Reporter when generation fails. Technical field: `error`. | `connection timeout` |
| Data Source | Whether the Report came from the expected database and schema. Check the filter `database` and the Template field mapping. | `production-postgres` |
## Expected result
***
The incorrect Report stays unchanged, and a new corrected Report is generated from the fixed setup.
## Common errors and care points
***
Reporter keeps generated Reports as historical outputs. Correct the Template, Data Source, or filters, then generate again.
Missing rows often come from filters, not from the Template. Review the generation inputs before changing the Template.
If values are wrong or schema access changed, test the Data Source before generating a corrected Report.
## Next steps
***
* Use [Update a Template](/en/reporter/console/updating-a-template) when layout or output format is wrong.
* Use [Testing a Data Source](/en/reporter/console/testing-a-data-source) when values or schema access are suspect.
* Use [Generate a Report](/en/reporter/console/generating-a-report) after fixing the source setup.
Generate the corrected Report. There is no direct edit API for historical outputs.
Monitor the corrected Report before download.
# Generate a Report
Source: https://docs.lerian.studio/en/reporter/console/generating-a-report
Generate a regulatory Report through a four-step wizard that guides you from Template selection to the final compiled output ready for submission.
Use this page to generate a Report from an existing Template. Generation is the moment Reporter applies a Template to data and creates a file that can be monitored, viewed, and downloaded.
## Objective
***
Create a generated Report for a specific operational need, period, entity, status, or submission.
For example, generate a monthly CSV for May transactions, an XML file for a regulatory submission, or a PDF summary filtered to one customer segment.
## When to use
***
Use this task when:
* the Template is ready;
* the Data Source used by the Template is available. It may be an internal source already configured for the environment or an external source added from **Data Sources**;
* an operator needs to produce a Report file;
* the Report needs optional filters, such as date range, status, type, or entity.
Do not use this task to change the Template structure. Update the Template first, then generate a new Report.
## Before you start
***
Confirm:
* the selected Template is the correct one for the obligation;
* the Template output format matches the file you need to deliver;
* the Data Source used by the Template is available and tested when the report depends on a connection that may have changed;
* you know whether the Report should include all data or a filtered subset;
* the filter values are precise enough for the reporting period.
Reports are generated asynchronously. A new Report starts in `Processing` and only becomes available for download when it reaches `Finished`.
## Step by step
***
Go to **Reports** and click **New Report**.
In the **Template** step, choose the Template that defines the output structure and format. Click **Next**.
In the **Filters** step, add one or more filters to narrow the data. This step is optional. If the Report should include all available data, click **Next**.
In the **Review** step, check the selected Template and applied filters.
Click **Generate Report**. The wizard moves to the **Generate** step and tracks progress while Reporter creates the file.
When generation succeeds, the wizard offers **Download** and **View in List** actions. If you close the wizard earlier, the Report appears in the list with `Processing` status; wait until it reaches `Finished` before downloading.
## Field guide
***
| Field | What to enter | Example |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Template** | Required Template selected in the **Template** step. The technical value sent to the API is `templateId`. | `Monthly tax CSV` |
| Search templates | Optional UI search. It filters the loaded Template list by name or file name and is not sent in the generation payload. | `tax` |
| **Database** | Required only when a filter row is used. Select the Data Source connection; the payload uses this value as `database`. | `production-postgres` |
| **Table** | Required only when a filter row is used. Select a table from the connection schema. | `transactions` |
| **Field** | Required only when a filter row is used. Select the field to compare. | `created_at` |
| **Operator** | Required only when a filter row is used. Use `eq`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, or `nin`. | **Between** / `between` |
| **Values** | Required only when a filter row is used. `between` needs exactly two values; `in` and `nin` accept comma-separated values; other operators use one value. | `2026-05-01, 2026-05-31` |
## Filter examples
***
| Filter | Result |
| ------------------------------------------------- | ---------------------------------------- |
| `status` **Equal** `active` | Includes only active records. |
| `created_at` **Between** `2026-05-01, 2026-05-31` | Includes records from May 2026. |
| `type` **In** `credit,debit` | Includes either transaction type. |
| `amount` **Greater Than** `1000` | Includes records with amount above 1000. |
## Status and next action
***
| Status | What it means | Operator action |
| ------------ | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `Processing` | Reporter is generating the file. | Wait. The list refreshes automatically. |
| `Finished` | The file is ready. | View, preview, or download the Report. |
| `Partial` | Some data sections failed. The Report covers only the sections that answered. | Review the failed sections in the Report details, check Data Source health, and generate again after correction. |
| `Error` | Reporter found an error during generation. | Review the message and generate again after correction. |
## Expected result
***
A new Report appears on the **Reports** page. When it reaches `Finished`, operators can preview or download the generated file.
## Common errors and care points
***
The Template controls layout and output format. If the wrong Template is selected, generate a new Report with the correct Template.
Failed or outdated connections can cause generation errors. Test the Data Source before generating operational Reports when connection health is uncertain, especially for external databases or recently changed credentials.
Empty filters can include all available data. Confirm whether the Report should cover all records or only a specific period, status, or entity.
Download is only available after the Report reaches `Finished`.
## Next steps
***
* Use [Download a Report](/en/reporter/console/downloading-a-report) when the status is `Finished`.
* Use [Correct a Report](/en/reporter/console/editing-a-report) if the generated file has wrong data, layout, or format.
* Use [Testing a Data Source](/en/reporter/console/testing-a-data-source) if generation fails because of connection or schema issues.
Generate Reports programmatically.
Poll generation status before download.
# Managing Data Sources
Source: https://docs.lerian.studio/en/reporter/console/managing-data-sources
Review, test, and manage the database connections that supply data to Reporter, keeping every regulatory report grounded in the correct source of truth.
Data Sources are connections that Reporter uses to fetch the data needed for report generation. Some Data Sources can already be configured for internal Lerian products, such as Midaz or CRM. Use this page to review those connections, test them, or manage external database connections through the [Lerian Console](/en/platform/console/about-lerian-console).
## What is a Data Source?
***
A Data Source represents a configured connection between Reporter and a data store. To generate reports, Reporter needs access to source data. For internal product data, the connection may already exist. For external databases, operators can add and manage the connection from this page.
Each Data Source includes:
* **Connection name** — a human-readable identifier for easy management.
* **Database type** — the database engine being used (PostgreSQL, MySQL, MongoDB, Oracle, or SQL Server).
* **Access configuration** — host, port, database name, credentials, and optionally SSL configuration, when the Data Source is an external database connection.
* **Schemas** — for databases that support schemas (PostgreSQL, Oracle, SQL Server), you can specify which schemas to access.
* **Metadata** — additional key-value fields for organization.
Operators usually use this page to answer three questions:
* Is the connection available?
* Which schemas, tables, and fields can Reporter read?
* Is this the right Data Source for the Template or Report being prepared?
## Supported databases
***
| Database | Default port | Schema support |
| ---------- | :----------: | :------------: |
| PostgreSQL | 5432 | ✅ |
| MySQL | 3306 | ❌ |
| MongoDB | 27017 | ❌ |
| Oracle | 1521 | ✅ |
| SQL Server | 1433 | ✅ |
## Accessing the Data Sources page
***
To open the **Data Sources** page, click **Data Sources** from the left-side menu on the **Reporter** page.
You do not need to start here in every Reporter journey. Start here when the needed Data Source is missing, when an external database must be added, or when a report fails because data is unreachable.
The page displays all configured connections, each in a card showing:
* Connection name and database type.
* Connection status (**Connected**, **Disconnected**, or **Connecting**).
* Configured schemas (if any).
* Latency from the last check.
* Host, database, and user details.
You can expand each card to view the database schema, including available tables and columns.
### Available actions
From the Data Sources page, you can:
Create a reusable external database connection for Reporter.
Update credentials, host, schemas, SSL, or metadata.
Confirm Reporter can reach the database and load schema information.
Remove retired connections that are no longer used.
If a Report fails because data is missing or unreachable, start by testing the Data Source and checking whether the expected schema and table appear in the expanded card.
# Managing Reports
Source: https://docs.lerian.studio/en/reporter/console/managing-reports
Generate, filter, track status, and download regulatory reports created from your uploaded templates in the Reporter module of the Lerian Console.
Once templates are uploaded, you can view and manage reports through [Lerian Console](/en/platform/console/about-lerian-console), including generating, filtering, tracking their status, and downloading results.
Use the Reports page as the operational queue for generated files. It shows what was generated, whether generation finished, and what action the operator should take next.
If the required Template and Data Source already exist, operators can start here to generate the next Report. Use **Data Sources** only when a connection is missing, unhealthy, or needs to be tested.
## Accessing the Reports page
***
To open the **Reports** page, click **Reports** from the left-side menu on the **Reporter** page.
The page displays all reports for the organization in two view modes:
* **Table** (default) — a list with columns: Name, Report ID, Status, Format, and Completed At.
* **Grid** — a card-based view. Toggle between modes using the button in the top-right area.
### Actions available
Start a new generation from an existing Template.
Save a finished Report for submission or archiving.
Learn what to change when a generated Report is wrong.
For reports with errors, additional actions are available in the menu ():
* **View Details** — opens a panel with error details.
* **Try Again** — reopens the generation wizard with the same template pre-selected.
## Filtering reports
***
You can filter reports by date using the filter fields above the list.
The list automatically refreshes while there are reports with `Processing` status. You don't need to reload the page.
To clear all filters, click the icon.
## Report status
***
Each report displays a status badge, so you always know where it stands:
| Status | Definition |
| :----------- | :-------------------------------------------------------------------------------- |
| `Processing` | The system is generating the report. |
| `Finished` | The report is ready for viewing and download. |
| `Partial` | Some data sections failed. The report covers only the sections that answered. |
| `Error` | Something went wrong during generation. You can view error details and try again. |
If the status is `Error` or `Partial`, the Report is not a final artifact. Open the details first, fix the Template, filters, or Data Source, and generate a new Report.
## Viewing a report
***
When a report reaches the `Finished` status, you can view it directly in the Console:
* Click the report name in the list, or
* Click the three dots () and select **View**.
A preview dialog will open showing the report content. The preview adapts to the report's output format:
| Format | Preview behavior |
| :----- | :--------------------------------------------------------------------------------- |
| CSV | Interactive table with headers, row numbers, and column/row count. |
| XML | Syntax-highlighted code with line numbers and a virtual scrollbar for large files. |
| PDF | Embedded PDF viewer rendered in an iframe. |
| HTML | Sandboxed HTML rendered in an iframe. |
| TXT | Plain text displayed in a monospaced block. |
### Preview actions
The preview dialog includes action buttons at the top:
* **Copy** — copies the report content to your clipboard (not available for PDF).
* **Print** — opens the browser print dialog for the report content (not available for PDF).
* **Download Report** — downloads the report file directly from the preview.
For PDF reports, use the embedded viewer controls to navigate pages, zoom, and print.
# Managing Templates
Source: https://docs.lerian.studio/en/reporter/console/managing-templates
Manage reusable templates that define the structure and format of every regulatory report Reporter generates from your Midaz and CRM data sources.
Templates are reusable configurations that define the structure and format of generated reports. In this section, you'll learn how to manage templates via the [Lerian Console](/en/platform/console/about-lerian-console).
Use the Templates page to manage the reusable definitions that operators select when generating Reports. A Template controls the output format, file layout, and the data fields Reporter uses.
In the common internal-product journey, this is often where operators start: Data Sources for products such as Midaz or CRM may already be configured, so the first manual task is choosing, creating, or updating the Template.
## Accessing the Templates page
***
To open the **Templates** page, click **Templates** from the left-side menu on the **Reporter** page.
The page displays all templates for the organization in a table format, with the following columns:
* **Name** — the template name.
* **Template ID** — the unique identifier for the template.
* **Format** — the output format (CSV, XML, HTML, TXT, or PDF).
* **Last Modified** — the date the template was last modified.
* **Actions** — action menu.
You can filter templates by creation date or output format using the filter fields above the table.
### Creating templates
The Templates page offers two creation modes:
* **Template Builder** — a visual editor for building templates in a guided flow.
* **New Template** — uploads a `.tpl` file created externally.
Choose **Template Builder** when the operator needs a guided, visual flow. Choose **New Template** when a technical team already prepared the `.tpl` file outside the Console.
If the Template needs fields from a Data Source, confirm that the source already appears in the builder. Add a new Data Source only when the required external connection is missing.
### Available actions menu
For each template, the following actions are available in the action menu ():
* **Preview** — previews the template (available only for templates created with the Template Builder).
* **Edit** — opens the edit wizard or the Template Builder, depending on how the template was created.
* **Delete** — permanently removes the template.
### Related pages
Create a Template visually or upload a prepared `.tpl` file.
Change the Template used for future Reports.
Remove a Template that is no longer operationally needed.
Build a Template with visual blocks instead of editing `.tpl` code.
Updating or deleting a Template does not change Reports already generated. It only changes what can be generated next.
# Concepts Map
Source: https://docs.lerian.studio/en/reporter/console/reporter-concepts-map
Understand how Templates, Reports, Data Sources, and downloads relate in Reporter before using the module in the Lerian Console for regulatory reporting.
The Reporter Module uses a small set of core objects. They are related, but the operator does not always start by creating a Data Source. In some environments, internal Data Sources for Lerian products such as Midaz or CRM are already configured. This page is the shortest path to the mental model.
## Core flow
***
Reporter has two common entry paths:
* **Internal product data already configured:** `Template → Report → Download`
* **External or missing data connection:** `Data Source → Template → Report → Download`
* The **Template** says *what the report should look like and in which format*.
* The **Data Source** says *where the data comes from*. It may already exist for internal products or be added manually for external databases.
* The **Report** is the generated file, produced by running a Template against the connected data, optionally narrowed by filters.
* The **Download** is the operator action that takes the finished file out of the Console.
### What each object means
| Object | What it represents | Why it exists first or later |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `Data Source` | A configured data connection. It can be an internal product connection already created for the environment, or an external database connection such as PostgreSQL or MongoDB. | It exists before generation, but the operator only needs to create one manually when the required connection is not already configured. |
| `Template` | A reusable definition of structure and output format (CSV, XML, HTML, TXT, or PDF), uploaded as a `.tpl` file or built visually with the Template Builder. | It describes *what* the report should look like. It does not execute anything by itself. |
| `Filter` | An optional rule applied at generation time (Database + Table + Field + Operator + Value). | It narrows the data used for a single Report run, without changing the Template. |
| `Report` | The output produced when a Template is run against the data. Has a status (`Processing`, `Finished`, `Partial`, `Error`). | It is the actual artifact the operator delivers. |
| `Preview` | The in-Console view of a `Finished` report, adapted to its output format. | It lets the operator validate the result before sending it out. |
| `Download` | The action that takes the finished report file out of the Console in its configured format. | It is the operational endpoint of the whole flow. |
| `Reminder` | An optional delivery deadline tracked from the Overview page (Once, Daily, Weekly, Monthly, Semiannual, Annual). | Used when a Report has a recurring delivery cadence to manage operationally. |
## When a Template becomes a Report
***
A Template by itself is just a definition. It becomes a Report at one specific moment: when you click **Generate Report** in the 4-step wizard on the **Reports** page.
At that point:
1. Reporter takes the selected Template.
2. It applies any filters you configured.
3. It reads from the Data Sources referenced by the Template.
4. It produces a file in the Template's output format and adds it to the Reports list with `Processing` status.
5. When generation finishes, the status moves to `Finished` and the file becomes downloadable.
Until that moment, you have only a *template*. After that moment, you have a *report*.
## Which fields actually matter
***
When operators get stuck, it is usually around the same handful of fields. This is the short list of the ones with real operational effect.
### When checking or adding a Data Source
* **Connection Name** — How the connection shows up everywhere else (in templates, in report filters). For external Data Sources, pick something that identifies the environment, not just the engine (e.g., `production-db`, not just `postgres`).
* **Database Type** — Determines the default port, whether schemas are supported, and the connection driver used.
* **Host / Port / Database Name / Username / Password** — The minimum required to reach the database.
* **Schemas** (PostgreSQL, Oracle, SQL Server) — Limits which schemas Reporter can browse. Leave empty to use the default (`public`) for engines that support it.
* **SSL Configuration** — Optional, but use it whenever the network path is not fully trusted.
### When adding a Template
* **`.tpl` file** (upload mode) — The actual template definition. Required.
* **Template Name** — How the Template appears in lists and pickers.
* **Output Format** — Locks the final file type (CSV, XML, HTML, TXT, or PDF). When PDF is selected in the Template Builder, the generated code is HTML, rendered as PDF at generation time.
### When generating a Report
* **Template** — The Template you want to run.
* **Filters** (optional) — Each row is `Database + Table + Field + Operator + Value(s)`. Use filters to produce a narrower slice without editing the Template.
## Practical reading of the UI
***
Use this interpretation when guiding users:
* `Templates` defines *what the output should look like*.
* `Reports` is where the structure and the data come together and produce a file.
* `Data Sources` defines *what Reporter is allowed to read*. Use it to inspect existing connections, test connectivity, or add external databases.
* `Overview` is the operational dashboard: KPIs, the delivery calendar, and reminders for upcoming or overdue deliveries.
## When users usually get lost
***
A Template is a definition. A Report is a generated file. The same Template can be turned into many Reports over time (each with its own filters, run date, and status).
The Data Source is the connection Reporter uses to read data. A Filter is a per-run narrowing that does not change the Template or the Data Source — only what data the current Report includes.
The Template Builder generates `.tpl` code from blocks you assemble in the Console. Uploading a `.tpl` skips the builder and uses a file prepared externally. Both end up as the same kind of Template, but only builder-created templates can be re-edited in the visual editor.
A Report reaching `Finished` means the file is generated and downloadable. Whether it was actually delivered (sent to a regulator, archived, etc.) is tracked separately via reminders on the Overview page.
Pair this page with [Setup path](/en/reporter/console/reporter-setup-path) when onboarding new users. One page explains the model; the other explains the order.
# What is Reporter module?
Source: https://docs.lerian.studio/en/reporter/console/reporter-module
Configure Data Sources, build Templates, generate Reports, and download the output through a visual interface in the Lerian Console.
**Reporter** is a module inside the [Lerian Console](/en/platform/console/about-lerian-console) for operating report generation from data already available in your environment.
Use it to define the structure of the output, generate a Report, review its status, and download the final file. When the report needs an external database or a connection needs validation, use **Data Sources** to manage that connection. Typical use cases include regulatory filings, internal audit extracts, recurring operational reports, and ad hoc reports for a specific period.
Reporter does not change how report generation behaves in the product. This page only explains how to operate the Reporter Module from the Console.
Reporter is only available if you've acquired the Reporter product. Want access? [Get in touch with our team](https://lerian.studio/contact) to learn more.
## Who uses this page
***
This page is for operators who need to produce or manage Reports in the Console. That usually means operations, compliance, finance, regulatory, or support teams responsible for turning stored data into a file that can be reviewed, submitted, or archived.
Developers and integration teams may also use Reporter, but their main reference is the [Reporter API reference](/en/reference/reporter/reporter-developer-quick-start). The Console documentation explains the visual workflow. The API reference explains programmatic access.
## Start here first
***
If this is your first time using Reporter, start by understanding which setup path applies. Some environments already include internal Data Sources for Lerian products, such as Midaz or CRM. In that case, the operator can often start from **Templates** and **Reports**. Use **Data Sources** first when you need to add an external database or validate an existing connection.
Choose the right path: use existing internal Data Sources, or add and test an external Data Source before generating Reports.
Understand how Data Sources, Templates, Reports, previews, and downloads relate.
## What you can operate
***
From the Reporter Module, you can:
* **Manage Data Sources** — Review, test, or register database connections that Reporter can read from during generation. Internal product connections may already exist; external databases are added from this area.
* **Build Templates** — Define the structure and output format of the Report, either by uploading a prepared `.tpl` file or building one visually in Template Builder.
* **Generate Reports** — Run a Template against connected data, with optional filters such as date range, status, or transaction type.
* **Review generation status** — Track whether a Report is `Processing`, `Finished`, `Partial`, or `Error`.
* **Preview and download output** — Open a finished Report in the Console and download the generated file in its configured format, such as CSV, XML, HTML, TXT, or PDF.
* **Track delivery reminders** — Use the Overview area to follow upcoming and overdue reporting deadlines.
## Operational areas
***
The Reporter sidebar is organized around the common operator workflow: understand the current state, define the output, generate and download files, then manage Data Sources when a connection needs to be added, tested, or changed.
Monitor the current Reporter workspace: KPIs, delivery reminders, upcoming deadlines, and recent error count.
Review internal connections and configure or test external database connections Reporter uses to fetch data.
Create, update, preview, and delete Templates that define the Report layout and output format.
Generate Reports, monitor their status, preview finished files, and download the result.
## Overview area
***
When you open Reporter, you land on **Overview**. Use it as the operating dashboard for the module.
It includes:
* **What is Reporter?** — a short description of the module and a link to the documentation.
* **Templates** — total number of registered Templates.
* **Reports Generated** — total number of generated Reports.
* **Data Sources** — total number of configured database connections.
* **Errors** — errors recorded in the last 30 days.
* **Delivery Calendar** — recurring or one-off reporting deadlines shown by date.
* **Upcoming Deadlines** — the next reminders, their due dates, and their status.
Delivery reminders help operators manage reporting routines. They do not generate or submit Reports automatically. Report generation still happens from the **Reports** area.
## Main tasks
***
Register an external database connection when the needed Data Source is not already configured.
Confirm that credentials, network access, SSL settings, and schema visibility are correct.
Upload a prepared `.tpl` file or build a Template visually with Template Builder.
Run a Template against your data and produce a downloadable file.
Save the final file from a finished Report generation.
Learn what to change when a generated Report has the wrong data, layout, or format.
## Console vs API
***
Use the **Console** when an operator needs to configure, generate, review, and download Reports through the visual interface.
Use the **API** when a system needs to automate Reporter operations, integrate Reporter into another workflow, or manage Templates, Reports, and Data Source connections programmatically.
The same Reporter concepts apply in both places: Data Sources provide the data, Templates define the output, and Reports are the generated files. The Data Source may be preconfigured for internal Lerian products or manually added for external databases.
## Behavior note
***
This documentation update does not introduce a product behavior change. It clarifies how to operate the existing Reporter Module in the Console and where to use the API reference for integration work.
# Setup Path
Source: https://docs.lerian.studio/en/reporter/console/reporter-setup-path
Follow the minimum setup path for operating Reporter from the Lerian Console — data sources, templates, first report generation, and download verification.
Use this path when you need to make Reporter usable from the Console for the first time. The sequence depends on your data source model: if internal product Data Sources are already configured, start with Templates; if you need an external database, add and test the Data Source first.
## Before you start
***
Read the [Reporter concepts map](/en/reporter/console/reporter-concepts-map) if you have not used Reporter before. It explains the operational chain:
* **Data Sources** define where Reporter reads data from.
* **Templates** define the structure and output format of the file.
* **Reports** are generated from a Template and can be monitored, viewed, and downloaded.
Before opening the setup screens, decide which report you want to produce first. For example: "monthly tax report in CSV", "daily operational report in PDF", or "regulatory XML file filtered by date range". That decision tells you whether the needed Data Source already exists, which Template is required, which output format is expected, and which filters you need.
## Prerequisites
***
| Requirement | What must be ready | Where it is used |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| Reporter access | Your organization can access Reporter in the Console. | All Reporter pages |
| Console permissions | Your user can manage Data Sources, Templates, and Reports for the organization. | Data Sources, Templates, Reports |
| Data Source model | Whether the report uses an internal Data Source already configured for products such as Midaz or CRM, or an external database that must be added manually. | Setup path |
| Database connection details | Host, port, database name, user, password, and any required network or SSL information. Required only when adding an external Data Source. | Data Sources |
| Source data context | The database, table, fields, and filters that should feed the first report. | Data Sources, Reports |
| Template input | Either an existing `.tpl` file or enough report structure to build one in the Template Builder. | Templates, Template Builder |
| Output expectation | The required file format, such as CSV, XML, HTML, TXT, or PDF. | Templates, Reports |
| API automation need | A decision on whether setup will stay manual in the Console or be automated through the API. | API reference |
If one of these items is missing, stop there first. Reporter can only generate useful files when the Template, intended output, and relevant data connection are known.
## Choose the setup path
***
Use this decision before following the steps:
| Scenario | Start with | Why |
| --------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| The report uses internal product data already configured in Reporter. | **Templates** | Internal Data Sources for products such as Midaz or CRM may already exist, so the operator does not need to create a connection first. |
| The report uses an external or new database. | **Data Sources** | Reporter needs a manually configured and tested connection before templates or filters can use its fields. |
| The report fails because data is missing or unreachable. | **Data Sources** | Test the connection and confirm schemas, tables, and fields before regenerating. |
## Minimum setup sequence
***
Start from the Reporter overview and confirm that the team knows which report will be created first. Use the overview when you need to explain what each Reporter area is for.
Go to [Reporter Module](/en/reporter/console/reporter-module) and [Reporter concepts map](/en/reporter/console/reporter-concepts-map).
Check whether the needed Data Source already exists. Internal product Data Sources may already be configured for the environment. If the report depends on an external database or a missing connection, add and test that Data Source before building reports.
On the **Data Sources** page, use existing connections when they are already present. Add a new connection only when the required source is not available, then run **Test Connection** before using it in a report.
Use [Adding a Data Source](/en/reporter/console/adding-a-data-source) and [Testing a Data Source connection](/en/reporter/console/testing-a-data-source).
The Template defines the report structure and output format. You can upload an existing `.tpl` file or build the Template visually in the Console.
On the **Templates** page, use [Adding a template](/en/reporter/console/adding-template). If the operator needs to assemble the layout from blocks, use the [Template Builder](/en/reporter/console/template-builder).
On the **Reports** page, create a report from the Template. Select the Template, apply filters when needed, review the setup, then generate the file.
Use [Generating a report](/en/reporter/console/generating-a-report). This is the point where a Template becomes a Report; before generation, it is only a reusable definition.
Generated reports start in `Processing`. Wait until the status reaches `Finished`, then view or download the output file.
If the report ends in `Error`, inspect the details and re-run it after fixing the Data Source, Template, or filter inputs. Use [Downloading a report](/en/reporter/console/downloading-a-report) when the file is ready.
If the report has a recurring deadline, use the **Overview** page to add the delivery reminder. This keeps upcoming and overdue deliveries visible without changing how reports are generated.
Use the reminder only for operational follow-up. It does not create or schedule reports automatically.
## When to use the API reference
***
Use the Console for the first operational setup: confirming or creating the Data Source, testing connections when needed, adding or building the Template, generating the first Report, and downloading the file.
Use the [Reporter API reference](/en/reference/reporter/reporter-developer-quick-start) when you need to automate or integrate the same workflow, such as:
* creating or listing Templates from another system;
* creating reports programmatically;
* checking report status without opening the Console;
* downloading generated files through an integration;
* validating or managing connections outside the operator workflow.
Manual Console setup does not require the API reference. The API reference is for implementation work, automation, and troubleshooting with engineering teams.
## What happens next
***
At the end of the setup path, you should have:
* one relevant Data Source available for the report, either preconfigured internally or manually added and tested;
* one Template that matches the required output format;
* one generated Report with `Finished` status;
* one downloaded file ready for delivery or validation;
* optional delivery reminders in the Overview page when the report has recurring deadlines.
After that, the normal operating loop is simple: keep the relevant Data Source healthy when the report depends on one, update the Template when the file structure changes, generate Reports when needed, and download the final output.
## Common sequencing mistakes
***
Some environments already include internal Data Sources for Lerian products such as Midaz or CRM. Check the existing list before adding a duplicate external connection.
The connection may exist, but credentials, network access, and schemas still need validation when the report depends on that source. Run **Test Connection** before relying on it.
The Template should match the required file format. Confirm whether the operation needs CSV, XML, HTML, TXT, or PDF before using it to generate reports.
Operators can complete the first setup in the Console. Use the API reference when the same workflow must be automated, integrated, or debugged with engineering support.
## Related links
***
Use when the Reporter workflow must be automated or integrated through APIs.
# Using the Template Builder
Source: https://docs.lerian.studio/en/reporter/console/template-builder
Build report templates visually by assembling blocks instead of writing .tpl code manually, and let the builder generate it for you.
Use Template Builder to create or maintain a Template visually. The builder converts blocks into `.tpl` code, so operators can define report structure without writing the Template file manually.
## Objective
***
Build a Template from visual blocks and save it for future Report generation.
For example, a monthly transaction Template can use fixed text for the header, a **Loop** for each transaction row, **Variable** blocks for fields such as amount and date, and an **Aggregation** block for the total.
## When to use
***
Use Template Builder when:
* operators need to create a Template without editing `.tpl` code;
* a Template Builder Template needs to be maintained visually;
* the report structure depends on fields, loops, conditions, or totals from a Data Source;
* the team wants to download the generated `.tpl` file after building it.
Use `.tpl` upload instead when a technical team already created and reviewed the Template outside the Console.
## Before you start
***
Confirm:
* at least one relevant Data Source exists if the Template needs database fields. It can be an internal source already configured for the environment or an external source added from **Data Sources**;
* the Data Source was tested when connection health or schema visibility is uncertain, so its tables and fields are available in the sidebar;
* the expected output format is known: XML, HTML, CSV, TXT, or PDF;
* the operator knows the report structure, such as required header, rows, totals, or conditional sections.
Blocks define the output structure. Filters applied during Report generation define which records enter that structure.
## Step by step
***
Go to the **Templates** page and click **Template Builder**.
Edit the Template name in the header. Use a name that operators can identify during report generation.
Select XML, HTML, CSV, TXT, or PDF.
When **PDF** is selected, the generated template code uses HTML format and is converted to PDF during report generation.
Use the blocks toolbar to add structure. Common blocks are **Text**, **Variable**, **Loop**, **Conditional**, and **Aggregation**.
Use the Data Sources sidebar to browse schemas, tables, and fields. Click a field to add a **Variable** block for that field.
Switch from **Visual** to **Code** to inspect the generated `.tpl` preview. The code view is for review and copy/download, not manual editing.
Click **Save**. The builder validates required block fields, generates `.tpl` code, and saves the Template.
## Field guide
***
### Builder-level fields and controls
| Field or control | What it does | Example |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **Template name** | Required name in the builder header. It maps to `name` and becomes the saved Template label and generated `.tpl` filename prefix. | `Monthly transaction CSV` |
| **Output format** | Required format. Use `xml`, `html`, `csv`, `txt`, or `pdf`. When `pdf` is selected, code generation uses HTML internally. | `pdf` |
| **Visual** | Block editor mode for assembling the Template. Technical state: `viewMode = visual`. | Use while arranging blocks |
| **Code** | Read-only preview of generated `.tpl` code. Technical state: `viewMode = code`. | Review generated `.tpl` |
| **Download .tpl file** | Downloads the generated `.tpl` without saving a new Template. | `monthly-transaction-csv.tpl` |
| **Save** | Validates blocks, generates `.tpl` code, and uploads or updates the Template. Requires a nonblank name and at least one block. | Save after blocks are valid |
### Block field guide
| Block | Fields to configure | Effect |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| **Text** | **Content** (`content`) | Renders fixed text such as CSV headers, XML tags, or HTML fragments. |
| **Variable** | **Data Source**, **Table**, **Field**, **Index (optional)**, **Filters** (`dataSource`, `table`, `field`, `index`, `filters[]`) | Inserts a field value. Inside a parent **Loop**, **Data Source** can be auto-filled from the loop iterator. |
| **Loop** | **Iterator name** and **Iterable source** (`iteratorName`, `iterableSource`) | Repeats child blocks over a collection. Use `dataSource.table` format for the iterable source. |
| **Conditional** | **Condition** and **Include else block** (`condition`, `hasElse`) | Renders child blocks only when the condition is true; optional else content can render when false. |
| **Aggregation** | **Aggregation type**, **Source**, **Field**, optional grouping/order/result fields (`aggregationType`, `source`, `aggregationField`) | Produces totals, counts, averages, min/max, or **Last Item by Group** values from a source collection. |
| **Calculation** | **Expression** (`expression`) | Renders a calculated value. Supported operators shown in the UI include `+`, `-`, `*`, `/`, `**`, and `%`. |
| **Date/Time** | **Format** and **Date source** (`format`, `dateSource`) | Formats a date value, for example `YYYY-MM-DD` from `transaction.createdAt`. |
| **Counter** | **Mode**, **Counter name**, and **Counter names** (`counterMode`, `counterName`, `counterNames[]`) | **Increment** advances one counter. **Display** outputs one or more counters. |
| **Comment** | **Comment** (`commentText`) | Stores an internal Template note and does not appear in the final Report. |
| **Section** | **Section title** (`sectionTitle`) | Groups child blocks to organize larger Templates. |
| **With (Assign)** | **Variable name** and **Assignment expression** (`variableName`, `assignment`) | Creates a reusable variable from an expression for child blocks. |
| **Expression** | **Expression** (`inlineExpression`) | Renders an inline expression such as `item.name\|upper`. |
| **Custom Tag** | **Tag name** and **Tag arguments** (`tagName`, `tagArgs`) | Emits an advanced Template tag such as `include` with arguments like `"header.html"` or `key=value`. |
## Block types
***
| Block | What it represents in practice |
| ----------------- | ------------------------------------------------------------------------------------ |
| **Text** | Fixed content that always appears in the output, such as a header or label. |
| **Variable** | A value from a data source field, such as document number, amount, or status. |
| **Loop** | A repeated section, such as one row for each transaction. |
| **Conditional** | Content that appears only when a rule is true, such as `account.status == "active"`. |
| **Aggregation** | A total, count, average, min, max, or last item by group. |
| **Calculation** | A calculated value, such as `value * 1.05`. |
| **Date/Time** | A formatted date or time value. |
| **Counter** | A row number or sequence value. |
| **Comment** | Internal Template note that does not appear in the generated Report. |
| **Section** | A named group of blocks for organizing larger Templates. |
| **With (Assign)** | A reusable variable created from an expression. |
| **Expression** | An inline expression rendered into the output. |
| **Custom Tag** | A custom Template tag when advanced Template syntax is needed. |
## Working with blocks
***
* Click a block type in the toolbar to add it to the canvas.
* Drag blocks to reorder the output.
* Configure each block inline. For example, a **Variable** needs a source and field, while a **Conditional** needs a condition.
* Use **Inline** when the block should render without a line break after it.
* Use **Trim whitespace** to remove extra spaces around block output.
* Use **Duplicate** to copy a configured block.
* Use **Delete** to remove a block from the Template.
Container blocks such as **Loop**, **Conditional**, **Section**, and **With** can hold child blocks. Use them when the output needs repeated or grouped content.
## Expected result
***
After saving, the Template appears on the **Templates** page and can be selected in the **Generate Report** wizard. You can also download the generated `.tpl` file from the builder.
## Common errors and care points
***
The sidebar only shows configured Data Sources. If the expected internal source is not listed, confirm setup with the Reporter administrator. If the report needs an external database, add and test that Data Source before using database fields in the Template.
The builder validates blocks during save. If a block is missing a source, field, condition, or expression, fix that block before saving.
Template blocks define the file structure. Report filters are selected later during report generation and decide which records enter the output.
PDF output is generated from HTML. Build the Template as HTML-compatible content when the final output format is PDF.
## Next steps
***
* Use [Generate a Report](/en/reporter/console/generating-a-report) to test the Template with real filters.
* Use [Update a Template](/en/reporter/console/updating-a-template) to edit the Template later.
* Use [Add a Template](/en/reporter/console/adding-template) if you need to upload a prepared `.tpl` file instead.
There is no separate API endpoint for the visual builder. Use [Upload template endpoint](/en/reference/reporter/upload-template) with a prepared `.tpl` file.
# Test a Data Source Connection
Source: https://docs.lerian.studio/en/reporter/console/testing-a-data-source
Verify that a Data Source's credentials and network settings are correct before relying on it for regulatory report generation in the Reporter module.
Use this page to confirm that Reporter can reach a Data Source and load its schema. The Data Source can be an internal connection already configured for a Lerian product or an external database connection added from the Console.
## Objective
***
Validate connectivity before operators rely on the Data Source in Templates or Reports.
For example, test after creating `production-postgres`, after a password rotation, or before generating a monthly report that depends on a recently changed schema.
## When to use
***
Use this task:
* immediately after adding an external Data Source;
* when checking an existing internal Data Source before using it in a Template or filter;
* after editing host, credentials, schema, or SSL settings;
* before generating Reports that depend on the connection;
* when no tables appear in the schema browser;
* when Report generation fails because of connection or schema errors.
## Before you start
***
Confirm:
* the Data Source exists on the **Data Sources** page;
* the expected host, port, database name, username, schema, and SSL settings are configured;
* the database should be reachable from Reporter;
* the database user has permission to read the required tables.
A successful test confirms access at that moment. It does not guarantee future Reports will succeed if credentials, network rules, or database permissions change later.
## Step by step
***
Go to **Data Sources**.
Locate the card for the Data Source you need to test.
Click **Test Connection**.
The status changes to **Connecting** while Reporter tests the connection.
If the test succeeds, the status changes to **Connected** and latency is displayed, such as `Latency: 45ms`. If it fails, the status changes to **Disconnected** and an error message appears.
Expand the connection card to view the schemas, tables, and fields returned for the connection.
## Field guide
***
### Test action and result fields
| Field or status | What it means | Example |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Test Connection** | Action on the Data Source card. The Console uses the selected `connectionId` to call the test endpoint. | Test `production-postgres`. |
| **Connecting** | Local status while the test request is pending. Wait for the final result before editing or generating a Report. | **Connecting** |
| **Connected** | Successful result. Reporter returned `connected` or `success`, and the Data Source is usable at that moment. | **Connected** with `Latency: 45ms`. |
| **Disconnected** | Failed result or request error. Check host, port, credentials, network access, schema, and SSL settings. | **Disconnected** after `authentication failed`. |
| Latency | `latencyMs`, returned in milliseconds after a successful test. It is a signal, not a guarantee of future Report generation speed. | `45` |
| Test message | `message` returned by Reporter, usually most useful on failure. Use it as the first clue for what to fix. | `authentication failed` |
### Schema fields shown after testing
| Schema item | What it shows | Example |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| Tables | `tables[].name` returned by schema validation. These are the tables operators can use in Template Builder and report filters. | `transactions` |
| Fields | `tables[].fields[]` inside each table. These fields can be inserted as Template variables or used as Report filters. | `amount`, `created_at` |
| No schema loaded | Empty `tables` array or schema load error. The connection may work, but table permissions or schema names still need correction. | No tables discovered. |
## Expected result
***
The Data Source status is **Connected**, schema information is available, and operators can safely use its fields in Template Builder and report filters.
## Common errors and care points
***
Check schema names and database permissions. The database user may connect but not have access to the tables needed for reporting.
Reopen the Data Source and confirm username, password, host, port, and SSL settings.
Network rules, rotated passwords, and database permissions can change. Re-test when generation errors appear.
## Next steps
***
* Use [Template Builder](/en/reporter/console/template-builder) after schemas and fields are visible.
* Use [Generate a Report](/en/reporter/console/generating-a-report) after the Data Source is healthy.
* Use [Edit a Data Source](/en/reporter/console/editing-a-data-source) if the test fails because settings are wrong.
Test a Data Source connection programmatically.
Validate schema access for tables and fields.
# Update a Template
Source: https://docs.lerian.studio/en/reporter/console/updating-a-template
Update a Template's name, output format, or replace the uploaded .tpl file directly from the Lerian Console — no need to delete and recreate the template.
Use this page when a Template should remain available, but something about its name, output format, layout, or source file needs to change.
## Objective
***
Update the reusable Template that future Reports will use.
For example, update a Template when the report owner changes the required CSV columns, the file must be generated as XML instead of TXT, or the Template name is too generic for operators to identify safely.
## When to use
***
Use this task when:
* the Template is still needed;
* the name or output format is wrong;
* an uploaded `.tpl` file must be replaced;
* a Template Builder Template needs block-level edits;
* future Reports should use the corrected Template.
Do not use this task to change a Report that was already generated. Generated Reports are historical files. Correct the source setup and generate a new Report.
## Before you start
***
Check what kind of Template you are editing:
* **Uploaded Template**: update the name, output format, or replace the `.tpl` file.
* **Template Builder Template**: open the builder, edit the blocks, and save the generated `.tpl`.
Confirm that the change is intended for future Reports. Reports already generated from the old Template stay unchanged.
Replacing a `.tpl` file affects every future Report generated from this Template. Validate the new file before using it for a reporting cycle.
## Step by step
***
Go to **Templates** and locate the Template you need to update.
Click the three dots () in the **Actions** column and select **Edit**.
In **Edit Template**, update the fields that changed. For uploaded Templates, you can change **Template Name**, **Output Format**, and **Template File (.tpl)**.
Click **Update Template**.
Generate a new Report with the updated Template and confirm that the output matches the expected structure.
Templates created with **Template Builder** open in the builder when edited. Templates uploaded as `.tpl` files open in the upload/edit wizard.
## Field guide
***
| Field | What to change | Example |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| **Template Name** | Required Console label for operators. It maps to `name` in the Console and to `description` in the Reporter API. | `Monthly tax XML` |
| **Output Format** | File type for future Reports. Use one of `csv`, `xml`, `html`, `txt`, or `pdf`. | `xml` |
| **Template File (.tpl)** | Optional replacement file. If supplied, it must be `.tpl`, larger than 0 bytes, and no larger than 5 MB. | Replace `tax-v1.tpl` with `tax-v2.tpl` |
| Builder blocks | Visual structure for builder-created Templates. Saving requires at least one valid block. | Add a **Total amount** **Aggregation** block |
## Expected result
***
The Template stays on the **Templates** page with the updated configuration. Future Reports generated from it use the new name, output format, and Template structure.
## Common errors and care points
***
Updating a Template does not rewrite Reports already generated. Generate a new Report after correcting the Template.
If the `.tpl` file structure and **Output Format** do not match the expected delivery format, operators can generate the wrong file type.
Names like `Template 1` or `New Template` are hard to distinguish during report generation. Use a name that includes the report purpose or period.
## Next steps
***
* Use [Generate a Report](/en/reporter/console/generating-a-report) to test the updated Template.
* Use [Correct a Report](/en/reporter/console/editing-a-report) if the issue was found in a generated Report.
* Use [Delete a Template](/en/reporter/console/deleting-a-template) only if the Template is no longer needed.
[Update templates endpoint](/en/reference/reporter/update-templates)
# Trial Balance Report with Midaz and Reporter
Source: https://docs.lerian.studio/en/reporter/creating-trial-balance-report
Produce a trial balance from your Midaz Ledger using Reporter and export it as XML, TXT, HTML, or PDF — no extra conversion tools needed.
Security, compliance, and clarity in financial reporting start with your ledger. **Midaz** is a financial ledger at its core — meaning it already stores all the data required to produce a trial balance, from account structures to transaction histories.
While Midaz provides the raw data, the **Reporter** product transforms this data into ready-to-share outputs. With Reporter, you can generate trial balances and export them directly in formats such as **XML, TXT, HTML, and PDF** — without needing extra conversion tools.
# Key concepts
***
Before diving into the step-by-step process, it's important to understand how Midaz structures data for regulatory reporting:
## OperationRoute and the code field
***
The **OperationRoute** entity is central to COSIF mapping. Each OperationRoute has a code field (max 100 characters) designed specifically to store external reference codes such as COSIF account codes.
When transactions are processed, each operation is associated with an OperationRoute. By aggregating operations grouped by OperationRoute.code, you can calculate balances per COSIF account — which is exactly what a trial balance requires.
```text theme={null}
OperationRoute
├── code: "1.1.1.10-8" ← COSIF code for aggregation
├── title: "Checking Account Debit"
├── operationType: "source" | "destination"
└── account: AccountRule ← Validation rules
```
## TransactionRoute
***
A **TransactionRoute** groups multiple OperationRoutes (sources and destinations) into a reusable template. When creating transactions, you reference a TransactionRoute, and Midaz validates that operations follow the defined rules.
# Step-by-step process
***
## 1. Prepare your Chart of Accounts in Midaz
***
Configure your **Organizations, Ledgers, Assets, Account Types, and Accounts** according to your business needs.
The key entities for trial balance generation are:
* **Account Types** — define the nature of each account (asset, liability, equity, revenue, expense) using the keyValue field.
* **OperationRoutes** — map each type of operation to a COSIF code via the code field.
* **TransactionRoutes** — group OperationRoutes into transaction templates with validation rules.
## 2. Create OperationRoutes with COSIF codes
***
For each COSIF account in your chart of accounts, create corresponding OperationRoutes with the COSIF code in the code field.
Example OperationRoutes for a Brazilian financial institution:
OperationRoute for checking account debits (source):
```json theme={null}
{
"title": "Checking Account Withdrawal",
"description": "Debit operations from personal checking accounts",
"code": "1.1.1.10-8",
"operationType": "source",
"account": {
"ruleType": "account_type",
"validIf": ["checking_account_pf", "checking_account_pj"]
}
}
```
OperationRoute for checking account credits (destination):
```json theme={null}
{
"title": "Checking Account Deposit",
"description": "Credit operations to personal checking accounts",
"code": "1.1.1.10-8",
"operationType": "destination",
"account": {
"ruleType": "account_type",
"validIf": ["checking_account_pf", "checking_account_pj"]
}
}
```
OperationRoute for investment account:
```json theme={null}
{
"title": "Investment Account Movement",
"description": "Operations involving investment accounts",
"code": "1.2.3.00-0",
"operationType": "source",
"account": {
"ruleType": "account_type",
"validIf": ["investment_account"]
}
}
```
Common COSIF mappings:
| COSIF Code | Description | Account Types |
| ---------- | ------------------------------- | --------------------- |
| 1.1.1.10-8 | Checking Accounts - Individuals | checking\_account\_pf |
| 1.1.1.20-5 | Checking Accounts - Companies | checking\_account\_pj |
| 1.2.3.00-0 | Investment Accounts | investment\_account |
| 7.1.1.00-0 | Service Revenue | revenue\_services |
| 8.1.1.00-0 | Administrative Expenses | expense\_admin |
## 3. Create TransactionRoutes
***
Group your OperationRoutes into TransactionRoutes that represent common transaction patterns.
When creating a TransactionRoute, you pass an array of OperationRoute UUIDs. When retrieving a TransactionRoute via the API, it returns the full OperationRoute objects with all their details.
```json theme={null}
{
"title": "Pix Transfer Between Accounts",
"description": "Internal Pix transfer from one checking account to another",
"operationRoutes": [
"uuid-of-checking-account-source-route",
"uuid-of-checking-account-destination-route"
]
}
```
## 4. Process transactions with routes
***
When creating transactions, reference the TransactionRoute. Each operation will be associated with its corresponding OperationRoute and COSIF code:
```json theme={null}
{
"code": "PIX_TRANSFER_001",
"description": "Pix lunch payment",
"route": "uuid-of-pix-transfer-route",
"send": {
"asset": "BRL",
"value": 4550,
"source": {
"from": [{
"accountAlias": "@joao_silva_cc"
}]
},
"distribute": {
"to": [{
"accountAlias": "@restaurante_abc"
}]
}
},
"metadata": {
"pixKey": "12345678901",
"description": "Lunch"
}
}
```
The `asset` and `value` are defined at the `send` level. Individual operations in `from` and `to` use `accountAlias` (not `account`). The value is expressed in the smallest unit of the asset (e.g., cents for BRL, so 4550 = R\$ 45.50).
The COSIF mapping happens automatically through the route:
```text theme={null}
Transaction uses route → TransactionRoute
↓
OperationRoutes[]
↓
source: code = "1.1.1.10-8" (COSIF)
destination: code = "1.1.1.20-5" (COSIF)
```
## 5. Understanding the data flow
***
Midaz stores all the financial data needed for trial balance generation:
* **Operations** — each operation includes a `route` field containing the OperationRoute UUID
* **OperationRoutes** — contain the `code` field with COSIF codes for aggregation
* **Account Balances** — current positions per account
**How aggregation works:**
Each Operation stores a `route` field that references the OperationRoute UUID (not the COSIF code directly). To aggregate by COSIF code, Reporter joins operations with their corresponding OperationRoutes to access the `code` field:
```text theme={null}
Operation.route (UUID) → OperationRoute.id → OperationRoute.code (COSIF)
```
**Available Midaz APIs:**
```text theme={null}
GET /v1/organizations/{org_id}/ledgers/{ledger_id}/accounts/{account_id}/operations
```
Query parameters for filtering include `cursor`, `limit`, `type`, and `direction`. To narrow results to a specific OperationRoute, use `route_id` (UUID) or `route_code` (such as the COSIF code). Operations are retrieved per account.
Reporter connects directly to the Midaz database for report generation, enabling efficient batch queries and aggregations. The APIs above are available for custom integrations and ad-hoc queries.
## 6. Generate your report with Reporter
***
Reporter uses a template-driven architecture to generate reports. The process involves:
1. **Select or create a template** — templates use Pongo2 syntax (Django-like) with custom aggregation tags
2. **Configure data sources** — Reporter queries Midaz database tables directly
3. **Apply filters** — specify date ranges, account types, or other criteria
4. **Generate the report** — Reporter aggregates data and renders the template
5. **Export** in your chosen format (XML, TXT, HTML, PDF, CSV)
**How Reporter aggregates data:**
Reporter templates support built-in aggregation tags for calculating balances:
* `{% sum_by collection by "field" %}` — sum values by field
* `{% count_by collection if condition %}` — count records matching condition
* `{% calc expression %}` — arithmetic calculations
**Example template snippet for COSIF aggregation:**
```
{% for route in operation_routes %}
{{ route.code }} |
Debits: {% sum_by operations by "amount" if operation.route == route.id and operation.type == "DEBIT" %} |
Credits: {% sum_by operations by "amount" if operation.route == route.id and operation.type == "CREDIT" %}
{% endfor %}
```
Reporter includes ready-to-use templates for different reporting needs. Use the [**CADOC 4010**](/en/reporter/cadoc-4010-and-4016) template when generating reports for **BACEN regulatory submission**, as it follows the official required structure.
## 7. Export and integrate
***
You can:
* Export a **ready-to-share PDF** for auditors and regulators
* Generate **XML** files formatted for BACEN submission
* Integrate with your existing reporting workflows or accounting systems
# Practical example — Complete Midaz setup
***
Below is a fictional institution, **DigitalBank**, showing how Midaz entities connect to support a trial balance:
```json theme={null}
{
"legalName": "DigitalBank S.A.",
"legalDocument": "12.345.678/0001-90",
"address": {
"line1": "Av. Faria Lima, 3064",
"line2": "12th floor",
"zipCode": "01451-000",
"city": "Sao Paulo",
"state": "SP",
"country": "Brazil"
},
"metadata": {
"cnae": "6422-1",
"licenseNumber": "BCB-2024-001",
"foundedAt": "2024-01-15"
}
}
```
```json theme={null}
[
{
"name": "Brazilian Real",
"type": "currency",
"code": "BRL",
"status": { "code": "ACTIVE" },
"metadata": {
"symbol": "R$",
"centralBank": "BACEN"
}
}
]
```
```json theme={null}
[
{
"name": "Personal Checking Account",
"keyValue": "checking_account_pf",
"description": "Checking accounts for individual customers"
},
{
"name": "Business Checking Account",
"keyValue": "checking_account_pj",
"description": "Checking accounts for business customers"
},
{
"name": "Investment Account",
"keyValue": "investment_account",
"description": "Investment and savings accounts"
}
]
```
COSIF codes are defined in OperationRoutes via the `code` field, **not** in Account Types. Account Types define the nature of accounts and are used by OperationRoutes for validation rules.
```json theme={null}
[
{
"title": "PF Checking - Debit",
"code": "1.1.1.10-8",
"operationType": "source",
"account": {
"ruleType": "account_type",
"validIf": ["checking_account_pf"]
}
},
{
"title": "PF Checking - Credit",
"code": "1.1.1.10-8",
"operationType": "destination",
"account": {
"ruleType": "account_type",
"validIf": ["checking_account_pf"]
}
},
{
"title": "PJ Checking - Debit",
"code": "1.1.1.20-5",
"operationType": "source",
"account": {
"ruleType": "account_type",
"validIf": ["checking_account_pj"]
}
},
{
"title": "PJ Checking - Credit",
"code": "1.1.1.20-5",
"operationType": "destination",
"account": {
"ruleType": "account_type",
"validIf": ["checking_account_pj"]
}
}
]
```
```json theme={null}
{
"title": "Pix Transfer PF to PJ",
"description": "Pix transfer from personal to business account",
"operationRoutes": [
"uuid-pf-checking-debit",
"uuid-pj-checking-credit"
]
}
```
```json theme={null}
{
"code": "PIX_TRANSFER_001",
"description": "Pix lunch payment",
"route": "uuid-pix-transfer-pf-to-pj",
"send": {
"asset": "BRL",
"value": 4550,
"source": {
"from": [{
"accountAlias": "@joao_silva_cc"
}]
},
"distribute": {
"to": [{
"accountAlias": "@restaurante_abc"
}]
}
},
"metadata": {
"pixKey": "12345678901",
"purpose": "Lunch payment"
}
}
```
# Trial Balance output example
***
After joining operations with their OperationRoutes and aggregating by the `code` field, your trial balance would look like:
| COSIF Code | Description | Debits (R\$) | Credits (R\$) | Balance (R\$) |
| ---------- | ------------- | -------------- | -------------- | ------------- |
| 1.1.1.10-8 | Checking - PF | 150,000.00 | 145,000.00 | 5,000.00 |
| 1.1.1.20-5 | Checking - PJ | 89,000.00 | 92,500.00 | -3,500.00 |
| 1.2.3.00-0 | Investments | 50,000.00 | 48,000.00 | 2,000.00 |
| **Total** | | **289,000.00** | **285,500.00** | **3,500.00** |
Using Reporter, this data can be formatted into:
* **XML** — structured for BACEN regulatory submission
* **PDF** — professional report for auditors and stakeholders
* **TXT** — flat file for legacy system integration
The report consolidates ledger data by COSIF code, presenting balances grouped by accounting sections (Assets, Liabilities, Equity, Revenues, and Expenses), along with subtotals and a global total. This output reflects the result of automated aggregation and validation performed on top of Midaz data, ready for audit, compliance, and regulatory use.
# Summary
***
The key to generating accurate trial balances with Midaz is proper use of the `code` field in OperationRoutes:
1. **Create OperationRoutes** with COSIF codes in the `code` field
2. **Group into TransactionRoutes** for transaction validation
3. **Process transactions** referencing the appropriate routes
4. **Join operations with OperationRoutes** using the `route` field (UUID) to access COSIF codes
5. **Aggregate by OperationRoute.code** to calculate balances per COSIF account
6. **Generate reports** with Reporter using templates that perform the aggregation automatically
This approach ensures:
* Regulatory compliance with COSIF standards
* Automatic balance aggregation by account code
* Validation of transaction operations
* Flexible reporting in multiple formats
# Related documentation
***
* [OperationRoutes API Reference](/en/reference/midaz/create-an-operation-route)
* [TransactionRoutes API Reference](/en/reference/midaz/create-transaction-route)
* [Account Types API Reference](/en/reference/midaz/create-an-account-type)
* [Operations API Reference](/en/reference/midaz/list-operations-by-account)
* [Reporter Overview](/en/reporter/what-is-reporter)
* [CADOC 4010 and 4016](/en/reporter/cadoc-4010-and-4016)
# How report generation works
Source: https://docs.lerian.studio/en/reporter/how-report-generation-works
The path a Reporter report takes: the request, extraction against the configured data sources, the render, the file in object storage, and the download.
Report generation has two halves. The **manager** accepts your request, checks it, and hands it to a queue. The **worker** extracts the data, renders the template, and writes the file. Reporter answers your request as soon as the first half completes, so you poll or listen for the second.
Both halves ship in one binary. A deployment runs the API surface, the worker surface, or both, and a queue carries the work between them.
This page follows one report along that path.
## The path end to end
***
You call [Create a report](/en/reference/reporter/create-report) with a template identifier and the row filters you want. Reporter validates the request and stores the report as `Processing`.
The worker queries each data source the template names, one section per data source, and keeps only the fields in the template's field map.
The extracted rows become the context for the template. Reporter renders the document in the template's output format.
Reporter writes the rendered file to the configured object storage bucket and records the terminal status of the report.
You call [Download a report](/en/reference/reporter/download-report) and receive the file with its content type and filename.
## What the manager does
***
The manager owns everything that happens before the queue. It runs four checks in order, and any one of them can reject the request outright.
**It applies idempotency.** Send an `X-Idempotency` header to name the request yourself. Without one, Reporter derives the key from the request body. A repeat of an in-flight request is rejected as a duplicate. A repeat of a completed request returns the original report and marks the response with `X-Idempotency-Replayed: true`. The window is 30 seconds.
**It resolves the template.** Reporter loads the field map, the output format, and the description that belongs to the template identifier. An unknown identifier fails here.
**It validates your filters.** Every field you filter on is checked against the live schema of its data source. A wrong field name fails the request instead of producing an empty report.
**It records the report and queues the work.** The report is stored as `Processing` and the response returns immediately with that status. Reporter then publishes the generation command for the worker.
The report keeps its own copy of the output format and the template description. That snapshot is what makes an old report reproducible after the template moves on.
## What the worker does
***
The worker consumes the command and owns the rest of the path.
A queue can deliver the same command twice. The worker therefore reads the report before it starts a run: a report that already settled as `Finished` or `Error` is left as it is, rather than generated again.
It then loads the template file from object storage and extracts data. **Each data source is one section.** A section that fails does not stop the others. The worker collects the outcome of every section and only then decides the status of the report.
When every section fails, Reporter records the failure and skips the render. No misleading file reaches the bucket.
The render step turns the surviving sections into the document. When the output format is PDF, Reporter renders HTML first and converts it through a headless browser pool. The finished bytes go to the bucket, the terminal status goes to the report record, and a terminal event goes to the event stream.
## Report states
***
A report reaches one of four states.
| State | Meaning | Artifact |
| ------------ | ------------------------------------------------------------- | ----------------- |
| `Processing` | Accepted and queued, or in extraction | Not yet |
| `Finished` | Every section returned data | Ready to download |
| `Partial` | Some sections failed, others returned data | Incomplete |
| `Error` | Every section failed, or the request never reached the worker | None |
Poll [Check report status](/en/reference/reporter/check-report-status) or consume the terminal event. A `Partial` report carries a per-section outcome in its metadata, which names the data sources that did not answer: fix those data sources, then generate the report again. [Download a report](/en/reference/reporter/download-report) serves the artifact once the report is `Finished`.
## Time limits on the path
***
Each stage carries its own limit, and an operator sets them per deployment.
| Limit | Default | Applies to |
| ---------------------- | ----------- | --------------------------------- |
| Idempotency window | 30 seconds | Repeat detection on the request |
| Extraction timeout | 300 seconds | One extraction run |
| Extraction concurrency | 4 | Parallel queries inside one run |
| PDF conversion timeout | 90 seconds | One HTML-to-PDF conversion |
| PDF worker pool | 2 | Concurrent conversions per worker |
Volume limits bound the same run: 10 data sources, 50 tables per data source, 200 fields per table, and 100 MiB of extracted result. See [Environment variables](/en/reporter/reporter-environment-variables) for the settings behind each one.
## Where deadlines fit
***
A deadline is a due date with a recurrence rule, and it can name the template that satisfies it. It sits beside the generation path rather than on it.
A deadline never starts a report. You generate the report through the request path above, then mark the deadline delivered. Reporter recomputes a deadline on read. A recurring obligation that carries a delivery mark rolls forward to its next due date on the first read after the current date passes, and that roll-forward clears the mark. The notifications operation returns the deadlines inside their alert window when you ask for them. See [Managing deadlines](/en/reporter/managing-deadlines).
## Storage and download
***
Rendered files live in one S3-compatible bucket, under the `reports/` prefix. Reporter stores each file under the template identifier and names it after the report, so files stay grouped by the template that produced them.
Download returns the bytes with the content type of the output format and a filename built from the report identifier. The download works after the template is deleted, because the report holds its own format snapshot.
A report is created once and then retained. Its filters, its status, and its file describe exactly the request that produced them, which is what makes an old report evidence of what you reported at the time.
Reporter keeps rendered files in the bucket. Set retention with a lifecycle policy on the bucket itself.
## Next steps
***
The field map, the data context, and the two kinds of filter.
Upload a template, generate a report, and download the result.
Track a regulatory obligation and mark it delivered.
One binary, two surfaces, and the stores they share.
# Managing deadlines
Source: https://docs.lerian.studio/en/reporter/managing-deadlines
Track when regulatory and business reports are due with Reporter deadlines — create recurring obligations, monitor their status, and mark them delivered.
A **deadline** is Reporter's tracking layer for report deliveries. Where templates define *how* a report looks and report generation produces the *output*, a deadline records *when* a report is due and whether it has been delivered. Each deadline represents a delivery obligation — typically a regulatory filing or a recurring business report — and can optionally be tied to the template used to fulfill it.
Deadlines don't generate reports themselves. They sit alongside the [reporting lifecycle](/en/reporter/reporter-quick-start#the-reporting-lifecycle) as an operational tracker, so teams can see at a glance what's `pending`, `overdue`, or already `delivered`.
## Why deadlines exist
***
Report generation answers *how* and *what*. Deadlines answer *when* and *whether it was met*. Without a tracking layer, a team can produce perfect reports and still miss a filing window, because nothing in the reporting engine itself knows that a report was *due*.
Deadlines solve a delivery-compliance problem. They turn recurring reporting obligations into tracked, dated commitments so that nothing slips silently:
* **Regulatory reporting obligations** — many filings must reach a regulator on a fixed schedule. A deadline records that obligation, its recurrence, and its delivery state, so a missed or late filing is visible before it becomes a compliance incident.
* **Internal SLAs** — recurring business reports often carry internal commitments ("finance receives the monthly close pack by the 5th"). Deadlines make those commitments explicit and measurable.
* **Audit trails for delivery** — because each deadline records `deliveredAt` and moves through `pending` → `overdue` → `delivered`, it leaves an auditable history of *when* each obligation was satisfied, not just that a report exists.
## Who uses deadlines
***
Deadlines are a business and compliance tool layered on top of the reporting engine. Typical users include:
* A **fintech delivering regulatory reports to BACEN** on fixed monthly or annual schedules, using deadlines to guarantee each filing window is tracked and met.
* A **treasury or finance team** tracking recurring monthly report deliveries, using the calendar and status view to confirm each routine output went out on time.
* A **compliance officer** monitoring overdue obligations across the organization, filtering by `overdue` status to catch anything at risk before it escalates.
For these teams, the value isn't in producing the report — that's the reporting engine's job — but in *knowing the obligation landscape*: what's coming up, what's late, and what's done.
## How deadlines fit the Reporter workflow
***
Deadlines wrap around reports and data sources to add a **delivery status layer** on top of the reporting engine. Data Sources provide the data, Templates define the output, and the reporting lifecycle produces the file. A deadline sits above all of that: it links (optionally) to the template that fulfills the obligation, watches the due date, and exposes a single status that tells the business whether the obligation has been met.
In practice this means deadlines are *non-intrusive*. They never trigger, generate, or submit a report — they observe and record. You still generate reports through the normal lifecycle; the deadline is simply the tracker that turns that activity into a clear delivery status the rest of the organization can rely on.
## What a deadline tracks
***
Each deadline captures the due date for a report obligation plus the metadata your team needs to manage it:
| Field | Description |
| ------------------ | ------------------------------------------------------------------------------------------ |
| `name` | Human-readable name of the deadline (for example, *Monthly Regulatory Report*). |
| `description` | Optional longer description of the obligation. |
| `type` | Classification of the deadline, such as `regulatory` or `custom`. |
| `frequency` | How often the deadline recurs, such as `monthly` or `annual`. |
| `dueDate` | When the report is due, in RFC 3339 format. |
| `color` | Hex color used to visually identify the deadline in dashboards. |
| `notifyDaysBefore` | Number of days before the due date when notifications start. |
| `monthsOfYear` | Months (1–12) in which the deadline applies. |
| `templateId` | Optional UUID of the [template](/en/reporter/using-reporter) used to fulfill the deadline. |
| `active` | Whether the deadline is currently active. |
Reporter also maintains read-only fields on each deadline — `id`, `status` (`pending`, `overdue`, or `delivered`), `deliveredAt`, `templateName`, `createdAt`, and `updatedAt`.
Include an `Authorization: Bearer ` header on every deadline request when [Access Manager](/en/platform/access-manager/access-manager) is enabled in your environment.
## Creating a deadline
***
Create a deadline with the [Create a Deadline](/en/reference/reporter/create-deadline) endpoint (`POST /v1/deadlines`).
The required fields are `name`, `type`, `frequency`, `dueDate`, and `color`. The remaining fields are optional — set `templateId` to link the deadline to the template that fulfills it, and `notifyDaysBefore` to control when reminders begin.
```json theme={null}
{
"name": "Monthly Regulatory Report",
"description": "Monthly regulatory compliance report",
"type": "regulatory",
"frequency": "monthly",
"dueDate": "2026-03-31T23:59:59Z",
"color": "#FF5733",
"notifyDaysBefore": 5,
"monthsOfYear": [1, 6],
"templateId": "00000000-0000-0000-0000-000000000000",
"active": true
}
```
A successful request returns `201 Created` with the full deadline, including its generated `id` and an initial `status`.
API reference: [Create a Deadline](/en/reference/reporter/create-deadline)
## Listing deadlines
***
Retrieve deadlines with the [Retrieve Deadlines](/en/reference/reporter/retrieve-deadlines) endpoint (`GET /v1/deadlines`). Results are paginated and can be filtered by status.
| Query parameter | Description | Default |
| --------------- | ----------------------------------------------- | ------- |
| `status` | Filter by `pending`, `overdue`, or `delivered`. | — |
| `limit` | Number of records per page (integer ≥ 1). | `10` |
| `page` | Page number to return (integer ≥ 1). | `1` |
For example, to list overdue deadlines, ten per page:
```
GET /v1/deadlines?status=overdue&limit=10&page=1
```
The response contains an `items` array plus `page`, `limit`, and `total` for pagination.
API reference: [Retrieve Deadlines](/en/reference/reporter/retrieve-deadlines)
## Updating a deadline
***
Update an existing deadline with the [Update a Deadline](/en/reference/reporter/update-deadline) endpoint (`PATCH /v1/deadlines/{id}`). Only the fields included in the request body are changed, so you can send a partial payload — for example, to push back a due date or deactivate a deadline:
```json theme={null}
{
"dueDate": "2026-06-30T23:59:59Z",
"notifyDaysBefore": 10,
"active": false
}
```
A successful request returns `200 OK` with the updated deadline.
API reference: [Update a Deadline](/en/reference/reporter/update-deadline)
## Deleting a deadline
***
Remove a deadline you no longer need to track with the [Delete a Deadline](/en/reference/reporter/delete-deadline) endpoint (`DELETE /v1/deadlines/{id}`). A successful request returns `204 No Content`.
API reference: [Delete a Deadline](/en/reference/reporter/delete-deadline)
## Marking a deadline as delivered
***
When the report behind a deadline has been filed or sent, mark the deadline as delivered with the [Deliver a Deadline](/en/reference/reporter/deliver-deadline) endpoint (`PATCH /v1/deadlines/{id}/deliver`). This is the action that closes the loop in the tracking workflow: it moves the deadline's `status` to `delivered` and stamps `deliveredAt`.
```json theme={null}
{
"delivered": true
}
```
Because `delivered` is a boolean, the same endpoint can also reverse the action — send `"delivered": false` to reopen a deadline that was marked delivered by mistake. A successful request returns `200 OK` with the updated deadline.
Delivering a deadline is a tracking action only. It records that the obligation was met — it does not generate or submit the underlying report. Generate the report through the [reporting lifecycle](/en/reporter/reporter-quick-start), then mark the deadline delivered to keep your tracker accurate.
API reference: [Deliver a Deadline](/en/reference/reporter/deliver-deadline)
## How deadlines fit the workflow
***
A typical obligation moves through these states:
Register the obligation with its due date, frequency, and optional template.It shows as `pending` until the due date, then `overdue` if the report hasn't been delivered in time.Produce the report through the normal [reporting lifecycle](/en/reporter/reporter-quick-start), using the linked template.Call the deliver endpoint to set `status` to `delivered` and record `deliveredAt`.
## Next steps
***
Walk through the reporting lifecycle that deadlines track.
Build the templates that fulfill your deadlines.
Ready-to-use templates for Brazilian regulatory reporting.
Full request and response reference for every deadline endpoint.
# Reporter architecture
Source: https://docs.lerian.studio/en/reporter/reporter-architecture
How Reporter runs: one binary whose RUN_MODE selects the API surface, the worker surface, or both, the queue between them, and the stores they share.
Reporter ships as **one binary**. `RUN_MODE` selects which surfaces that binary serves: `api`, `worker`, or `all`. The API and the worker are two roles of the same program, not two products, and they are built, versioned, and released together.
That single fact shapes everything else on this page. You choose a topology at deploy time by setting an environment variable, not by assembling separate services.
## Two surfaces, one binary
***
| Surface | `RUN_MODE` | Serves |
| ------- | ---------- | ------------------------------------------------------------------------------------------------------------------- |
| API | `api` | Every REST operation on port `4005`, plus `/health`, `/readyz`, and `/version`. |
| Worker | `worker` | No REST surface. Consumes the report queue. A small health server on `HEALTH_PORT` carries `/health` and `/readyz`. |
| Both | `all` | Both surfaces in one process. |
```bash theme={null}
# API surface
RUN_MODE=api
SERVER_ADDRESS=:4005
# Worker surface
RUN_MODE=worker
HEALTH_PORT=4006
```
Run `all` when one process is enough, which is the usual choice for local development and small deployments. Split the roles into two deployables when report generation needs to scale on its own: rendering a large PDF costs far more than accepting the request that asked for it, and separate deployments let you size each side to its own load.
## The path a report takes
***
A `POST` to `/v1/reports` names a template and its filters. Reporter takes an idempotency lock first, keyed on the `X-Idempotency` header when you send one and on a hash of the request body when you do not. A duplicate that is still in flight is rejected; a duplicate of a completed request replays the original report and marks the response as a replay.
Reporter loads the template's field map and output format, checks the filter fields against the live schema of each data source, and writes the report with status `Processing`.
It publishes a command message to the internal RabbitMQ queue and answers `201 Created` with the `Processing` report. The caller is done here. Rendering has not started.
The worker consumes the command, skips it if the report already settled as `Finished` or `Error`, loads the template from object storage, extracts the data for each data source, renders the document, converts it to PDF when the format calls for it, and uploads the artifact.
It writes `Finished`, `Partial`, or `Error` and emits the matching event. The download operation serves the artifact once the report is `Finished`.
## The queue between them
***
The API and the worker communicate over one RabbitMQ queue that carries report commands in one direction, with a dead-letter queue behind it. This queue is private plumbing between the two surfaces of the same binary. It is not an integration point, it carries no contract you should build against, and its exchange, queue, and routing key are all operator-configured.
Business events are a separate channel. Reporter publishes them to their own exchange, which the operator configures and which the deployment's reference value names `reporter.events`. Every emission happens after the database commit and never fails the work that produced it. High-value events go through a durable outbox rather than a direct publish, so a broker outage delays them instead of losing them.
## Data extraction
***
The worker does not talk to your databases through hand-written queries. It runs the same extraction engine that powers [Fetcher](/en/fetcher/fetcher-core-concepts), embedded in process, with no network hop to a separate service.
The engine bounds every run. The defaults allow 10 data sources per report, 50 tables per data source, 200 fields per table, four data sources extracted in parallel, and a five-minute deadline for the whole extraction. Failures are collected rather than fatal: a report whose sections partly succeed renders from what it has and settles as `Partial`, recording which sections failed.
## Stores and artifacts
***
| Dependency | Role |
| ---------------------------- | ------------------------------------------------------------------ |
| MongoDB | Templates, reports, deadlines, and the event outbox. |
| RabbitMQ | The internal report queue and the business-event exchange. |
| Redis or Valkey | Idempotency locks, the schema cache, and tenant lifecycle signals. |
| S3-compatible object storage | Template files and rendered artifacts, in one bucket. |
Object storage is S3-compatible: Reporter speaks the S3 protocol and any service that answers it works as a target. Template sources land under the `templates/` prefix, and rendered artifacts under `reports/`, keyed by template and report identifier with the output format as the extension. Reporter applies no expiry of its own, so retention is a lifecycle policy you set on the bucket.
## Reporter and Midaz
***
Midaz is a **data source** to Reporter, not a runtime dependency. Reporter carries no Midaz-specific code: a Midaz database is registered with the same `DATASOURCE_*` variables as any other PostgreSQL database, and templates address it by the `configName` the operator chose.
Two consequences follow. Reporter starts and serves templates, deadlines, and metrics with no data source configured at all, so a Midaz outage never stops Reporter from running. And the same deployment can report over Midaz, over your own databases, and over both in one template.
Point Reporter at a read replica where one exists. The extraction path only reads, but the query load of a heavy report is real, and a replica keeps it off the ledger's write path.
See [Connecting Reporter to Midaz](/en/reporter/connecting-reporter-to-midaz) for the variables, the `configName` convention, and how to confirm the source is visible.
## Health and readiness
***
Both surfaces expose `/health` and `/readyz` before authentication, so probes reach them without a token. `/health` answers 503 until the startup self-probe succeeds, which lets an orchestrator restart a pod that could not reach its dependencies at boot. `/readyz` reports the dependencies that surface actually uses, and its response states which deployment mode the process is running in. The API surface also serves `/version` with the build provenance.
Read the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract shared across Lerian products.
## Next steps
***
Templates, data sources, reports, deadlines, and the four report states.
Every setting that shapes a Reporter deployment.
# BACEN regulatory formats
Source: https://docs.lerian.studio/en/reporter/reporter-bacen-templates
Build BACEN-compliant regulatory reports — CADOC and CCS layouts — from Reporter templates that align with official BACEN validation rules and schedules.
This section provides practical examples of Brazilian financial templates, focusing on regulatory reports required by the **Central Bank of Brazil (BACEN)**.
The **Reporter** product helps you build these reports efficiently. Instead of starting from scratch, you can use the following guides to create templates that meet BACEN's official layout and validation rules.
Each guide shows how to model your data, define XML structures, and use Reporter to generate compliant files. You can adapt these templates to your own business context while keeping them aligned with CADOC, CCS, and APIX standards.
## Template examples
***
Here you will find step-by-step guides for some of the most common BACEN regulatory reports:
Each page includes detailed examples, schema explanations, and sample outputs that you can generate through Reporter.
These examples follow the official layouts and data models published by the Central Bank of Brazil.
Use them as implementation guides to help you design compliant templates for your own reporting needs.
# Reporter core concepts
Source: https://docs.lerian.studio/en/reporter/reporter-core-concepts
The Reporter model in one place: templates, data sources, reports, deadlines, the four report states, and the tenant boundary that separates them.
Reporter has a small model. You upload a **template** that describes a document. An operator configures the **data sources** Reporter may read. You request a **report** against a template, and Reporter renders an artifact you download. A **deadline** tracks when a report is due and whether someone delivered it.
This page defines each noun and the rules that connect them.
## The model at a glance
***
| Concept | Definition |
| ----------- | -------------------------------------------------------------------------------------- |
| Template | A plain-text document definition, uploaded as a file, that declares one output format. |
| Data source | A read-only database that an operator registers through configuration. |
| Report | One rendering of one template, under the filters you asked for. |
| Artifact | The file a finished report produces, held in object storage. |
| Filter | A per-field condition that narrows the rows a report reads. |
| Deadline | A due date and a recurrence rule, optionally bound to a template. |
| Tenant | The isolation boundary. Every operation resolves exactly one. |
## Templates
***
A template is a plain-text `.tpl` file that you upload as `multipart/form-data`, together with the output format it produces and a short description. Reporter keeps the file in object storage and its metadata in MongoDB. The template language is Pongo2, so a template mixes literal document text with variables, loops, conditionals, filters, and aggregation tags.
One template declares one output format: `PDF`, `HTML`, `XML`, `TXT`, or `CSV`. PDF renders as HTML first and then converts through a headless-browser worker pool.
A template addresses data by `configName`, the stable name of a data source, and by table: `{{ midaz_onboarding.accounts }}`. When more than one schema is in play, qualify it as `{{ midaz_onboarding:public.accounts }}`.
At upload, Reporter reads the template text and derives the fields it touches, per data source and per table. It then checks every referenced table and field against the live schema of that data source. A data source that is unreachable produces a warning rather than a rejection, so a template still uploads while a database is down.
Read [Template reference](/en/reporter/template-reference) for the tags, filters, and expressions the language offers.
## Data sources
***
A data source is a database Reporter reads, registered entirely through environment configuration. Reporter connects to PostgreSQL and MongoDB. Access is read-only by construction: the extraction path issues `SELECT` statements and MongoDB finds, and no write path to a data source exists.
`DATASOURCE__CONFIG_NAME` is the variable that makes a data source exist. Its value is the `configName` that templates and filters use, and it stays stable while hosts and credentials change around it. The rest of the block supplies the connection itself.
The API surface over data sources is read-only too. You can list the configured sources and get one by identifier, which is how you confirm that a deployment sees the source a template expects. There is no create, update, or delete operation, because the environment owns that decision.
Read [Connecting Reporter to Midaz](/en/reporter/connecting-reporter-to-midaz) for a worked configuration.
## Reports
***
A report request names a template and, optionally, the filters that narrow the data behind it. Filters nest three levels — data source, then table, then field — and each field carries one condition:
```json theme={null}
{
"templateId": "0198c0f6-6d4d-7a34-a9ba-2c2b3ad2f1a0",
"filters": {
"midaz_onboarding": {
"accounts": {
"status": { "in": ["ACTIVE"] },
"type": { "nin": ["internal"] }
}
}
}
}
```
Eight operators exist: `eq`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, and `nin`.
A report keeps its own copy of the output format and description that the template carried when the report was created. That snapshot is why an artifact stays downloadable in its original shape after the template moves on.
Reports are create-and-retain. Four operations exist: create one, get it by identifier, list them, and download the artifact of a finished one. A report and its artifact remain in place once created; retention is a lifecycle policy on the object-storage bucket, set by whoever operates the deployment.
### The four report states
Creation is synchronous only up to the point where the work is queued. `POST` answers `201 Created` with a report already in `Processing`, and a worker takes it from there.
| State | Meaning |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `Processing` | Accepted and queued. The worker has not finished. |
| `Finished` | Every data section succeeded and the artifact is stored. |
| `Partial` | Some data sections failed. The report records which ones, by section, so you can fix those data sources and generate the report again. |
| `Error` | Every attempted section failed, or the request never reached the worker. |
`Processing` is the only entry state, and nothing returns to it once the worker settles the report. A queue can deliver the same command twice, so the worker reads the report before it starts a run: one that already settled as `Finished` or `Error` is left as it is, rather than generated again.
The download operation serves the artifact once a report is `Finished`. Poll `GET /v1/reports/{id}` until the state settles, or subscribe to the terminal events Reporter emits and skip the polling.
## Deadlines
***
A deadline models an obligation: a due date, a recurrence rule, and optionally the template that satisfies it. Its `type` is `regulatory` or `custom`. Its `frequency` is `once`, `daily`, `weekly`, `monthly`, `semiannual`, or `annual`, and the semiannual and annual rules also carry the months they fall in.
Status is derived, never stored. A deadline is `delivered` once someone marks it so, `overdue` once its due date passes undelivered, and `pending` otherwise. A recurring deadline rolls forward on the first read after its due date passes, and only when it was marked delivered. That roll-forward clears the delivery mark, so the record returns to `pending` for the next occurrence and one record carries the whole series.
Notification is pull-based. A single operation returns the deadlines inside their alert window, ranked overdue first, then warning, then informational. Reporter sends no mail and calls no endpoint of yours.
Read [Managing deadlines](/en/reporter/managing-deadlines) for the full lifecycle.
## Tenants
***
The tenant is the isolation boundary in Reporter. Reporter resolves it from the authenticated request itself — the bearer token that authorizes the call carries the tenant in its `tenantId` claim. No API operation takes a tenant from a request header, and no client-supplied value can widen the scope of a call.
Isolation runs the whole depth of the stack. Each tenant gets its own metadata database, its own message-broker virtual host, its own connection pools, and its own event outbox. Resolution is fail-closed: a request whose tenant cannot be resolved is rejected, and nothing falls back to a shared pool.
Single-tenant operation is the default and needs no tenant configuration at all. See [Multi-tenancy](/en/multi-tenancy) for the platform-wide model.
## Next steps
***
One binary, two surfaces, and the queue between them.
Upload a template and generate your first report.
The tags, filters, and expressions available to a template.
Create, track, and deliver a reporting obligation.
# Reporter deployment
Source: https://docs.lerian.studio/en/reporter/reporter-deployment
Deploy Reporter: one binary with a run-mode selector, MongoDB, the report command queue, S3-compatible object storage, Redis or Valkey, worker sizing, and report retention through a bucket lifecycle policy.
Reporter ships as a single binary with two surfaces. `RUN_MODE` selects which surfaces a process serves, so the same image runs as the API, as the report worker, or as both. Four dependencies sit behind them.
Reporter reads your databases through an extraction engine that runs inside the worker process. There is no separate extraction service to deploy.
## What you deploy
***
| Component | Role | Scale on |
| ------------------- | ---------------------------------------------------------------------------------------- | ------------------------------ |
| **API surface** | REST API for templates, reports, deadlines, and data sources. Publishes report commands. | Request rate. |
| **Report worker** | Queue consumer. Extracts the data, renders the report, and writes the artifact. | Queue depth. |
| **MongoDB** | Templates, reports, deadlines, and the durable event outbox. | Metadata volume. |
| **RabbitMQ** | The report command queue, and the business event exchange. | Report rate. |
| **Object storage** | Template sources and rendered reports. S3-compatible. | Artifact volume and retention. |
| **Redis or Valkey** | Idempotency locks, schema cache, and tenant lifecycle. | Both surfaces. |
## Run modes
***
`RUN_MODE=api` serves every REST operation, plus `/health`, `/readyz`, and `/version`, on the address in `SERVER_ADDRESS`. `RUN_MODE=worker` consumes the report command queue and serves `/health` and `/readyz` on `HEALTH_PORT`. `RUN_MODE=all` runs both surfaces in one process.
Use `all` for local development. In production, deploy the two surfaces separately, so report generation scales on its own.
```bash theme={null}
# API deployment
RUN_MODE=api
SERVER_ADDRESS=:4005
# Worker deployment
RUN_MODE=worker
HEALTH_PORT=4006
```
## MongoDB
***
Both surfaces use the same MongoDB deployment. The API surface writes templates, reports, and deadlines; the worker updates a report as it completes.
In single-tenant mode, `MONGO_HOST` and `MONGO_NAME` are required at startup. In multi-tenant mode, each tenant gets its own database, resolved from the JWT on the request. That path fails closed: a request that carries a tenant with no tenant database returns an error instead of touching a shared database.
## RabbitMQ
***
Two separate concerns share one broker.
### The report command queue
This queue carries work from the API surface to the worker. `RABBITMQ_EXCHANGE`, `RABBITMQ_GENERATE_REPORT_QUEUE`, and `RABBITMQ_GENERATE_REPORT_KEY` name the exchange, the queue, and the routing key. The API surface publishes, so it needs all three. The worker only consumes, so it needs `RABBITMQ_GENERATE_REPORT_QUEUE` alone. Both surfaces need the broker connection itself, and the objects must exist before the services start.
The channel is internal to Reporter. To learn that a report finished, subscribe to the business events below, or poll the report.
A worker that fails a message retries it up to five times with backoff, and then rejects it without requeue. Bind a dead-letter exchange to the command queue, so a rejected report lands somewhere you can inspect.
### The events exchange
Business events (`template.*`, `report.*`, `deadline.*`) go to an exchange you name in `RABBITMQ_REPORT_EVENTS_EXCHANGE`. The value is operator-configured; `reporter.events` is the reference value.
Set that exchange, `STREAMING_BROKERS`, and `STREAMING_CLOUDEVENTS_SOURCE` whenever `STREAMING_ENABLED=true`. With streaming off, both surfaces start normally and publish nothing.
## Object storage
***
Reporter uses one S3-compatible bucket, named in `OBJECT_STORAGE_BUCKET`. It holds two kinds of object, each under its own prefix:
* the template source, as `templates/.tpl`
* the rendered report, as `reports//.`
In multi-tenant mode, both prefixes sit under the tenant that owns the object: `/templates/...` and `/reports/...`.
AWS S3, MinIO, and SeaweedFS all work. Reach SeaweedFS through its S3 gateway. `OBJECT_STORAGE_USE_PATH_STYLE=true` is what MinIO and SeaweedFS expect, and `OBJECT_STORAGE_DISABLE_SSL` stays `false` outside local development.
### Report retention
Reporter keeps every report it renders, so the bucket grows with your report volume. Set expiry on the bucket, with the object store's own lifecycle policy, over the window your retention rules require.
**Anchor the rule on the report prefix that your tenancy mode produces.** In single-tenant mode every report key starts at `reports/`, so one rule on that prefix covers the bucket. In multi-tenant mode the key carries the tenant ahead of it, so the rule needs the full `/reports/` prefix, one rule per tenant. Keep `templates/` out of scope either way: a rule that covers the whole bucket also removes the templates your reports are rendered from.
## Redis or Valkey
***
`REDIS_HOST` is required on the API surface. On the worker it is required only when `MULTI_TENANT_ENABLED=true`, where it caches tenant discovery. Redis backs the idempotency lock on report creation, the datasource schema cache, and tenant lifecycle messages in multi-tenant mode.
Idempotency state lives in Redis, not in process memory, so API replicas share it. The same report request sent twice, to two replicas, creates one report.
## Data sources
***
Reporter reads reporting data from PostgreSQL and MongoDB data sources declared in the environment, one `DATASOURCE_{NAME}_*` block per source. See [Environment variables](/en/reporter/reporter-environment-variables) for the block.
There is no API that registers a data source, so a new source is a configuration change and a restart. Reporter also starts with none configured, and serves templates, deadlines, and metrics.
## Sizing the worker
***
`RABBITMQ_NUMBERS_OF_WORKERS` sets how many report jobs one worker process runs in parallel. For horizontal scale, add worker replicas and drive them from the depth of the command queue.
PDF output renders through a headless-browser pool: `PDF_POOL_WORKERS` concurrent renders, which defaults to 2, each bounded by `PDF_TIMEOUT_SECONDS`, which defaults to 90. Size worker memory against that pool, not only against the rows a report reads. The other output formats do not use it.
Every report also carries fixed extraction limits. They are 10 data sources, 50 tables per data source, and 200 fields per table, with 4 data sources read at a time, a 300-second deadline, and a 100 MiB cap on extracted data. The `ENGINE_*` variables change them.
## Deployment mode and TLS
***
`DEPLOYMENT_MODE` declares the flavor of the deployment: `local`, `byoc`, or `saas`. It tags the `/readyz` response, and in `saas` it enforces TLS.
**SaaS mode requires TLS on every dependency.** Set `DEPLOYMENT_MODE=saas` and a plaintext MongoDB, RabbitMQ, Redis, object storage, or Tenant Manager URL stops the process. It stops before any connection opens.
Leave `ALLOW_INSECURE_TLS` unset in production. It bypasses those checks, and the local development stack is the only place for it.
## Startup checks
***
Reporter validates its configuration before it serves anything. Each check below stops the process, and the error names every variable at fault.
| Trigger | What the operator sees |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `SERVER_ADDRESS`, `REDIS_HOST`, or any of the three command-queue variables missing on the API surface | Config validation fails and lists each missing variable. |
| `RABBITMQ_GENERATE_REPORT_QUEUE` missing on the worker surface | Config validation fails and names it. |
| `MONGO_HOST` or `MONGO_NAME` missing, single-tenant mode | Config validation names them. |
| `MULTI_TENANT_ENABLED=true` without `MULTI_TENANT_URL` or `MULTI_TENANT_SERVICE_API_KEY` | Config validation names the variable the tenant runtime needs. |
| `STREAMING_ENABLED=true` without `RABBITMQ_REPORT_EVENTS_EXCHANGE`, `STREAMING_BROKERS`, and `STREAMING_CLOUDEVENTS_SOURCE` — all three are required for a streaming-enabled start | Startup aborts rather than run with events that go nowhere. |
| `DEPLOYMENT_MODE=saas` with a plaintext dependency URL | Startup aborts before any connection opens. |
**A dependency that is down at boot does not produce a silently broken pod.** `/health` answers 503 until the startup self-probe succeeds. The kubelet then restarts the pod instead of sending it traffic.
## Rolling updates
***
On `SIGTERM`, both surfaces enter a drain. `/readyz` answers 503 from the moment the signal arrives, before the servers begin shutdown, and in-flight requests and in-flight messages finish. Kubernetes removes the pod from Service endpoints while it still works.
Set the termination grace period above your longest report render. See the [health and readiness reference](/en/reference/health-and-readiness) for what the probes report during a drain.
## Next steps
***
Every Reporter variable, by category.
The configuration blocks every Lerian product shares.
The probe contract and what each response means.
Point Reporter at a Midaz database and render your first report.
# Environment variables
Source: https://docs.lerian.studio/en/reporter/reporter-environment-variables
Reference the environment variables used to configure Reporter, organized by category — HTTP server, database, storage, authentication, and telemetry.
This reference lists the environment variables used to configure **Reporter**, the service that generates regulatory, compliance, and accounting reports from configurable templates. Reporter ships as a single binary whose active surfaces are selected by `RUN_MODE` — the API manager, the report worker, or both. You set these at deploy time, through Helm values, Docker Compose, or your orchestrator's environment. Variables marked as required cause the server to fail on startup if not set.
For the configuration blocks that every Lerian product shares — TLS posture, OpenTelemetry, Access Manager authentication, multi-tenancy, service discovery, and event streaming — see the [BYOC configuration reference](/en/reference/byoc-configuration). This page focuses on what is distinctive to Reporter.
## Run mode and ports
`RUN_MODE` decides which surfaces the process serves. Run the API and worker as one process (`all`) for small deployments, or split them into separate deployables (`api` and `worker`) to scale report generation independently. See the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract.
| Variable | Description | Default | Required |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------- |
| `RUN_MODE` | Surfaces to run: `api`, `worker`, or `all` | `all` | No |
| `SERVER_PORT` / `SERVER_ADDRESS` | API bind address (`RUN_MODE=api`/`all`), read from `SERVER_ADDRESS`; `SERVER_PORT` is the convention used to build it (`:4005`). Serves `/health`, `/readyz`, `/version`. | — | Yes (`api`/`all`) |
| `HEALTH_PORT` | Worker health port (`RUN_MODE=worker`). Serves `/health`, `/readyz`. | `4006` | No |
## Deployment and TLS
| Variable | Description | Default | Required |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `DEPLOYMENT_MODE` | Deployment flavor: `local`, `byoc`, or `saas`. In `saas`, TLS is mandatory for every dependency connection and the server refuses to start without it. In `byoc`, TLS is recommended and warned-on rather than enforced. Also tags the `/readyz` response. | `local` | No |
| `ALLOW_INSECURE_TLS` | Bypass per-connection TLS enforcement on infrastructure DSNs. Leave unset or `false` in production. | `false` | No |
## CORS and proxies
| Variable | Description | Default | Required |
| ---------------------- | ------------------------------------------------------------------------------------------- | ------- | -------- |
| `CORS_ALLOWED_ORIGINS` | Allowed CORS origins (CSV, or `*`). Lock to explicit origins in production. | `*` | No |
| `CORS_ALLOWED_METHODS` | Allowed CORS methods | varies | No |
| `CORS_ALLOWED_HEADERS` | Allowed CORS headers | varies | No |
| `TRUSTED_PROXIES` | Trusted proxy CIDRs for `X-Forwarded-For` parsing. Set when running behind a load balancer. | — | No |
| `SWAGGER_ENABLED` | Serve the OpenAPI/Swagger UI | `false` | No |
## API pagination
| Variable | Description | Default | Required |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------- | -------- |
| `MAX_PAGINATION_LIMIT` | Largest `limit` a list operation accepts. A request above the ceiling is rejected with a pagination error rather than clamped. | `100` | No |
## Database (MongoDB)
Stores report metadata, templates, and run history.
| Variable | Description | Default | Required |
| --------------------- | ---------------------------------------------------------------------- | --------- | -------- |
| `MONGO_URI` | Connection scheme: `mongodb` or `mongodb+srv` | `mongodb` | No |
| `MONGO_HOST` | MongoDB host | — | Yes |
| `MONGO_PORT` | MongoDB port | — | Yes |
| `MONGO_USER` | Database user | — | Yes |
| `MONGO_PASSWORD` | Database password. Sensitive. | — | Yes |
| `MONGO_NAME` | Database name | — | Yes |
| `MONGO_MAX_POOL_SIZE` | Maximum connection pool size | varies | No |
| `MONGO_TLS_CA_CERT` | Base64-encoded PEM CA certificate for TLS (for example AWS DocumentDB) | — | No |
## Message broker (RabbitMQ)
Carries the generate-report command queue between the API and the worker.
| Variable | Description | Default | Required |
| ----------------------------- | ---------------------------------------------------------------- | ------- | -------- |
| `RABBITMQ_HOST` | Broker host | — | Yes |
| `RABBITMQ_PORT_AMQP` | AMQP port | — | Yes |
| `RABBITMQ_PORT_HOST` | Management/HTTP port | — | No |
| `RABBITMQ_URI` | Connection scheme: `amqp` or `amqps`. Use `amqps` in production. | `amqp` | No |
| `RABBITMQ_DEFAULT_USER` | Broker user | — | Yes |
| `RABBITMQ_DEFAULT_PASS` | Broker password. Sensitive. | — | Yes |
| `RABBITMQ_NUMBERS_OF_WORKERS` | Worker consumer concurrency | `5` | No |
## Object storage (S3-compatible)
Where rendered reports are stored. Works with any S3-compatible endpoint.
| Variable | Description | Default | Required |
| ------------------------------- | ----------------------------------------------------------------- | ------------------ | -------- |
| `OBJECT_STORAGE_ENDPOINT` | S3-compatible endpoint URL | — | Yes |
| `OBJECT_STORAGE_REGION` | Storage region | `us-east-1` | No |
| `OBJECT_STORAGE_BUCKET` | Bucket for rendered reports | `reporter-storage` | No |
| `OBJECT_STORAGE_ACCESS_KEY_ID` | Access key ID. Sensitive. | — | Yes |
| `OBJECT_STORAGE_SECRET_KEY` | Secret access key. Sensitive. | — | Yes |
| `OBJECT_STORAGE_USE_PATH_STYLE` | Use path-style addressing (needed by some S3-compatible stores) | `false` | No |
| `OBJECT_STORAGE_DISABLE_SSL` | Disable TLS to the storage endpoint. Leave `false` in production. | `false` | No |
## Cache (Redis / Valkey)
| Variable | Description | Default | Required |
| ------------------- | -------------------------------------------------------------- | ------- | -------- |
| `REDIS_HOST` | Redis/Valkey host and port | — | Yes |
| `REDIS_PASSWORD` | Authentication password. Sensitive. | — | No |
| `REDIS_DB` | Logical database index | `0` | No |
| `REDIS_PROTOCOL` | RESP protocol version | varies | No |
| `REDIS_TLS` | Enable TLS for the connection | `false` | No |
| `REDIS_CA_CERT` | Base64-encoded PEM CA certificate for TLS | — | No |
| `REDIS_MASTER_NAME` | Sentinel master name (Sentinel deployments) | — | No |
| `REDIS_USE_GCP_IAM` | Authenticate to GCP Memorystore with IAM instead of a password | `false` | No |
## PDF rendering (worker)
| Variable | Description | Default | Required |
| --------------------- | --------------------------------------- | ------- | -------- |
| `PDF_POOL_WORKERS` | Concurrent PDF render workers | `2` | No |
| `PDF_TIMEOUT_SECONDS` | Per-report PDF render timeout (seconds) | `90` | No |
## Report datasources
Reports read from PostgreSQL and MongoDB datasources declared in the environment, one block per source. Replace `{NAME}` with your datasource identifier, and use the same identifier in every key of the block.
`DATASOURCE_{NAME}_CONFIG_NAME` is the variable that makes a block exist. Reporter discovers datasources by scanning the environment for keys that match `DATASOURCE_*_CONFIG_NAME` — the prefix counts as much as the suffix, so a key that only ends in `_CONFIG_NAME` declares nothing. Reporter reads the rest of a block only for a name it found that way. The value is the name your templates use to address the source.
Within a block, the variables marked required are the ones Reporter needs before it reads the block.
| Variable | Description | Default | Required |
| ------------------------------- | ----------------------------------------------------------------------- | -------- | -------- |
| `DATASOURCE_{NAME}_CONFIG_NAME` | Name that templates use to address this datasource. Declares the block. | — | Yes |
| `DATASOURCE_{NAME}_TYPE` | Datasource engine: `postgresql` or `mongodb` | — | Yes |
| `DATASOURCE_{NAME}_HOST` | Datasource host | — | Yes |
| `DATASOURCE_{NAME}_PORT` | Datasource port | — | Yes |
| `DATASOURCE_{NAME}_DATABASE` | Database name | — | Yes |
| `DATASOURCE_{NAME}_USER` | Datasource user | — | No |
| `DATASOURCE_{NAME}_PASSWORD` | Datasource password. Sensitive. | — | No |
| `DATASOURCE_{NAME}_SCHEMAS` | Schemas to expose from the datasource (CSV) | `public` | No |
| `DATASOURCE_{NAME}_SSLMODE` | SSL mode for a PostgreSQL connection | — | No |
| `DATASOURCE_{NAME}_SSLROOTCERT` | Path to the PostgreSQL SSL root certificate | — | No |
| `DATASOURCE_{NAME}_SSL` | Enable TLS on a MongoDB connection | — | No |
| `DATASOURCE_{NAME}_SSLCA` | Path to the MongoDB CA certificate file | — | No |
| `DATASOURCE_{NAME}_OPTIONS` | Extra MongoDB URI options | — | No |
| `CRYPTO_HASH_SECRET_KEY_CRM` | Hashing key to read CRM datasource PII. Sensitive. | — | No |
| `CRYPTO_ENCRYPT_SECRET_KEY_CRM` | Encryption key to read CRM datasource PII. Sensitive. | — | No |
A complete block. We recommend that you set the `CONFIG_NAME` value to the same string as the `{NAME}` segment, as every example on this page does — one name across the environment and your templates:
```bash theme={null}
DATASOURCE_ONBOARDING_CONFIG_NAME=onboarding
DATASOURCE_ONBOARDING_TYPE=postgresql
DATASOURCE_ONBOARDING_HOST=midaz-postgres-replica
DATASOURCE_ONBOARDING_PORT=5702
DATASOURCE_ONBOARDING_DATABASE=onboarding
DATASOURCE_ONBOARDING_USER=reporter
DATASOURCE_ONBOARDING_PASSWORD=
DATASOURCE_ONBOARDING_SCHEMAS=public
```
A template then addresses that source by its config name, as in `{{ onboarding.accounts }}`. Datasources are declared only in the environment, so adding one is a configuration change and a restart.
## Shared configuration backbone
The following blocks are identical across Lerian products and are documented in full in the [BYOC configuration reference](/en/reference/byoc-configuration). They default off.
* **Access Manager authentication** — `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_ADDRESS`. Enable in production.
* **Multi-tenancy** — `MULTI_TENANT_*`, plus `RABBITMQ_MULTI_TENANT_SYNC_INTERVAL` and `RABBITMQ_MULTI_TENANT_DISCOVERY_TIMEOUT`. Off by default.
* **Service discovery** — `SD_*` (Consul; Reporter also accepts the legacy `SD_ADVERTISE_*` / `CONSUL_ADDR` aliases). Off by default.
* **Event streaming** — `STREAMING_ENABLED`, `STREAMING_BROKERS`, `STREAMING_CLOUDEVENTS_SOURCE`, plus `RABBITMQ_REPORT_EVENTS_EXCHANGE` for the events exchange. Off by default.
* **OpenTelemetry** — `ENABLE_TELEMETRY`, `OTEL_*`, `OTEL_INSECURE_EXPORTER`. Telemetry is OTLP push.
# Reporter events
Source: https://docs.lerian.studio/en/reporter/reporter-events
Subscribe to Reporter's report and deadline events: the streaming manifest operation, the logical topic, the CloudEvents envelope, delivery policies, and what each payload carries.
Reporter publishes a business event whenever a template, a report, or a deadline changes state. Subscribe to those events and you learn that a report finished without polling `GET /v1/reports/{id}` for it.
`GET /v1/streaming/events` describes the contract in machine-readable form. This page covers the consumer side: what the manifest tells you, what arrives on the wire, and what each event carries.
## The manifest operation
***
[Get streaming events](/en/reference/reporter/get-streaming-events) returns the static event catalog. It is authenticated like every other operation, answers `Cache-Control: no-store`, and is served whether or not this deployment publishes events. Read it at startup to check that your consumer and Reporter agree on the contract.
The response has four fields:
| Field | What it carries |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version` | Semantic version of the manifest wire format. |
| `publisher` | Who publishes: `serviceName` is `reporter`, `sourceBase` is `//lerian.studio/reporter`, `routePath` is this operation, `outboxSupported` is `true`, plus the application and library versions. |
| `events` | One entry per event definition, with its key, resource type, event type, schema version, description, and default delivery policy. |
| `routes` | Empty. Reporter publishes no broker topology through the API. |
## The topic is a routing key
***
Each event entry carries a `topic`. It is a **logical routing key**: a stable identifier for one event stream, composed from the publisher source and the event definition. With the source Reporter advertises, `report.requested` composes to:
```
lerian.studio-reporter.report.requested
```
That string is not a broker address. Do not bind a queue to it, and do not treat it as a destination. It exists so a consumer can key on one event stream across transports.
What you bind to lives in deployment configuration instead. Reporter publishes to the exchange that `RABBITMQ_REPORT_EVENTS_EXCHANGE` names, and the routing key of each message is the event definition key verbatim — `report.finished`, `deadline.delivery_reverted`, underscores included.
## The event catalog
***
Twelve event definitions exist across the two run modes.
| Event | Emitted when | Class |
| ---------------------------- | ----------------------------------------------------------------- | --------- |
| `template.created` | A template is uploaded. | Important |
| `template.updated` | A template's file or metadata changes. | Important |
| `template.deleted` | A template is removed, with the count of deadlines that cascaded. | Important |
| `report.requested` | A report request is accepted and queued. | Important |
| `report.finished` | Every data section succeeded and the artifact is stored. | Critical |
| `report.partial` | Some sections failed. An artifact exists. | Critical |
| `report.errored` | The report ended in error. | Critical |
| `deadline.created` | A deadline is created. | Important |
| `deadline.updated` | A deadline changes. | Important |
| `deadline.deleted` | A deadline is removed. | Important |
| `deadline.delivered` | A deadline is marked delivered. | Critical |
| `deadline.delivery_reverted` | A delivery mark is cleared. | Critical |
This channel is publish-only. Reporter emits these events and consumes none of them.
## Delivery
***
The class in the table above selects a delivery policy.
| Class | Direct publish | Outbox | Dead letter |
| --------- | -------------- | ----------------------------------------------- | ------------------- |
| Important | Yes | Falls back to the outbox when the circuit opens | On routable failure |
| Critical | No | Always | On routable failure |
A critical event therefore never publishes straight to the broker. It lands in a durable outbox inside the same transaction, and a dispatcher replays it after a broker outage. Nothing is lost to a broker restart.
Emission happens after the commit and never fails the work. A publication problem does not turn a stored report into an errored one.
Delivery is at-least-once. Deduplicate on `ce-id`. Report events key on the report identifier and its terminal status, in the form `reporter.report..`, so every re-emission of the same fact carries the same identifier. Deadline events key on the deadline identifier, the event type, and the transition timestamp.
## The CloudEvents envelope
***
Messages travel in CloudEvents binary mode, version 1.0. Context attributes ride as message headers.
| Header | Value |
| ------------------ | ------------------------------------------------------------------------------- |
| `ce-specversion` | `1.0` |
| `ce-id` | The deduplication key described above |
| `ce-source` | The value of `STREAMING_CLOUDEVENTS_SOURCE` |
| `ce-type` | `studio.lerian..`, for example `studio.lerian.report.finished` |
| `ce-time` | RFC 3339 emission timestamp |
| `ce-subject` | The identifier of the report, template, or deadline |
| `ce-resourcetype` | `report`, `template`, or `deadline` |
| `ce-eventtype` | `finished`, `created`, `delivery_reverted`, and so on |
| `ce-schemaversion` | `1.0.0` |
| `ce-tenantid` | The tenant that owns the change |
Messages are marked persistent. A single-tenant deployment still stamps a tenant value, so one consumer handles both deployment shapes with the same code.
## What a payload carries
***
Payload keys are `snake_case`, unlike the camelCase REST surface. A `report.finished` body:
```json theme={null}
{
"report_id": "019826f4-6a9c-7b31-9d40-2f1e8c5a4b77",
"template_id": "8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33",
"output_format": "pdf",
"status": "Finished",
"artifact_object_key": "8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33/019826f4-6a9c-7b31-9d40-2f1e8c5a4b77.pdf",
"artifact_content_type": "application/pdf",
"completed_at": "2026-07-29T14:22:08Z",
"duration_ms": 8421,
"section_count": 3
}
```
`artifact_object_key` is the artifact path relative to the report storage prefix, in the form `/.`. [Download a report](/en/reference/reporter/download-report) is the supported way to fetch the file, and it serves a report in the `Finished` state. [Deployment](/en/reporter/reporter-deployment) shows where that prefix sits in the bucket.
`report.partial` adds `section_failures` and `failed_section_count` alongside the same artifact fields, so a consumer can route a usable-but-incomplete report differently from a clean one.
`report.errored` replaces the artifact fields with `error_code` and `error_summary`. Both come from a fixed vocabulary — `report_generation_failed`, `report_generation_timeout`, or `report_generation_canceled` — each paired with a fixed summary. Raw error text never travels on the wire, so a payload cannot leak a query, a connection string, or tenant data. Branch on `error_code`.
## Enabling event publication
***
Event publication is a deployment choice, set with `STREAMING_ENABLED`. Turn it on and Reporter requires three more settings at startup:
| Setting | Value |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `RABBITMQ_REPORT_EVENTS_EXCHANGE` | The exchange that carries the events. Operator-configured; the reference value is `reporter.events`. |
| `STREAMING_BROKERS` | Must be present and non-empty. |
| `STREAMING_CLOUDEVENTS_SOURCE` | Copied verbatim into `ce-source`. Give each deployment its own value when several producers share one broker. |
Reporter refuses to start when publication is on and any of the three is blank, so a misconfigured deployment fails at startup instead of dropping events quietly.
## Next steps
***
The 23 operations, authentication, pagination, and errors.
The streaming manifest operation, with its full response shape.
Every setting behind the streaming, exchange, and run-mode surfaces.
Templates, reports, deadlines, and where they fit.
# HTML template
Source: https://docs.lerian.studio/en/reporter/reporter-html-template
Build an analytical financial report in HTML with Reporter — organization details, per-account breakdowns, and detailed operations lists.
This example shows how to structure an **Analytical Financial Report** focused on account-level transaction analysis, in HTML format. The report includes:
* Organization and Ledger information.
* Per-account breakdown.
* Detailed operation list.
## Template code
***
```html HTML expandable theme={null}
Analytical Financial Report
Analytical Financial Report
Detailed analysis of movements by linked accounts.
`**
This line renders the legal name of the first Organization associated with the onboarding data.
* `midaz_onboarding.organization`: the list of Organizations in the system.
* `0`: fetches the first item in the list.
* `legal_name`: fetches the value of the `legal_name` field.
**`
Ledger: {{ midaz_onboarding.ledger.0.name }}
`**
This line renders the name of the first Ledger associated with the onboarding data.
* `midaz_onboarding.ledger`: the list of Ledgers in the system.
* `0`: fetches the first item in the list.
* `name`: fetches the value of the `name` field.
### Looping through accounts
```html HTML theme={null}
{% for account in midaz_onboarding.account %}
{% with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}
```
**`{% for account in midaz_onboarding.account %}`**
This loop goes through each account in the `midaz_onboardTing.account` list.
* This list represents all accounts linked to a customer or associated with an onboarding flow.
* For each iteration, it creates a new scope where `account` refers to one specific item in that list.
**`{% with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}`**
Inside the loop, this line:
* Uses the filter function to find a balance from the `midaz_transaction.balance` list where the `account_id` matches the current account's ID.
* The `[0]` gets the first matching balance (assuming there's only one per account).
* The result is stored in a temporary variable called `balance`, scoped only within this block.
### Account information
```html HTML theme={null}
Account: {{ account.alias }}
ID: {{ account.id }}
Currency: {{ balance.asset_code }}
Current Balance: {{ balance.available }}
```
**`
Account: {{ account.alias }}
`**
Displays a section title with the account alias (a friendly or readable name for the account).
**`
ID: {{ account.id }}
`**
Shows the unique identifier of the account.
**`
Currency: {{ balance.asset_code }}
`**
Displays the currency code (like "BRL" or "USD") associated with the account’s balance.
**`
Current Balance: {{ balance.available }}`**
Shows the current available balance.
### Operations information
```html HTML theme={null}
{% for operation in midaz_transaction.operation %}
{% if operation.account_id == account.id %}
{% set original_amount = operation.amount %}
{% set discount_amount = original_amount * 0.03 %}
{% set final_amount = original_amount - discount_amount %}
```
**`{% for operation in midaz_transaction.operation %}`**
This loops through all operations from the `midaz_transaction.operation` list.
**`{% if operation.account_id == account.id %}`**
This line checks whether the current operation is related to the current account in the outer loop (from your earlier block) and only proceeds if:
* The operation's `account_id` matches the `account.id` in focus.
**`{% set original_amount = operation.amount %}`**
This line sets a temporary variable named `original_amount` and gives it the value of the `amount` field in the operation object.
**`{% set discount_amount = original_amount * 0.03 %}`**
This line calculates a 3% discount on the original amount.
* The result is saved as `discount_amount`.
**`{% set final_amount = original_amount - discount_amount %}`**
This line subtracts the discount from the original amount.
* The final result is stored in `final_amount`, which can be rendered later.
### Table columns
```html HTML theme={null}
{{ operation.id }}
{{ operation.type }}
{{ original_amount|floatformat:2 }}
{{ discount_amount|floatformat:2 }}
{{ final_amount|floatformat:2 }}
{{ operation.description }}
```
This block outputs a clean, readable table row for each operation, including the following information:
**`
{{ operation.id }}
`**
Unique identifier for the operation.
**`
{{ operation.type }}
`**
Type of operation, such as `credit` or `debit`.
**`
{{ original_amount|floatformat:2 }}
`**
Shows the original (unadjusted) amount, formatted with 2 decimal places.
**`
{{ discount_amount|floatformat:2 }}
`**
Displays the calculated discount, also with 2 decimal places.
**`
{{ final_amount|floatformat:2 }}
`**
Shows the final amount after applying the 3% discount, nicely formatted.
**`
{{ operation.description }}
`**
Renders the description or label of the operation, such as "Transfer to savings".
### Report footer
```html HTML theme={null}
```
This line renders a footer at the bottom of the document, indicating that the content was automatically generated using Reporter by Lerian.
* It also includes the legal document number of the first organization linked to the onboarding data, pulled dynamically using the `{{ midaz_onboarding.organization.0.legal_document }}` placeholder.
## Template output
***
```html HTML expandable theme={null}
Analytical Financial Report
Analytical Financial Report
Detailed analysis of movements by linked accounts.
Generation Date: 23/05/2025 14:08
Organization: Mante - Pfannerstill
Ledger: Rice, Rowe and O'Hara
Account: @external/USD
ID: 0196d97e-a58d-7814-8eb3-e510b818f6a5
Currency: USD
Current Balance: 499950.00
Operation ID
Type
Original Amount
Discount (3%)
Final Amount
Description
0196d982-4085-7e1d-b698-3aad306df721
CREDIT
500000.00
15000.00
485000.00
Duplicated account test
0196d983-a2c2-7dce-ad14-78eb36d9f2d1
DEBIT
20.00
0.60
19.40
Loan payment person1
0196d983-a2c2-7e0d-a246-45d5234138e3
DEBIT
30.00
0.90
29.10
Loan payment person1
```
## Rendered output
***
# Reporter quick start
Source: https://docs.lerian.studio/en/reporter/reporter-quick-start
Upload a minimal Reporter template, generate your first report from live data, and verify the downloaded output — a hands-on setup in under ten minutes.
Use this quick start to generate one report and confirm that your Reporter installation works end to end. For a recurring production workflow, see [Using Reporter](/en/reporter/using-reporter).
For complete request and response schemas, use the [Reporter API quick start](/en/reference/reporter/reporter-developer-quick-start) and the linked API reference pages.
## Goal
***
You will:
1. Create a minimal `.tpl` template.
2. Upload it as an HTML template.
3. Generate a report without data filters.
4. Track the report until processing ends.
5. Download and verify the finished file.
## Prerequisites
***
You need:
* A running Reporter instance
* An authentication token, if Access Manager is enabled
* `curl`
Set the values used in the examples:
```bash theme={null}
export REPORTER_URL="https://reporter.example.com"
export TOKEN="your-access-token"
```
## Create a minimal template
***
Create `first-report.tpl` with static HTML content:
```html theme={null}
My first Reporter output
```
Reporter templates always use the `.tpl` extension. The template content must match the selected output format. Reporter supports HTML, CSV, XML, PDF, and TXT outputs. For PDF output, write the template in HTML and set `outputFormat=PDF`.
For templates that read data, see [Template formats](/en/reporter/template-examples).
## Upload the template
***
Upload the file with the three required multipart fields: `template`, `outputFormat`, and `description`.
```bash theme={null}
curl --fail-with-body -X POST "$REPORTER_URL/v1/templates" \
-H "Authorization: Bearer $TOKEN" \
-F "template=@first-report.tpl" \
-F "outputFormat=HTML" \
-F "description=First Reporter output"
```
Save the returned template `id` as `TEMPLATE_ID`:
```bash theme={null}
export TEMPLATE_ID="returned-template-id"
```
See [Upload template](/en/reference/reporter/upload-template) for the complete operation contract.
## Generate the report
***
Create a report with the required `templateId` and `filters` fields. Use an empty object when you do not need filters.
```bash theme={null}
curl --fail-with-body -X POST "$REPORTER_URL/v1/reports" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"templateId\":\"$TEMPLATE_ID\",\"filters\":{}}"
```
Save the returned report `id` as `REPORT_ID`:
```bash theme={null}
export REPORT_ID="returned-report-id"
```
See [Create report](/en/reference/reporter/create-report) for filter syntax and the complete operation contract.
## Track processing
***
Check the report until it leaves `Processing`:
```bash theme={null}
curl --fail-with-body \
-H "Authorization: Bearer $TOKEN" \
"$REPORTER_URL/v1/reports/$REPORT_ID"
```
The active lifecycle is:
```text theme={null}
Processing -> Finished | Partial | Error
```
| Status | Meaning | What to do |
| ------------ | --------------------------------------------- | ------------------------------------------------------- |
| `Processing` | Reporter is generating the output. | Check again later. |
| `Finished` | Generation completed successfully. | Download the report. |
| `Partial` | Generation completed with incomplete results. | Inspect the report details and application logs. |
| `Error` | Generation failed. | Inspect the error and application logs before retrying. |
See [Check report status](/en/reference/reporter/check-report-status) for the response schema.
## Download and verify the output
***
Download only when the report status is `Finished`:
```bash theme={null}
curl --fail-with-body \
-H "Authorization: Bearer $TOKEN" \
"$REPORTER_URL/v1/reports/$REPORT_ID/download" \
-o first-report.html
```
Confirm that the file is not empty and contains the expected heading:
```bash theme={null}
test -s first-report.html
grep -F "My first Reporter output" first-report.html
```
See [Download report](/en/reference/reporter/download-report) for response headers and error details.
## Troubleshooting
***
| Problem | Check |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Template upload is rejected | Include the `.tpl` file, `outputFormat`, and `description`; confirm that the file content matches the format. |
| Report creation is rejected | Send both `templateId` and `filters`; use `{}` when no filter applies. |
| Report remains in `Processing` | Check Reporter worker and queue health, then inspect application logs. |
| Report ends in `Partial` or `Error` | Inspect the report details, referenced data sources, and application logs before retrying. |
| Download is rejected | Confirm that the report exists and has status `Finished`. |
## Next steps
***
* Follow [Using Reporter](/en/reporter/using-reporter) to design a reusable, recurring workflow.
* Learn how to reference configured data in [Template formats](/en/reporter/template-examples).
* Review the specialized guidance in [BACEN templates](/en/reporter/reporter-bacen-templates) before working with regulatory report structures.
* Use the [Reporter API quick start](/en/reference/reporter/reporter-developer-quick-start) for complete API examples.
# Reporter REST API
Source: https://docs.lerian.studio/en/reporter/reporter-rest-api
Orient yourself in the Reporter API: the /v1 base path, bearer authentication, the 23 operations grouped by job, idempotency, pagination, and the RFC 9457 error shape.
Reporter serves one HTTP API. Every operation sits under the base path `/v1`, with no product segment ahead of it. A template list is `GET /v1/templates`.
The API carries **23 operations** across seven areas: templates, the template builder, reports, data sources, deadlines, metrics, and the streaming manifest. All 23 render under the **Reporter** anchor in the [API Reference](/en/reference/introduction). This page is the map, not the territory. It covers what the operations share and points you at the reference page for each one.
The OpenAPI documents in this portal are render sources for the reference pages. They are not client contracts, and they are not a basis for SDK generation.
## Authentication
***
Reporter accepts a JWT bearer token. One security scheme applies to every operation:
```http theme={null}
Authorization: Bearer
```
Reporter authorizes each request against the `reporter` application, a resource, and an action. The resource follows the area — `templates`, `reports`, `deadlines`, `data-source`, `metrics`, or `streaming`. The action is the lower-cased HTTP method, so `POST /v1/reports` authorizes as the `post` action on `reports`. `PLUGIN_AUTH_ENABLED` turns the middleware on, and `PLUGIN_AUTH_ADDRESS` points it at the Access Manager.
Probe routes stay outside authentication so an orchestrator reaches them without a token: `/health`, `/readyz`, and `/version`.
Tenant identity travels inside the token. Reporter resolves the tenant from the JWT `tenantId` claim. None of the operations above take a tenant from a request header, and no client-supplied value overrides it. See [Multi-tenancy](/en/multi-tenancy).
## The operations by job
***
### Templates
Five operations own the template lifecycle: [list](/en/reference/reporter/list-templates), [upload](/en/reference/reporter/upload-template), [get](/en/reference/reporter/retrieve-template-details), [update](/en/reference/reporter/update-templates), and [delete](/en/reference/reporter/delete-template). A template is a plain-text `.tpl` file plus its output format and description. Deleting one also removes the deadlines that point at it.
### Template builder
Four operations back a visual editor without persisting anything. [List block definitions](/en/reference/reporter/list-block-definitions) and [list filter definitions](/en/reference/reporter/list-filter-definitions) return the catalog an editor draws from. [Validate blocks](/en/reference/reporter/validate-template-blocks) checks a block tree and reports per-block errors. [Generate code](/en/reference/reporter/generate-template-code) turns that tree into template source you can then upload.
### Reports
Four operations, and only four: [create](/en/reference/reporter/create-report), [get](/en/reference/reporter/check-report-status), [list](/en/reference/reporter/retrieve-reports), and [download](/en/reference/reporter/download-report).
The lifecycle is **create-and-retain**. `POST /v1/reports` answers `201` with a report in `Processing` and hands the work to a background worker. The report then reaches `Finished`, `Partial`, or `Error`. Poll the get operation, or subscribe to the events described in [Events](/en/reporter/reporter-events). Download serves a report in `Finished`. Every report you create stays addressable by its identifier for as long as your object-storage retention policy keeps the artifact.
### Data sources
Two read operations: [list](/en/reference/reporter/list-data-sources) and [get one](/en/reference/reporter/retrieve-data-source), both under `/v1/data-sources` — note the hyphen. An operator registers data sources through environment variables, so these operations report what the deployment already has and expose no write path.
### Deadlines
Six operations model a filing obligation with a due date and a recurrence rule: [list](/en/reference/reporter/retrieve-deadlines), [create](/en/reference/reporter/create-deadline), [update](/en/reference/reporter/update-deadline), [delete](/en/reference/reporter/delete-deadline), [deliver](/en/reference/reporter/deliver-deadline), and [notifications](/en/reference/reporter/retrieve-deadline-notifications). Status is derived from the due date and the delivery mark, never sent by a client. Notifications are pull-only: the operation returns the deadlines inside their alert window, sorted overdue first.
### Metrics and streaming
[Metrics](/en/reference/reporter/get-metrics) returns deployment counters — templates, reports, data sources, and report errors for the current window against the one before it. `errorPeriodDays` sets that window and defaults to 7. [Streaming events](/en/reference/reporter/get-streaming-events) returns the event manifest covered in [Events](/en/reporter/reporter-events).
## Content types
***
Template create and update are `multipart/form-data`: a `template` file part, plus `outputFormat` and `description`. Everything else that carries a body is `application/json`.
Download answers with the bytes and a `Content-Disposition` filename. The media type follows the report's output format — `application/pdf`, `application/xml`, `text/csv`, `text/html`, or `text/plain`.
## Idempotency
***
Template create and report create accept an `X-Idempotency` request header. Send your own key, or omit the header and Reporter derives one from a hash of the request body.
* A request that repeats one still in flight is rejected rather than duplicated.
* A request that repeats one already completed replays the original response and stamps `X-Idempotency-Replayed: true` on it.
## Pagination
***
List operations use offset pagination with the same two parameters.
| Parameter | Default | Rules |
| --------- | ------- | ----------------------------------------------------------------------------- |
| `page` | `1` | Page number. |
| `limit` | `10` | Items per page. The ceiling is `MAX_PAGINATION_LIMIT`, which defaults to 100. |
A `limit` above the ceiling is rejected with a pagination error rather than clamped. The report list also accepts a `cursor` parameter, which carries the position of the previous page in place of a page number.
The report list adds filters on top: `status`, `template_id`, `output_format`, `description`, `type`, `active`, a `start_date`/`end_date` window, and `sort_order`, which defaults to `desc`. The template list filters on `outputFormat`, and the deadline list on `status`.
## Errors
***
Every error answers `application/problem+json` and follows [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457), carrying a `title`, a `status`, a `detail`, and a stable domain `code`. Match on the code, never on the prose. The [Reporter error list](/en/reference/reporter/reporter-error-list) maps each code to its HTTP status and its fix.
## Reading the spec from a running service
***
`SWAGGER_ENABLED=true` mounts a browsable reference at `/swagger/docs`, with the OpenAPI 3.1 document at `/swagger/openapi.json` and `/swagger/openapi.yaml`. It is off unless you turn it on. Keep it off in production.
## Next steps
***
The 23 operations, with full request and response shapes.
Upload a template, generate a report, and download it with cURL.
Subscribe to report and deadline events instead of polling.
Templates, reports, data sources, and where they fit.
# Reporter SDK and embedding
Source: https://docs.lerian.studio/en/reporter/reporter-sdk-and-embedding
Integrate Reporter from your own software: the REST surface that carries every operation, the base URL and auth model, a first call, the Go SDK, and the pattern for running Reporter behind a service you already operate.
Reporter's **REST API is the complete surface**. Templates, reports, data sources, deadlines, the template builder, and metrics are all HTTP calls. Nothing exists only inside a client library, and nothing exists only inside the Console.
Start there. This page covers what you authenticate with, what your base URL looks like, what a first call returns, where the Go SDK saves you work, and how to run Reporter behind a service your own users already talk to.
## Authenticating
***
One scheme protects every operation: a bearer token on the `Authorization` header.
```
Authorization: Bearer
```
Access Manager authorises each call against the resource behind the path — templates, reports, data sources, deadlines, metrics, or streaming — and the action, which is the HTTP verb. A token granted read access to reports can list and download them, and gets a `403` on create. Failures answer `application/problem+json`, so parse the problem document rather than the status line alone.
The token also carries the isolation scope of the call. Reporter resolves it from the credential itself, so no operation takes a scoping header from your client and no header widens what a token already permits.
## Base URL
***
The base path is `/v1`, with no product segment in front of it. Every path on this page appends to that:
```
https://reporter.example.com/v1
```
Three content-type rules follow from the surface itself. Template upload and update are `multipart/form-data`, because a template is a `.tpl` file. Report creation is `application/json`. A download streams the rendered bytes with the `Content-Type` of the output format and a `Content-Disposition` filename.
List operations take `limit` and `page`, 10 and 1 by default. The largest page is `MAX_PAGINATION_LIMIT`, which each deployment sets and which defaults to 100. A `limit` above that ceiling is rejected, not reduced.
## Your first call
***
```bash theme={null}
curl -s "https://reporter.example.com/v1/templates" \
-H "Authorization: Bearer $TOKEN"
```
A `200` with a template list proves both. A `401` means the token; a `404` means the base URL.
```bash theme={null}
curl -s -X POST "https://reporter.example.com/v1/reports" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "X-Idempotency: daily-balance-2026-07-28" \
-d '{
"templateId": "0196b270-a315-7137-9408-3f16af2685e1",
"filters": {
"midaz_onboarding": {
"account": { "status": { "eq": ["ACTIVE"] } }
}
}
}'
```
The response is `201` with the report in `Processing`. Generation runs asynchronously.
```bash theme={null}
curl -s "https://reporter.example.com/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $TOKEN"
curl -s "https://reporter.example.com/v1/reports/$REPORT_ID/download" \
-H "Authorization: Bearer $TOKEN" -o report.pdf
```
Download requires `Finished`.
Send `X-Idempotency` on every report request, and on template upload for the same reason. Repeat a request that is still running and you get an error instead of a second report. Repeat one that already finished and Reporter replays the original report, marking the response `X-Idempotency-Replayed: true`. Derive the key from your own request identifier and a retry costs nothing.
A report is created once and kept: create, get, list, download are its four operations. How long a rendered file lives is a lifecycle policy on the storage bucket, not an API call.
## Reading data sources
***
An operator configures data sources through environment variables, so the API over them is read-only. Use it to discover what your templates can reference:
```bash theme={null}
curl -s "https://reporter.example.com/v1/data-sources" \
-H "Authorization: Bearer $TOKEN"
```
Each entry carries the config name that templates address, plus its tables and fields. Fetch one by identifier with `/v1/data-sources/{dataSourceId}`.
## The Go SDK
***
[`lerian-sdk-golang`](https://github.com/LerianStudio/lerian-sdk-golang) ships a `reporter` package alongside the other Lerian products. It is a convenience over the calls above for template and report work: it acquires an OAuth2 client-credentials token, refreshes it, and hands back typed results and paginated iterators.
Configure it with the same `https:///v1` base URL you use with cURL, plus the client ID, the client secret, and the token URL of your authorization server, and a request timeout.
```go theme={null}
report, err := client.Reporter.Reports.Get(ctx, reportID)
if err != nil {
return err
}
if report.Status == "Finished" {
data, err := client.Reporter.Reports.Download(ctx, reportID)
if err != nil {
return err
}
if err := os.WriteFile(reportID+"."+report.Format, data, 0o600); err != nil {
return err
}
}
```
`Download` returns the rendered bytes in the report's own format. Write them to disk, as above, or stream them to your caller.
The SDK covers a slice of the product, not all of it. It carries template create, get, list, and delete, and report create, get, list, and download. Data sources, template update, deadlines, the template builder, metrics, and the streaming manifest are REST calls. Mixing both in one integration is normal and expected: the package where it fits, plain HTTP everywhere else.
## Running Reporter behind your own service
***
Reporter is a service you deploy, not a library you link. To put it behind an application your customers already use, keep the credentials on your side and call Reporter server to server. Four rules keep that boundary clean.
**Never hand a Reporter token to a browser.** Your service authenticates your user, decides whether that user may run this report, and then makes the call with its own token.
**Answer immediately with an identifier.** Report creation returns in `Processing`. Return that identifier to your caller and let your own status endpoint expose progress.
**Learn about completion once.** Poll `GET /v1/reports/{id}` on a modest interval, or subscribe to Reporter's report events and stop polling. See [Reporter events](/en/reporter/reporter-events).
**Proxy the download.** The download endpoint streams bytes to an authenticated caller, so your service fetches them and re-serves them under its own auth.
A `Partial` report means some data sections failed while others succeeded, and its metadata names the failing sections. Treat it as a signal about a data source or a filter, and decide in your own service what your users should see.
## Next steps
***
Every operation, grouped by the job it does.
The event contract, and how to subscribe instead of polling.
Upload a template and generate a report with cURL.
Reporter's error codes and what resolves them.
# Reporter template engine
Source: https://docs.lerian.studio/en/reporter/reporter-template-engine
How Reporter evaluates a template: the field map it derives, the data context it builds, blocks, template filters, row filters, and the multi-schema data source model.
A Reporter template is a plain-text `.tpl` file. Reporter renders it with a Pongo2 engine, which reads Django-style tags and pipe filters.
[Template reference](/en/reporter/template-reference) lists every tag and filter. [Template formats](/en/reporter/template-examples) shows a worked file per output format. This page covers what the engine does around that syntax: what it reads out of your file, what data it puts in front of it, and which limits it applies.
## The field map
***
When you upload a template, Reporter analyses the text and derives a **field map**: every data source, every table, and every field the template names.
```json theme={null}
{
"external_db": {
"sales__orders": ["id", "total", "created_at"]
}
}
```
The map is stored with the template metadata and travels with each report request. Extraction reads only what the map lists. A field your template never names is never queried, so a template stays cheap as the underlying tables grow.
Two consequences follow. First, Reporter derives the map when you upload or replace the file, never at render time. A new field reference reaches a report only after you upload the changed template.
Second, Reporter checks the map against the live schema of each data source at upload time. A wrong table name or a wrong field name surfaces there, before any report runs. When a data source cannot answer, Reporter returns warnings and still accepts the template. One unreachable database does not block your work.
## The data context
***
Extraction builds one context for the render. The first level is the data source config name. The second level is the table. Each value is a list of rows.
```json theme={null}
{
"external_db": {
"orders": [
{ "id": "018f...", "total": "1200.00", "created_at": "2026-07-01" }
]
}
}
```
Your template addresses that context by the same names it used to declare them:
```django theme={null}
{% for order in external_db.orders %}
{{ order.id }} — {{ order.total|floatformat:2 }}
{% endfor %}
```
Rows come back under the key your template wrote. A bare table name stays bare. A schema-qualified reference becomes `schema__table`, with a double underscore, in both the field map and the render context. Row filters are looser and accept either `schema.table` or `schema__table`.
### Variables
A template reads the context through variables. A loop alias binds one row at a time, and `{% with %}` names part of the context for the block below it. Both aliases are local to the block that declares them.
The analyser follows an alias back to the table behind it. A field you read as `order.total` inside a loop over `external_db.orders` lands in the field map as `total` under that table, so extraction returns it.
## Multi-schema data sources
***
An operator declares each data source through environment variables. `CONFIG_NAME` sets the name your templates use, and `SCHEMAS` lists the schemas Reporter discovers on it. Keep the environment-name segment and the `CONFIG_NAME` value identical, as in the block below:
```bash theme={null}
DATASOURCE_EXTERNAL_DB_CONFIG_NAME=external_db
DATASOURCE_EXTERNAL_DB_HOST=external-postgres
DATASOURCE_EXTERNAL_DB_PORT=5432
DATASOURCE_EXTERNAL_DB_USER=db_user
DATASOURCE_EXTERNAL_DB_DATABASE=external_database
DATASOURCE_EXTERNAL_DB_TYPE=postgresql
DATASOURCE_EXTERNAL_DB_SCHEMAS=sales,inventory,reporting
```
Without `SCHEMAS`, Reporter discovers the `public` schema alone. See [Environment variables](/en/reporter/reporter-environment-variables) for the full block.
The engine resolves a bare table name against every discovered schema:
| Owners of the table name | Result |
| -------------------------------------- | ------------------------------------------------------------------- |
| Exactly one schema | The engine reads that table. |
| Several schemas, one of them `public` | The engine reads the `public` table. |
| Several schemas, none of them `public` | The engine reports the table as ambiguous and names the candidates. |
| No schema | The engine reports the table as not found. |
Qualify the reference to remove the ambiguity. The qualified form names the source, the schema, and the table:
```django theme={null}
{% for order in external_db:sales.orders %}
{{ order.id }}
{% endfor %}
```
A qualified reference must match the discovered schema exactly. The engine does not search other schemas for it.
## Blocks
***
A block is the unit the visual template builder works in. Thirteen types exist, in six categories:
| Category | Block types |
| --------------------- | ------------------------------------------------------- |
| Basic (`basic`) | `text`, `variable`, `comment` |
| Control (`control`) | `loop`, `conditional`, `with` |
| Data (`data`) | `aggregation`, `calculation`, `date_time`, `expression` |
| Layout (`layout`) | `section` |
| Numbering (`dimp`) | `counter` |
| Advanced (`advanced`) | `custom_tag` |
The value in brackets is the category string the block catalogue returns, so a client matching on the API response keys on it rather than on the label.
Four of them hold children: `loop`, `conditional`, `section`, and `with`. A conditional also carries alternative branches. Blocks nest up to fifty levels deep.
Blocks are never stored. The builder sends them to Reporter, and Reporter returns Pongo2 source plus the field map that source implies. The template you upload is always the text.
Three operations support that flow:
* [Validate template blocks](/en/reference/reporter/validate-template-blocks) checks the structure, parses the generated source, and reports each problem against the block that caused it.
* [Generate template code](/en/reference/reporter/generate-template-code) returns the finished source for an output format.
* [List block definitions](/en/reference/reporter/list-block-definitions) returns the catalogue, so a client stays in step with the engine.
## Two kinds of filter
***
Reporter uses the word filter for two different things, and they run at different times.
**Row filters** run during extraction. They live in the report request, not in the template, and they narrow the rows the database returns. The payload nests them three levels deep: data source, then table, then field.
```json theme={null}
{
"templateId": "018f2a6c-1c2f-7a10-9f1e-2b8c4d5e6f70",
"filters": {
"external_db": {
"orders": {
"status": { "nin": ["cancelled", "draft"] },
"created_at": { "gte": ["2026-07-01"], "lte": ["2026-07-31"] }
}
}
}
}
```
Eight operators exist:
| Operator | Meaning | Values |
| --------- | --------------------------- | ----------- |
| `eq` | Matches any listed value | One or more |
| `gt` | Above the value | One |
| `gte` | At or above the value | One |
| `lt` | Below the value | One |
| `lte` | At or below the value | One |
| `between` | Inside an inclusive range | Exactly two |
| `in` | Matches any listed value | One or more |
| `nin` | Excludes every listed value | One or more |
Every operator takes an array. Several operators on one field combine, as the date range above shows.
**Template filters** run during the render, after the rows arrive. They shape a value inside the document, and they use pipe syntax: `{{ value|percent_of:total }}`. [List filter definitions](/en/reference/reporter/list-filter-definitions) returns the catalogue with an example per filter.
## The render step
***
The engine parses the prepared template once per report and executes it against the context. Two behaviours of that step change how you design a document.
Numeric output loses its trailing zeros, so `1200.00` renders as `1200`. Use `floatformat` when a column needs fixed decimals. Values that carry several dots stay intact, which keeps an accounting code such as `1.1.2.00.000` unchanged.
An XML template can declare the encoding of the stored file. Put the marker on the same line as the XML declaration:
```django theme={null}
{# reporter:output-encoding=utf-16be #}
```
Reporter then writes the file in UTF-16BE with no byte order mark. PDF output ignores the marker.
## Limits the engine applies
***
Reporter blocks the Pongo2 tags that load or extend another file: `include`, `extends`, `import`, `block`, and `ssi`. A template is a self-contained document.
Block nesting stops at fifty levels, on validation and on code generation alike. Free-form block fields reject template delimiters, so builder input cannot inject a tag. Uploaded templates reject script tags.
## Next steps
***
The path from a report request to a file you can download.
Every tag, filter, and operator the syntax accepts.
A worked template per output format.
Templates, data sources, reports, and deadlines in one place.
# TXT template
Source: https://docs.lerian.studio/en/reporter/reporter-txt-template
Build a transaction receipt template in plain text with Reporter — a simple .tpl file that renders into a printable TXT proof of payment.
This page guides you through the creation of a transaction receipt template using **Reporter** in `.txt` format.
## Template code
***
```text Text expandable theme={null}
##########################################
# PROOF OF PAYMENT #
##########################################
Generation Date: {% date_time "dd/MM/YYYY HH:mm" %}
Ledger Name: {{ midaz_onboarding.ledger.0.name }}
{%- for transaction in midaz_transaction.transaction %}
-----------------------------------------
Transaction ID: {{ transaction.id }}
Transaction Date: {{ transaction.created_at }}
Total Amount: {{ transaction.amount }}
Transaction Status: {{transaction.status}}
------------------------------------------
Source Accounts:
{%- for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %}
{%- if operation.type == "DEBIT" %}
- Alias: {{operation.account_alias }}
- Debit Amount: {{ operation.amount }}
{%- endif %}
{%- endfor %}
Target Accounts:
{%- for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %}
{%- if operation.type == "CREDIT" %}
- Alias: {{operation.account_alias }}
- Credit Value: {{operation.amount }}
{%- endif %}
{%- endfor %}
{%- endfor %}
------------------------------------------
Automatically generated document.
```
## Code breakdown
***
\*\* `Generation Date: {% date_time "dd/MM/YYYY HH:mm" %}`\*\*
Renders the **current date and time** when the template is processed.
* Uses the format `dd/MM/YYYY HH:mm` (e.g., `26/05/2025 11:45`).
\*\* `Ledger Name: {{ midaz_onboarding.ledger.0.name }}`\*\*
Displays the **name of the ledger** associated with this report.
* `midaz_onboarding.ledger` is a list, and `[0]` selects the **first ledger**.
* `.name` fetches the name property, like `"Corporate Ledger"`.
\*\* `{% for transaction in midaz_transaction.transaction %}`\*\*
This loop iterates over **each transaction** in the `midaz_transaction.transaction` list. For every transaction, the code below will run once to display its information.
### Transaction header block
Each transaction prints the following details:
```txt Text theme={null}
-----------------------------------------
Transaction ID: {{ transaction.id }}
Transaction Date: {{ transaction.created_at }}
Total Amount: {{ transaction.amount }}
Transaction Status: {{ transaction.status }}
------------------------------------------
```
* `transaction.id`: Unique ID of the transaction.
* `transaction.created_at`: Timestamp when the transaction was created.
* `transaction.amount`: The raw amount of the transaction.
* `transaction.status`: Current status such as `COMPLETED`, `PENDING`, or `FAILED`.
### Source accounts section
```txt Text theme={null}
Source Accounts:
{% for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %}
{% if operation.type == "DEBIT" %}
- Alias: {{ operation.account_alias }}
- Debit Amount: {{ operation.amount }}
{% endif %}
{% endfor %}
```
* Filters the operations list to include only operations linked to the **current transaction**.
* Further filters it to only show operations with `type == "DEBIT"` (funds leaving an account).
* For each matching debit:
* Displays the **account alias**.
* Displays the **debit amount**.
### Target accounts section
```txt Text theme={null}
Target Accounts:
{% for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %}
{% if operation.type == "CREDIT" %}
- Alias: {{ operation.account_alias }}
- Credit Value: {{ operation.amount }}
{% endif %}
{% endfor %}
```
* Same logic as the debit block, but filters for `type == "CREDIT"` (funds received).
* For each matching credit:
* Displays the **account alias**.
* Displays the **credit amount**.
### Footer text
```text Text theme={null}
------------------------------------------
Automatically generated document.
```
* Simple text footer that marks the end of the report.
* Indicates that the file was generated automatically, typically for traceability.
## Template output
***
```text Text expandable theme={null}
##########################################
# PROOF OF PAYMENT #
##########################################
Generation Date: 23/05/2025 14:08
Ledger Name: Rice, Rowe and O'Hara
-----------------------------------------
Transaction ID: 75906707-8c31-479c-b354-aa805c4cefbc
Transaction Date: 2025-05-22T14:15:22.123Z
Total Amount: 5000.00
Transaction Status: ACTIVE
------------------------------------------
Source Accounts:
- Alias: @external|USD
- Debit Amount: 1500.00
Target Accounts:
- Alias: @wallet_12345
- Credit Value: 1500.00
------------------------------------------
Automatically generated document.
```
# XML template
Source: https://docs.lerian.studio/en/reporter/reporter-xml-template
Build an analytical financial report in XML with Reporter — account-level activity, custom logic like discounts, and per-account summaries.
This page guides you through a full example of an **Analytical Financial** report in **XML**. The template breaks down account-level activity, applies custom logic (like discounts), and includes clear summaries to support analysis and auditing.
**What this report shows:**
* **Organization and Ledger details**.
* **Account-level overview**: balance, currency, and alias.
* **Operation breakdowns**: original amount, discount, final amount, status, description, and type.
* **Summaries per account**: total operations, total value, and average per operation.
## Template code
***
```xml XML expandable theme={null}
{{ midaz_onboarding.legal_name }} - Tax ID: {{ midaz_onboarding.legal_document }}{% date_time "dd/MM/YYYY HH:mm" %}
{%- with ledger = midaz_onboarding.ledger[0] %}
{{ ledger.name }}
{%- for account in midaz_onboarding.account %}
{{ account.id }}{{ account.alias }}
{%- with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}
{{ balance.available }}
{%- endwith %}
{%- for balance in midaz_transaction.balance %}
{%- if balance.account_id == account.id %}
{{ balance.available }}
{%- endif %}
{%- endfor %}
{{ account.asset_code }}
{%- for operation in midaz_transaction.operation %}
{%- if operation.account_id == account.id %}
{%- set original_amount = operation.amount %}
{%- set discount_amount = original_amount * 0.03 %}
{%- set final_amount = original_amount - discount_amount %}
{{ operation.id }}{{ operation.description }}{{ operation.type }}{{ operation.route }}{{ original_amount }}{{ discount_amount }}{{ final_amount }}{{ operation.asset_code }}{{ operation.status }}
{%- endif %}
{%- endfor %}
{% count_by midaz_transaction.operation if account_id == account.id %}{% sum_by midaz_transaction.operation by "amount" if account_id == account.id %}{% avg_by midaz_transaction.operation by "amount" if account_id == account.id %}
{%- endfor %}
```
## Code breakdown
***
### Organization and Ledger information
```xml XML theme={null}
{{ midaz_onboarding.legal_name }} - Tax ID: {{ midaz_onboarding.legal_document }}28.04.2025
{%- with ledger = midaz_onboarding.ledger[0] %}
{{ ledger.name }}
```
**`{{ midaz_onboarding.organization.legal_name }} - CNPJ: {{ midaz_onboarding.organization.legal_document }}`**
Displays the organization’s legal name and corresponding *CNPJ* (Brazilian Tax ID).
* `{{ midaz_onboarding.organization.legal_name }}`: retrieves the organization's name.
* `{{ midaz_onboarding.legal_document }}`: retrieves the *CNPJ*.
**`{% date_time "dd/MM/YYYY HH:mm" %}`**
This function applies the date and time when the template is rendered, using the `dd/MM/YYYY HH:mm` format.
**`{%- with ledger = midaz_onboarding.ledger[0] %}`**
Creates a temporary variable pointing to the first available ledger. This keeps the code cleaner and avoids repetitive references.
* `-`: removes extra spaces in the rendered file.
* `ledger`: the name of the temporary variable.
* `midaz_onboarding.ledger[0].name`: points to the first available ledger.
### Looping through accounts
```xml XML theme={null}
{%- for account in midaz_onboarding.account %}
{{ account.id }}{{ account.alias }}
{%- with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}
{{ balance.available }}
{%- endwith %}
```
**`{%- for account in midaz_onboarding.account %}` ... `{% endfor %}`**
Loops through all accounts linked to the user. Since most users have more than one account, this allows you to display individual data for each one.
* `midaz_onboarding.account`: the full list of user accounts.
* `{%- for account in midaz_onboarding.account %}`: the loop runs once per account.
**`{%- with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}`**
Defines a variable named `balance` that holds the current account’s balance.
* `filter()`: scans through `midaz_transaction.balance` to find the one matching the `account.id`.
* `[0]`: grabs the first match.
* The result is stored in `balance`.
**` {{ balance.available }}`**
Shows the account’s available balance (`balance.available`).
### Looping without `{%with...%}`
```xml XML theme={null}
{%- for balance in midaz_transaction.balance %}
{%- if balance.account_id == account.id %}
{{ balance.available }}
{%- endif %}
{%- endfor %}
```
**`{%- for balance in midaz_transaction.balance %}`**
Loops through all balances to find the one that matches the current account. This is a more explicit alternative to `filter()`.
* `midaz_transaction.balance`: The list of all balances.
* `{%- balance in midaz_transaction.balance %}`: the loop runs once per balance.
**`{% if balance.account_id == account.id %}`**
Filters balances to show only those that match the current account being processed.
* If they match, the balance is displayed.
### Currency information
```xml XML theme={null}
{{ account.asset_code }}
```
Shows the account’s currency (e.g., BRL, USD) to help you understand the value context.
* `account.asset_code`: dynamically injects the currency code.
### Grouping operations
```xml XML theme={null}
{%- for operation in midaz_transaction.operation %}
{%- if operation.account_id == account.id %}
{%- set original_amount = operation.amount %}
{%- set discount_amount = original_amount * 0.03 %}
{%- set final_amount = original_amount - discount_amount %}
```
### `...`
Defines the section where all operations for an account are listed.
* Keeps the report structured and easy to scan.
**`{%- for operation in midaz_transaction.operation %}`**
Loops through all operations in the transaction, and displays operations that match the current account.
* `operation`: represents the current operation that is part of the current transaction.
* `midaz_transaction.operation`: the list of all operations in the transaction.
**`{%- if operation.account_id == account.id %}`**
Ensures that only the relevant operations for the current account are included.
* If the `account_id` of the operation is equal to the current `account.id`, the rest of the block will be executed.
**`{%- set original_amount = operation.amount %}`**
Defines the original amount and sets it as a variable.
* `original_amount`: the name of the variable.
**`{%- set discount_amount = original_amount * 0.03 %}`**
Defines a variable and calculates a 3% discount on the original amount.
* `discount_amount`: the name of the variable.
* `original_amount * 0.03`: applies a 3% discount on the original amount.
**`{%- set final_amount = original_amount - discount_amount %}`**
Subtracts the discount from the original amount to get the final value.
* `final_amount`: variable that represents the final value.
* `original_amount - discount_amount`: the subtraction operation.
### Operation block
```xml XML theme={null}
{{ operation.id }}{{ operation.description }}{{ operation.type }}{{ operation.route }}{{ original_amount }}{{ discount_amount }}{{ final_amount }}{{ operation.asset_code }}{{ operation.status }}
```
Each `` contains detailed information about the transaction. These fields help users audit and understand what happened.
**`{{ operation.id }}`**
Unique ID of the operation.
**`{{ operation.description }}`**
Short description of the operation.
**`{{ operation.type }}`**
Operation type (e.g., credit, debit, adjustment).
**`{{ operation.route }}`**
Associated operation route entry.
**`{{ original_amount }}`**
Original amount before any adjustments.
**`{{ discount_amount }}`**
Value of the discount applied.
**`{{ final_amount }}`**
Final amount after discount.
**`{{ operation.asset_code }}`**
The currency used for the operation.
**`{{ operation.status }}`**
Operation status (e.g., Approved, pending).
### Account summary block
```xml XML theme={null}
{% count_by midaz_transaction.operation if account_id == account.id %}{% sum_by midaz_transaction.operation by "amount" if account_id == account.id %}{% avg_by midaz_transaction.operation by "amount" if account_id == account.id %}
```
**`{% count_by midaz_transaction.operation if account_id == account.id %}`**
Counts the number of operations associated with your account and assists in understanding the transaction volume for the reporting period.
* The `count_by` function goes through `midaz_transaction.operation` and adds up how many operations have the same `account_id` as the current account.
**`{% sum_by midaz_transaction.operation by "amount" if account_id == account.id %}`**
Sums up the total value of all operations for this account, and outputs the result using two decimal places.
* `sum_by` loops through transactions filtered by `account_id`.
* Sums the `amount` fields.
**`{% avg_by midaz_transaction.operation by "amount" if account_id == account.id %}`**
Calculates the average operation value for this account. Useful for spotting spending patterns or outliers.
* `avg_by` applies the average calculation to the amount values.
* Only considers operations for the current account.
## Template output
***
```xml XML expandable theme={null}
Ferry, Stiedemann and Jast - CNPJ: 7842523000019028.04.2025O'Connell, Dietrich and Bernhard01965efe-5afd-733d-9a64-3d53f73b13c8@external/BRL1000.001000.00BRL01965f04-7087-73f1-a802-f8fceedb34ecInitial transferDEBIT1000.0030.00970.00BRL11000.001000.0001965eff-0b2d-73cf-bbfe-0dc1ac9032d3@wallet_46040127 0 0BRL00.000.0001965f01-aa72-7254-bb4a-fa9c8c9d4009@account1300.00300.00BRL01965f04-7087-73d2-a167-cb5146bee801External to accounts 1 and 2CREDIT300.009.00291.00BRL1300.00300.0001965f01-ff74-712a-9e2c-d987f15a1d14@account2700.00700.00BRL01965f04-7087-73b1-aff9-69f59d623d31External to accounts 1 and 2CREDIT700.0021.00679.00BRL1700.00700.0001965f02-55fc-7a06-9f95-07e9a9ff4941@account3 0 0BRL00.000.00
```
# Template formats
Source: https://docs.lerian.studio/en/reporter/template-examples
Design Reporter templates for HTML, XML, CSV, PDF, or TXT outputs — practical examples to start building reports that fit your use case.
The **Reporter** product supports multiple output formats, so you can generate exactly the file type you need, whether it's HTML, XML, CSV, PDF or plain text.
In this section, you'll find practical examples of how to structure your `.tpl` files for each format. These examples are a great starting point to help you design templates that fit your use case, follow formatting rules, and get parsed correctly during generation.
The file content **must** follow the output format you need (HTML, XML, CSV, PDF or TXT), but make sure to save it with a `.tpl` extension. **This is required for the template to work properly.**
## Available examples
***
Analytical financial report with account-level transaction analysis.
Analytical financial report in XML with custom logic and summaries.
Transaction receipt template in plain text format.
Each page includes the full template code, field-by-field explanations, and sample outputs that you can generate through Reporter.
## Tip for rendering a transaction receipt
***
To generate a transaction receipt using Reporter, you can send a request like this to the [Create a Report](/en/reference/reporter/create-report) endpoint:
```json JSON theme={null}
{
"templateId": "0196f99c-4d3e-73bc-b5d3-2a5bb1efbee4",
"ledgerId": [
"0196d97e-587e-76d8-a458-9d2517ed9aba"
],
"filters": {
"midaz_transaction": {
"transaction": {
"id": ["0196d983-a2c2-7d5a-a5b7-029fe0dcb710"]
}
}
}
}
```
### What does this filter do?
The `filters` field lets you limit the data used when rendering the template. In this case, you're telling the system to use **only** the transaction with the ID `0196d983-a2c2-7d5a-a5b7-029fe0dcb710`.
That means your receipt will include just that one transaction — and the operations linked to it — keeping the output clean and focused.
### What happens next?
Once the request is sent:
* The API returns a `reportId`.
* You can use this ID to check the status of the report.
* When it's ready, the report will be available for download.
### Why it matters
This approach gives you clarity and control. Each receipt is generated in isolation, using the filters and templates you define.
# Template reference
Source: https://docs.lerian.studio/en/reporter/template-reference
Reference every template tag, filter, and operator available in Reporter — loops, conditionals, placeholders, and built-in formatting helpers.
This page is a complete reference for all template tags, filters, and operators available in Reporter. For an introduction to templates and placeholders, see [What is Reporter](/en/reporter/what-is-reporter).
## Building templates
***
### Common blocks
* **Loop**
```
{% for in %}
...
{% endfor %}
```
* **Loop with explicit schema**
```
{% for order in external_db:sales.orders %}
{{ order.id }} - {{ order.total }}
{% endfor %}
```
* **Simple condition**
```
{% if value_a == value_b %}
...
{% endif %}
```
* **Temporary scope**
```
{% with