# Welcome Source: https://docs.lerian.studio/en/index Guides, platform capabilities, API references, and integration tutorials for the Lerian core banking platform: Midaz, the products around it, Brazil Rails, and Interfaces. # Build core banking on primitives you control. Guides, platform capabilities, and API references for the entire Lerian ecosystem, from the double-entry ledger up. [Explore the products →](/en/products/about-products) [See the integration diagrams](/en/diagrams/introduction) [Get started →](/en/start-here/getting-started) · [Start with the fundamentals →](/en/start-here/fundamentals/core-banking/what-is-core-banking) · [API reference →](/en/reference/introduction) ## Go from zero to your first transaction. 01 ### [Model your ledger](/en/start-here/getting-started) Set up your Organization, Ledger, and Accounts. 02 ### [Post a transaction](/en/products/midaz/transactions-overview) Move value with strict double-entry guarantees. 03 ### [Deploy anywhere](https://github.com/LerianStudio/helm) BYOC on AWS, GCP, on-prem, or Lerian Cloud. ## Products Lerian is a complete, modular core banking platform. Start with the components that fit your operation and compose them into the platform you need. ### [![](https://mintcdn.com/lerian-49cb71fc/5dD4BTy_lmC0R8p5/images/icons/midaz.svg?fit=max\&auto=format\&n=5dD4BTy_lmC0R8p5\&q=85\&s=016d1c1908b9ae42d80bb50a49d90bad) Midaz CORE](/en/products/midaz/what-is-midaz) The double-entry ledger at the platform's core: organizations, ledgers, accounts, and transactions you fully control. Get started with Midaz → ### [![](https://mintcdn.com/lerian-49cb71fc/5dD4BTy_lmC0R8p5/images/icons/tracer.svg?fit=max\&auto=format\&n=5dD4BTy_lmC0R8p5\&q=85\&s=84a6c37f8f2cf59ada19656e3559f3f6) Tracer](/en/products/tracer/what-is-tracer) Apply real-time spending controls and analyze transaction risk as money moves. Trace a transaction → ### [![](https://mintcdn.com/lerian-49cb71fc/5dD4BTy_lmC0R8p5/images/icons/matcher.svg?fit=max\&auto=format\&n=5dD4BTy_lmC0R8p5\&q=85\&s=55ca7734fc78f84f2aacfc823802af86) Matcher](/en/products/matcher/what-is-matcher) Reconcile transactions across banks, processors, and ERPs, and work through the exceptions that don't match. Reconcile a batch → ### [![](https://mintcdn.com/lerian-49cb71fc/5dD4BTy_lmC0R8p5/images/icons/flowker.svg?fit=max\&auto=format\&n=5dD4BTy_lmC0R8p5\&q=85\&s=82555c849a03e5f2d4d4d5c6b8372a7a) Flowker](/en/products/flowker/what-is-flowker) Orchestrate financial workflows and connect third-party services through its API. Design a workflow → ### [ Lender](/en/products/lender/what-is-lender) Run the full credit journey, from product definition and origination through servicing, accounting, and audit. Explore Lender → ### [![](https://mintcdn.com/lerian-49cb71fc/5dD4BTy_lmC0R8p5/images/icons/reporter.svg?fit=max\&auto=format\&n=5dD4BTy_lmC0R8p5\&q=85\&s=3a2ac26a15de0a860fc4566278f28c2a) Reporter](/en/products/reporter/what-is-reporter) Turn ledger and platform data into audit-ready reports from configurable templates. Build a report → ## Brazil Rails and Interfaces Where the platform meets Brazil's payment system. A Brazil rail is Lerian's own messaging with BACEN, Núclea, or Dataprev. An interface is the product surface over a rail, from Lerian or from a connectivity partner. Either way, you keep your ledger, your data, and your business logic. ### Brazil Rails What are Brazil Rails? The Brazil Rails tab documents Brazil's national payment and regulatory systems. → [Open](/en/rails/what-are-brazil-rails) Native messaging The two ways the Lerian platform reaches Brazil's payment system. → [Open](/en/rails/native-messaging) SPI (Pix) A native Pix connector to BACEN's SPI. → [Open](/en/rails/spi/what-is-lerian-spi) SPB (TED) Lerian SPB is your direct, non-intermediated BACEN STR connector. → [Open](/en/rails/spb/what-is-lerian-spb) ### Interfaces What are Interfaces? Lerian's integrations to payment products. → [Open](/en/interfaces/introduction) Pix Lerian A Pix application plugin for integration and testing with a mock provider adapter. → [Open](/en/interfaces/pix-lerian) Pix JD Direct Pix via JD: participate as a direct participant on SPI and DICT. → [Open](/en/interfaces/pix-jd/direct-pix-via-jd) TED JD Understand how the Bank Transfer plugin handles SPB communication. → [Open](/en/interfaces/ted-jd/ted-in-lerian) ## Lerian Console One web interface to operate Lerian products: dashboards, settings, and per-product modules in a single place. [About the Console →](/en/platform/console/about-lerian-console) [Console settings →](/en/platform/console/about-settings) ## Platform ### [Access Manager](/en/platform/access-manager) Control who can do what, across your Lerian products. ### [Streaming Hub](/en/platform/streaming-hub/what-is-streaming-hub) Consume platform events and fan them out to your systems through signed webhooks, queues, or a pull API (the platform's event delivery edge). ### [Multi-tenancy](/en/platform/multi-tenancy) How Lerian Cloud and supported BYOC products isolate tenants. ### [Observability](/en/platform/observability) Centralized logs, metrics, and traces across services. Also in Platform: [Narya](/en/platform/narya/what-is-narya) · [Deploy](https://github.com/LerianStudio/helm) · [Lifecycle Management](/en/platform/lifecycle-management) · [Licensing](/en/platform/licensing/license-manager) ## News Features, improvements, and changes across the platform and its documentation. [View latest announcements →](/en/news/latest-news) [Discord](https://discord.gg/DnhqKwkGv3) [GitHub](https://github.com/LerianStudio) [LinkedIn](https://www.linkedin.com/company/lerianstudio) [Instagram](https://www.instagram.com/lerian.studio) [Dev.to](https://dev.to/lerian) © 2026 Lerian. All rights reserved. # Our plugins Source: https://docs.lerian.studio/en/products/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 and add new logic, endpoints, or workflows to support specific business needs. Plugin availability is product-specific. The currently documented plugins below extend Midaz. In this documentation the plugins live under the [Interfaces](/en/interfaces/introduction) tab, as Pix JD, Pix BTG, TED JD, and Payments BTG. > [!WARNING] > > Plugins are premium features for Enterprise institutions only. Lerian licenses each plugin separately. > [!NOTE] > > **CRM** is no longer a plugin. Midaz embeds CRM as a core component, and CRM needs no additional license. See [CRM Overview](/en/products/midaz/crm/crm-overview) for details. > [!NOTE] > > **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/products/midaz/fees/fees-engine-overview) for details. ## Available plugins for Midaz *** > **[Pix](/en/interfaces/pix/pix-overview)** > > Integrate Pix with Midaz, including SPI and DICT services, to manage keys, transactions, claims, and regulatory workflows. > **[Bank Transfer (TED)](/en/interfaces/ted/ted-overview)** > > Process electronic transfers between banks through TED, managing transaction validation, settlement, and regulatory compliance. > **[Payments (Boletos & Bill Payment)](/en/interfaces/payments-btg/boleto-and-bill-payment)** > > Issue boletos and pay bills (DARF taxes and utility invoices) through the Payments module in Lerian Console, recorded in your Midaz ledger. ## Why plugins are separate services *** Lerian designed Midaz to be composable. This gives you: * **Flexibility**: Add, update, or remove plugins without downtime * **Separation of concerns**: Business-specific logic lives in the plugin, not the core * **Better control**: Each plugin has its own versioning, deployment, and monitoring * **Safe evolution**: The platform can grow without breaking your integrations > [!TIP] > > Think of a plugin as a microservice with a clear purpose. It feels native to the platform, but it evolves on its own schedule. ## What a plugin can do *** A plugin can: * Expose new APIs * Persist and transform data * Run background jobs and scheduled tasks * React to events, such as new transactions or ledger updates * Define its own configuration, rules, and access policies A plugin that follows the Midaz interface contract and authentication model integrates like a native feature. ## Building your own plugin *** We provide deployment guides to help teams build plugins that are reliable, secure, and easy to maintain. > [!NOTE] > > 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/products/about-products Discover Lerian products, platform services, Brazil Rails, and Interfaces, and how modular building blocks compose into a complete core banking platform. Lerian is a complete, modular core banking platform. Each product covers one financial domain end to end. You adopt one product on its own, or you combine several into one platform. Lerian has six products: Midaz, Tracer, Matcher, Flowker, Lender, and Reporter. Lerian licenses every product to institutions. ## Products *** > **[Midaz](/en/products/midaz/what-is-midaz)** > > 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. > **[Tracer](/en/products/tracer/what-is-tracer)** > > Validate transactions in real time, apply rules and spending limits, and return ALLOW, DENY, or REVIEW decisions. > **[Matcher](/en/products/matcher/what-is-matcher)** > > Validate external transactions against the ledger and surface mismatches. Built for accuracy, traceability, and automation. > **[Flowker](/en/products/flowker/what-is-flowker)** > > Orchestrate transaction flows using validations, approvals, and external triggers with logic and plugin coordination. > **[Lender](/en/products/lender/what-is-lender)** > > Run the credit journey end to end: loan products, origination, servicing, accounting, and Brazil-first regulatory support. > **[Reporter](/en/products/reporter/what-is-reporter)** > > Generate and download reports from reusable templates, filter their data, and manage recurring reporting deadlines. Supports HTML, XML, CSV, PDF, and TXT output. ## Platform services *** Console, Access Manager, Streaming Hub, Multi-tenancy, and Observability are platform services. They serve every product and are not products themselves. > **[Console](/en/platform/console/about-lerian-console)** > > Operate every Lerian product from one web interface, with one login and one navigation. > **[Access Manager](/en/platform/access-manager)** > > Control human and machine access across the products, and issue the tokens each service trusts. > **[Streaming Hub](/en/platform/streaming-hub/what-is-streaming-hub)** > > Consume platform events and fan them out to your systems through signed webhooks, queues, or a pull API. > **[Multi-tenancy](/en/platform/multi-tenancy)** > > Serve independent institution contexts from one deployment, and keep each request inside its own tenant. > **[Observability](/en/platform/observability)** > > Collect metrics, traces, and logs from Lerian workloads through OpenTelemetry. ## Brazil Rails and Interfaces *** A Brazil rail is Lerian-owned messaging software that connects your institution to BACEN or to a market infrastructure, with no intermediary. An interface is the product surface over a rail. Pix Lerian is an application plugin that uses a connectivity adapter and Midaz. It is not the native Lerian SPI rail. Pix JD, Pix BTG, TED JD, and Payments BTG are partner interfaces that ship as separately deployed Midaz plugins. Pix Lerian v1.0.0 is available for integration and testing with the mock provider adapter. > **[Brazil Rails](/en/rails/what-are-brazil-rails)** > > Read what each rail carries, and how Lerian native messaging reaches BACEN and the market infrastructures. > **[Interfaces](/en/interfaces/introduction)** > > Read the API, the ledger integration, and the operational controls for each payment product. # Building a complete core banking Source: https://docs.lerian.studio/en/products/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/start-here/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 combines Midaz with the products and interfaces you choose. You can adopt a product on its own. That is by design. Midaz embeds CRM and the Fees Engine. Other pieces deploy separately, and Lerian does not pre-wire them to each other. You connect them in 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. You set no environment variables and make 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. After you connect it, you can operate from the Console or call the APIs directly. That is a UX choice and needs no extra wiring. **Enterprise** is a commercial tier, not a deployment model. Enterprise clients can be **managed or self-hosted**. See [Products, platform, and plans](/en/products/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.** Midaz includes the holders and aliases you learned about. You wire nothing extra. You just enable it. → [Set up Midaz](/en/products/midaz/midaz-setup) · [CRM in Midaz](/en/products/midaz/crm/crm-overview) ## Connect the pieces to the ledger *** Most other pieces are standalone services. The Fees Engine is the exception, because it runs inside the ledger process. The pieces do not all connect the same way. Here is who calls whom at runtime: ![Runtime flow across your application, the payment plugins, the Fees Engine, Tracer, Reporter, and the Midaz ledger](https://mintcdn.com/lerian-49cb71fc/Gw2zNPznAbAjotN1/images/en/d2/flow-core-banking.svg?fit=max\&auto=format\&n=Gw2zNPznAbAjotN1\&q=85\&s=c72ab5a19b8cf1cc340e0f11599bd62b) Who calls whom at runtime * **Payment interfaces write to the ledger for you.** Your application calls a [payment interface](/en/interfaces/introduction) such as TED or Pix. The interface moves the money and records the operations in Midaz, even from its own background workers. Your application does not touch the ledger for those movements. Partner interfaces ship as Midaz plugins: separately deployed services that extend Midaz and that Lerian licenses separately. Lerian sets the connection to Midaz at deploy time, and your application never sees it. → [TED setup](/en/interfaces/ted-jd/ted-configuration) * **The Fees Engine runs inside the ledger.** Lerian licenses the **Fees Engine** as part of the Midaz ledger process. When a configured fee package applies, Midaz adds the fee calculations to the transaction you submit. No separate fee service sits between your application 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 payment interface, your application writes to the Midaz API directly. → [Connect the Fees Engine to Midaz](/en/products/midaz/fees/connecting-fees-to-midaz) * **Reporter reads configured data sources.** It reads those sources in one direction only, to build statements and reports, and 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/products/reporter/reporter-quick-start) * **Tracer connects to Midaz through configuration.** Without `TRACER_BASE_URL`, the integration stays off. Set that variable to the Tracer gRPC address, and Midaz initializes a reservation client. Midaz calls reserve, confirm, and release only when `tracer.mode` is `advisory` or `enforce`. Midaz honors a Tracer skip only when the request asks for it and the ledger enables `allowTracerSkip`. → [Tracer integration guide](/en/products/tracer/integration-guide) Your application can also integrate with Tracer on its own. > [!NOTE] > > Tracer checks your configured policies and limits, not account balances. The ledger stays the source of truth for what an account holds. The exact URLs, credentials, and environment settings live in each product's own setup docs (linked above). ## One login across the pieces *** The pieces share authentication through **Access Manager**, so they work as one product with one login. Access Manager issues the tokens each service trusts, and it holds the credentials each piece uses. Keep those details in one place, and point every piece at it. → [Access Manager](/en/platform/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 interfaces → Console** The Console comes last because it needs the others already running. → [Helm charts](https://github.com/LerianStudio/helm) ## Operate it from the Console *** After everything runs, the **Console** is your single pane of glass. Enable the pieces and manage them with one login. When you enable a module in the Console, it turns on in the interface and shares the login. The piece-to-ledger wiring is the deployment step above, and the Console does not do it for you. → [Lerian Console](/en/platform/console/about-lerian-console) ## On a managed setup *** If Lerian runs your infrastructure, Lerian already did all the wiring above. From your side, you enable the modules in the Console and start using them. You set no environment variables, and you run no deployment. The integration journey on this page is what Lerian handles underneath. ## Going further (optional) *** You assemble a complete Lerian core banking from modular building blocks. Add **[Matcher](/en/products/matcher/what-is-matcher)** for reconciliation and **[Flowker](/en/products/flowker/what-is-flowker)** for workflow automation when their capabilities fit your operation. Matcher runs in the SaaS or the BYOC model, and you sign in through Access Manager. Flowker runs in the SaaS model only. # About Midaz Source: https://docs.lerian.studio/en/products/midaz/about-midaz Explore Midaz's Ledger service, its Onboarding and Transaction domains, and its embedded CRM and Fees capabilities. Midaz is a core ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.). 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. > [!NOTE] > > This domain-driven architecture keeps configuration (Onboarding) and execution (Transaction) separate but integrated. ## 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 and configurations. One platform then supports multi-entity operations. * **Ledgers**: The core financial book of an organization. Ledgers (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.) 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 (A unit of value tracked in the ledger: traditional currencies (BRL, USD), digital currencies (BTC), or custom units like loyalty points. Every account is linked to exactly one asset.)**: 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 (A classification defining the nature and purpose of an account, such as checking, savings, escrow, or settlement. Account types enable proper transaction routing.)**: 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 lives. Each account always links to an Asset. You can group accounts into segments or portfolios to reflect your business structure. * **Portfolios (A grouping of accounts belonging to the same account holder, business unit, or purpose. It ties together checking, savings, and investment accounts for a unified view.)**: 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 (A classification applied to accounts that share characteristics, like account holder tiers (VIP, Student) or regional divisions. Rules and pricing can differ by segment.)**: 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. > [!TIP] > > The Onboarding Domain configures how your financial world is structured. It gives consistency before any transaction takes place. ### Transaction Domain After you configure the Onboarding Domain, the **Transaction Domain** takes over. It handles the real-time execution and movement 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 (A reusable template that defines the rules for one leg of a financial transaction: which accounts participate, in what direction, and under what accounting annotation.)**: Reusable templates that define the rules for each leg of a financial transaction. A route specifies which accounts participate, in what direction (source, destination, or bidirectional), 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. Every transaction is composed of one or more operations. Operations make transactions precisely traceable for compliance and reporting.)**: 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 (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) and precise traceability for compliance and reporting. * **Accounting Routes (A complete transaction blueprint composed of multiple Operation Routes. It defines all the debits and credits involved in a transaction type, so each transaction of that type complies with double-entry rules. Exposed as transactionRoute in the API.)**: Complete transaction blueprints that combine multiple Operation Routes. For example, a Pix Transfer route defines its source and destination rules. When you enable Accounting Route validation, Midaz validates the transaction route and its Operation Routes 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. > [!TIP] > > The Transaction Domain processes every movement in compliance with the business and accounting rules you configure upstream. ## Domain reference *** Midaz is a domain-driven ledger platform. | 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 | # Account types Source: https://docs.lerian.studio/en/products/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/products/midaz/v2/create-account-type) has a name, a description, a unique key value, and a default [balance direction](/en/products/midaz/balance-overdraft). 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/products/midaz/ledgers#ledger-settings). Send a `PATCH` request to the Ledger Settings endpoint: ```json PATCH /v2/organizations/{organization_id}/ledgers/{ledger_id}/settings theme={null} { "accounting": { "validateAccountType": true } } ``` ### 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. > [!TIP] > > Midaz checks the `type` only when you create an Account. You cannot change the `type` of an existing Account. ### The `keyValue` field > [!NOTE] > > 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/products/midaz/v2/create-account-type): Register a new Account Type for your Ledger. * [List Account Types](/en/reference/products/midaz/v2/list-account-types): Retrieve all Account Types configured in your Ledger. * [Retrieve an Account Type](/en/reference/products/midaz/v2/get-account-type-by-id): Get detailed information about a specific Account Type. * [Update an Account Type](/en/reference/products/midaz/v2/update-account-type): Modify the definition of an existing Account Type. * [Delete an Account Type](/en/reference/products/midaz/v2/delete-account-type): Remove an Account Type that is no longer in use. > [!NOTE] > > **See also** > > * [Transaction Routing](/en/products/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/products/midaz/accounting-entries): How Midaz resolves debit and credit rubrics for the accounts these types classify. # Accounting Entries Source: https://docs.lerian.studio/en/products/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`). 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. > [!NOTE] > > 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. If they are 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 9 action types *** Each action is a distinct transactional event. The first five actions cover the transaction lifecycle. The next three cover overdraft, block, and unblock movements. The last one covers transactions that span Ledgers. An Operation Route can map different debit and credit rubrics for each action. Every stage of an operation then receives the right accounting annotation. The Hold and Cancel rows show a balance with `direction: credit`. On a `direction: debit` balance, a hold increases `available` and a cancel decreases it. `onHold` moves the same way on both. | Action | Identifier | Description | | :- | :- | :- | | **Direct** | `direct` | Immediate, single-step debit/credit with no intermediate stages (e.g., a fee or adjustment). | | **Hold** | `hold` | Reserves funds by creating a pending movement (moves value from `available` to `onHold` on the source account). | | **Commit** | `commit` | Confirms a previously held amount, releasing the `onHold` value to the destination account. | | **Cancel** | `cancel` | Cancels/reverses a hold, returning the `onHold` value to the `available` balance on the source account. | | **Revert** | `revert` | Reverses an `APPROVED` 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. | | **Cross-ledger** | `crossLedger` | Classifies the External Account legs that Midaz adds when a transaction spans Ledgers. The **debit** rubric marks value that arrives from another Ledger. The **credit** rubric marks value that leaves to another Ledger. Both rubrics are required. | **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 those rubrics, those actions use the `direct` rubric. When needed, register dedicated rubrics for these actions in the same way as for the other actions. > [!TIP] > > Each action can point to different debit and credit rubrics on the same Operation Route. 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" } } } } ``` You manage these entries through the Operation Route endpoints. See [Create an Operation Route](/en/reference/products/midaz/v2/create-operation-route) and [Update an Operation Route](/en/reference/products/midaz/v2/update-operation-route). For the full configuration flow, refer to [Transaction Routing](/en/products/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/products/midaz/ledgers#ledger-settings) to enforce route validation. In strict mode, Midaz returns `0157` when no Operation Route of the Transaction Route has an entry for the requested action. It returns `0117` when a leg names no Operation Route, or one that the Transaction Route does not provide for that action. An overdraft movement on a route without an `overdraft` entry returns `0492`. ```json PATCH /v2/organizations/{organization_id}/ledgers/{ledger_id}/settings theme={null} { "accounting": { "validateRoutes": true } } ``` > [!TIP] > > 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/products/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 supports route-driven double-entry flows for direct transactions, as well as lifecycle actions such as cancellation. Cancellation operates on the source side only. Route validation is disabled by default. Enable it to validate the configured account and accounting-entry rules. Midaz supports digital products, embedded finance, and high-volume operations. 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, a description, and a default [balance direction](/en/products/midaz/balance-overdraft). 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. > [!NOTE] > > 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: ![Diagram showing the Accounting primitives workflow](https://mintcdn.com/lerian-49cb71fc/SFzzdxyH5SN7w_fC/images/en/d2/accounting-primitives.svg?fit=max\&auto=format\&n=SFzzdxyH5SN7w_fC\&q=85\&s=ae5fe0b8b7b390d8c8be5c76f53fe479) Accounting primitives workflow > [!TIP] > > If you are new to Midaz accounting, start with the **[Accounting Walkthrough](/en/products/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/products/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 only enough accounting context to model a real product. 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/products/midaz/accounting-in-midaz)**. ## 1. Double-entry accounting fundamentals *** Midaz supports double-entry bookkeeping through its transaction routes. Configure a route with source and destination Operation Routes, or a Bidirectional Operation Route that covers both sides. With **Validate Routes** enabled, Midaz validates each transaction against the configured route rules. Route validation is disabled by default. Enable it only after you configure the routes your Ledger needs. Lifecycle actions do not all use both sides: cancellation is source-only and releases held funds on the source Account. > [!NOTE] > > Think about *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. > [!NOTE] > > Midaz does not have a dedicated Chart of Accounts API. The CoA 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. * **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 **1. Map your financial model** List the balances you need: customer balances, internal accounts, reserve/settlement accounts, fee and revenue accounts. This is your CoA blueprint. **2. Define account types** Create an Account Type per conceptual category, e.g. `CASH`, `SETTLEMENT`, `FEE_REVENUE`, `FEE_EXPENSE`, `TREASURY`. **3. Create segments and portfolios** Segments separate business domains (`CUSTOMER_FUNDS`). Portfolios manage ownership and grouping (`customer_12345_wallet`). **4. Create accounts** 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/products/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. 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/products/midaz/amount) for the decimal value model. The two-phase actions change `available` and `onHold` on the source balance. These rows show a balance with `direction: credit`. On a `direction: debit` balance, a hold increases `available` and a cancel decreases it. `onHold` moves the same way on both. | 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/products/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 you do not configure them, 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 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 rubrics on the same Operation Route. 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/products/midaz/v2/create-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/products/midaz/accounting-entries)**. ## 5. Transaction routing *** Routing is a two-layer system that resolves at runtime: * **Operation Routes** define the rules for each leg of a transaction: its side (source, destination, or both) and an optional account rule. 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, you name its Transaction Route. With route validation enabled, Midaz checks each leg against its Operation Route and resolves the rubric for the current action. For route structure, fields, the operation-type validation matrix, and API behavior, see **[Transaction Routing](/en/products/midaz/transaction-routing-entities)**. ## 6. End-to-end example: a Pix payment *** This example is a simple Pix cash-out. A customer sends BRL out of their wallet to an external account. **1. Account setup** 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 **2. Enable route validation and register rubrics** 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. **3. Transaction** Submit a transaction against the `PIX_CASH_OUT` Transaction Route. Move, say, `100.00 BRL` from `customer_12345_brl` to `@external/BRL`. **4. Resulting operations** 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`. This gives 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 source route and the `commit` rubric on the destination 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/products/midaz/ledgers#ledger-settings). Strict mode then validates the routes on every transaction: ```json theme={null} { "accounting": { "validateRoutes": true } } ``` In strict mode, Midaz returns `0157` when no Operation Route of the Transaction Route has an entry for the requested action. It returns `0117` when a leg names no Operation Route, or one that the Transaction Route does not provide for that action. When a route resolves, Midaz stamps `routeCode` and `routeDescription` from its rubric. > [!TIP] > > 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/products/midaz/accounting-in-midaz)** overview and reference pages for full field-level detail. * Once your ledger produces structured postings, see **[Lerian Reporter](/en/products/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/products/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. > [!TIP] > > 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/products/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. * Each Account has a unique identifier within a Ledger. ## Multiple accounts per customer *** A single customer often holds more than one balance. 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 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/products/midaz/account-types), group it under the customer with a [Portfolio](/en/products/midaz/portfolios), and tie it to identity through [CRM](/en/products/midaz/crm/crm-overview). One account with labels works until the balances diverge. Separate accounts keep each balance accurate from the start. > [!TIP] > > For the full reference architecture (one customer, many accounts, with step-by-step examples), see [Midaz for multi-account customers](/en/products/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. * **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`. These accounts record inflows and outflows at the ledger boundary. > [!CAUTION] > > **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 is 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. The parent Account must be in the same Ledger and use the same Asset. You can use it for a traditional parent-child structure or for another relationship that your business needs. ## Blocked accounts *** Set `blocked` to `true` when you create or update an Account. Midaz then refuses every transaction that moves the Account, as source or destination. The block does not stop the cancellation of a pending transaction. To allow one debit of an exact amount out of a blocked Account, [create a single-use block exception](/en/reference/products/midaz/v2/create-account-block-exceptions). Then send its ID in the `accountBlockExceptionId` field of the transaction. ## 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, identify each Account by its **account alias**. 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. The alias you set can have up to 100 characters from `a-z`, `A-Z`, `0-9`, `@`, `:`, `_`, and `-`. ## Managing Accounts *** You can manage your Accounts through the API or the Lerian Console. ### Via API * [Create an Account](/en/reference/products/midaz/v2/create-account): Open a new Account linked to an Asset. * [Open a holder-owned Account](/en/reference/products/midaz/v2/create-holder-account): Open an Account that a Holder owns, and optionally its Instrument, in one call. * [List Accounts](/en/reference/products/midaz/v2/list-accounts): View all Accounts in a Ledger. * [Retrieve an Account](/en/reference/products/midaz/v2/get-account-by-id): Get details of a specific Account. * [Retrieve an Account by Alias](/en/reference/products/midaz/v2/get-account-by-alias): Get details of a specific Account by its alias. * [Retrieve an External Account](/en/reference/products/midaz/v2/get-account-external-by-code): Get details of a specific External Account by its asset code. * [Update an Account](/en/reference/products/midaz/v2/update-account): Edit the metadata or settings of an existing Account. * [Delete an Account](/en/reference/products/midaz/v2/delete-account): Delete a specific Account. * [Close an Account](/en/reference/products/midaz/v2/close-account): Close an Account whose balances are all exactly zero. Midaz keeps its history and refuses any later movement. > [!WARNING] > > **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. Midaz also refuses to delete or close an account with a balance that owes [fee debt](/en/products/midaz/fees/fee-debt) (`0527`) or that other balances owe fee debt to (`0528`). ### 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/products/midaz/console/managing-accounts) # Accounts Source: https://docs.lerian.studio/en/products/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/products/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/products/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/products/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/products/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. The v1 creation payloads use decimal `value`. The API accepts a JSON number or string. A string avoids client-side IEEE-754 rounding. The v2 `direct`, `hold`, `block`, `unblock`, and `batch` creation endpoints require string `amount`. Commit, cancel, and revert identify an existing transaction by its ID and take no amount. > [!WARNING] > > 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/products/midaz/architecture Explore Midaz's modular architecture, centered on the Ledger service and its supporting infrastructure. > [!NOTE] > > 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/products/midaz/about-midaz). Midaz can run in public or private cloud environments. Select and operate the infrastructure that fits your deployment requirements. 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. ## Repository and deployment boundary Midaz ships two runtimes in one repository and release: the Ledger modular monolith (with CRM and Fees) and Tracer. Tracer is not embedded in the Ledger process: deploy and scale it separately. Ledger HTTP v2 can opt into the one-way reservation seam to Tracer. Ledger HTTP v1 never calls it. Tracer's public HTTP surface remains `/v1`, so the seam does not create a public Tracer v2 API. Tracer never calls Ledger. ## Modular architecture *** The diagram below (*Figure 1*) shows how the pieces fit together. ![Layered diagram of Midaz: the cluster layer that you manage, the front-end layer with Lerian Console, the back-end layer with the Ledger (with CRM and Fees) and Tracer as a separate runtime, the data layer with PostgreSQL, MongoDB, Valkey, and RabbitMQ, and the Helm chart layer.](https://mintcdn.com/lerian-49cb71fc/Wqp8NG2BwBLM-rZd/images/en/d2/midaz-macro-architecture.svg?fit=max\&auto=format\&n=Wqp8NG2BwBLM-rZd\&q=85\&s=f9045b806d418acce709ff232958ea58) Figure 1. Midaz macro-architecture ### 1. Cluster layer (managed by you) You manage this layer. It provides the Kubernetes foundation that runs Midaz. Enable these services: * **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). > [!NOTE] > > 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. The Lerian Console includes Midaz as one product module. > > If you use an older version of Midaz, you may still have the standalone Midaz Console. Midaz v3.5.0 and later releases do not include it. 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 with a primary connection and an optional replica connection. * **Valkey**: A Redis-compatible key-value store. The ledger validates and updates live balances in it, then syncs them to PostgreSQL. It also caches data. * **MongoDB**: Stores semi-structured data: entity metadata, CRM holders and instruments, and fee and billing packages. * **RabbitMQ**: Message broker for asynchronous transaction processing and event exchanges of the ledger. * **Redpanda**: Kafka-compatible broker for Ledger event streams. Ledger publishes configured events to it only when you set `STREAMING_ENABLED=true` and configure streaming brokers. 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 to deploy and configure the infrastructure. The chart connects the code to Kubernetes. It deploys each component the same way across environments. > [!TIP] > > For more details, see the [Deploying using Helm](https://github.com/LerianStudio/helm/tree/main/charts/midaz) page. ## How the layers work together *** Each layer has a focused role. * Kubernetes coordinates how the layers communicate. * Helm keeps deployments and configurations in sync. You can add new components as needed without disruption to what already runs. # Assets Source: https://docs.lerian.studio/en/products/midaz/assets Define the currencies, cryptocurrencies, and tokenized instruments your Accounts hold, with 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 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. > [!NOTE] > > When you create an Asset, Midaz automatically creates an [External Account](/en/products/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/products/midaz/v2/list-assets): View all Assets in a Ledger. * [Retrieve an Asset](/en/reference/products/midaz/v2/get-asset-by-id): Get details of a specific Asset. * [Create an Asset](/en/reference/products/midaz/v2/create-asset): Register a new Asset in the Ledger. * [Update an Asset](/en/reference/products/midaz/v2/update-asset): Edit the properties of an existing Asset. * [Delete an Asset](/en/reference/products/midaz/v2/delete-asset): Mark an Asset as deleted. > [!CAUTION] > > Deleting an Asset marks its External Account as deleted. It does not move or clear any balance. Settle the balances in that Asset before you delete it. ### 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/products/midaz/console/managing-assets) # Async transaction processing Source: https://docs.lerian.studio/en/products/midaz/async-transaction-processing Async transaction processing validates the call fast, then persists the write through RabbitMQ, for lower API latency and higher throughput. ## When synchronous writes become the bottleneck *** 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. 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. For broader scaling guidance, see [Scalability strategies](/en/products/midaz/scalability-strategies). ## How it works *** ### Synchronous mode (default) ![Sequence diagram showing the client sending a POST /transaction to the Midaz API, which validates it, writes the transaction and operations to PostgreSQL, and returns 201 Created to the client.](https://mintcdn.com/lerian-49cb71fc/eJbUTctk-eLsW0J5/images/en/d2/sync-transaction-flow.svg?fit=max\&auto=format\&n=eJbUTctk-eLsW0J5\&q=85\&s=80107b258808f015d108c00ffdd6a92f) Figure 1. Synchronous transaction flow. The full flow has these steps: 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. **The API returns `201 Created`** to the client with the created transaction. The response carries the `CREATED` status, and Midaz stores the transaction as `APPROVED`. A `201` means Midaz applied the transaction to balances. **Characteristics:** * Response time includes database write latency. * Each transaction is an independent database operation. * Simpler to reason about. > [!NOTE] > > 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. ![Sequence diagram showing the client sending a POST /transaction to the Midaz API, which validates it, publishes the payload to RabbitMQ, and immediately returns 201 Created to the client. In parallel, RabbitMQ delivers the message to a background consumer, which writes the transaction and operations to PostgreSQL; balances are handled by the dedicated balance-sync worker.](https://mintcdn.com/lerian-49cb71fc/gKGrvDlaceQM1W4Y/images/en/d2/async-transaction-flow.svg?fit=max\&auto=format\&n=gKGrvDlaceQM1W4Y\&q=85\&s=be9a180c76a5d019e32f25af357905a9) Figure 2. Asynchronous transaction flow. The client receives a response as soon as the message is published, and persistence happens in the background. The full flow has these steps: 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 `CREATED` status, and Midaz stores the transaction as `APPROVED`. A `201` means Midaz applied the transaction to balances. 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 copies balances to PostgreSQL in both modes (see the **Balance synchronization** section). **Characteristics:** * Response time excludes database write latency. * List queries show a new transaction after the consumer writes it. * Background consumers write to the database at their own pace, with retries. Batch inserts require an enabled Bulk Recorder. > [!TIP] > > The validation step is identical in both modes. Balance checks, request-shape validation, and limit enforcement all happen before the API responds, regardless of processing mode. The difference is only in *when* the data hits the database. ## Built-in resilience *** If the publish to RabbitMQ fails in async mode, Midaz writes the transaction to the database in the request. If that write also fails, the transaction stays applied to balances. Midaz keeps the transaction record and retries the write in the background. Synchronous mode uses the same recovery when its database write fails. This means: * During a queue outage, Midaz writes directly to the database. * A failed database write does not reject a transaction that Midaz already applied to balances. * Midaz logs the queue failure and the deferred write so your operations team can investigate them. > [!WARNING] > > 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 ledger still connects to RabbitMQ and runs its consumer in this mode. 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 | Example value | | :- | :- | :- | | `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` | > [!TIP] > > The consumer uses separate credentials (`RABBITMQ_CONSUMER_USER` / `RABBITMQ_CONSUMER_PASS`) from the producer. The consumer republishes the messages that it retries, so it also needs write access to the transaction exchange. ## Balance synchronization *** A dedicated balance-sync worker copies balances from Redis to PostgreSQL. This worker runs in both synchronous and asynchronous modes. | 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 *** A built-in circuit breaker protects the RabbitMQ publishes of audit messages, overdraft events, and annotation transactions. 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 RabbitMQ path. 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. An annotation transaction goes straight to the database, and Midaz returns an error if that write fails. 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 reaches 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` | > [!TIP] > > 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/products/midaz/bulk-recorder) 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. | Configuration | Processing behavior | | :- | :- | | Async `false` | Direct database write per transaction (synchronous) | | Async `true`, Bulk Recorder `false` | Queue-based, one message processed at a time | ## 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 work in a development or testing environment where simplicity matters more than throughput. > [!TIP] > > 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/products/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, the balance uses `credit`. External Accounts always use `debit`. > [!NOTE] > > 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`. | > [!NOTE] > > 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) `allowOverdraft` is `false` by default. A debit cannot take a balance with `direction: credit` below zero. ```json JSON theme={null} { "key": "checking", "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", "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", "settings": { "allowOverdraft": true, "overdraftLimitEnabled": true, "overdraftLimit": "5000.00" } } ``` > [!WARNING] > > 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`. ## 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. **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. > [!NOTE] > > 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`. ### 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**. **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 | > [!TIP] > > Repayment is automatic. You cannot bypass it. Midaz reduces overdraft positions as early as possible, which keeps the balance healthy. ### Pending transactions and overdraft A `PENDING` transaction creates a hold. It does not draw overdraft. If a hold would exceed Available, Midaz rejects the transaction with error `0018 - Insufficient Funds Error`. The rejected hold does not change Available, OnHold, or `OverdraftUsed`. When you cancel a pending transaction, Midaz releases its hold. A pending transaction created by an earlier Midaz version can already carry overdraft. Midaz still unwinds that legacy state correctly during cancellation. ## 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. | > [!WARNING] > > 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 create or update a balance with `allowOverdraft` set 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`. * It mirrors the liability as a proper double-entry record, so the ledger stays balanced. > [!NOTE] > > 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`. 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. 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": "300" }, "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. > [!TIP] > > 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" } } ``` > [!TIP] > > 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", "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", "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", "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", "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", "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", "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", "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", "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. For error codes 0167–0175, see the [Midaz error list](/en/reference/products/midaz/error-list). ## Next steps *** * Learn about [Balances](/en/products/midaz/balances), the foundation that overdraft builds on. * Understand [Operations](/en/products/midaz/operations) to trace how overdraft splits appear in the ledger. * Set up the [Event Publisher](/en/products/midaz/event-publisher) to consume overdraft lifecycle events. * Explore [Transactions](/en/products/midaz/transactions) for the full picture of double-entry accounting in Midaz. # Balances Source: https://docs.lerian.studio/en/products/midaz/balances Track multiple Balances per Account to segment funds into 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. > [!CAUTION] > > 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 increases flexibility. It keeps the double-entry model (debit and credit) intact for accounting consistency, traceability, and transparency. > [!WARNING] > > 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"`. > [!NOTE] > > 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:** * **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/products/midaz/v2/update-balance) endpoint. If an update request omits a flag, its current value stays unchanged. > [!WARNING] > > Midaz reads permission flags during transaction validation. A flag change applies to transactions validated after the update succeeds. 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. > [!NOTE] > > 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/products/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. ![Balance structure relationships](https://mintcdn.com/lerian-49cb71fc/SFzzdxyH5SN7w_fC/images/en/d2/balance-structure-relationships.svg?fit=max\&auto=format\&n=SFzzdxyH5SN7w_fC\&q=85\&s=7b2fdced4a1a0db1ed925b210b01b16d) Figure 2. Balance structure relationships diagram. A balance 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/products/midaz/v1/create-transaction-json) * [Create an Inflow Transaction](/en/reference/products/midaz/v1/create-transaction-inflow) * [Create an Outflow Transaction](/en/reference/products/midaz/v1/create-transaction-outflow) * [Create a Transaction Annotation](/en/reference/products/midaz/v1/create-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. > [!CAUTION] > > Always use the `balanceKey` consistently across requests and responses. This avoids mismatches when accounts hold multiple balances. ## Balance cache key (Valkey) *** Balances in the cache (Valkey) include the `balanceKey`. ```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. > [!WARNING] > > Update direct Valkey readers to construct `balance:{transactions}:::#`, including `#default` for the default balance. ## 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`**: A new balance of a non-external account takes the `defaultDirection` of its account type when that type sets one. Otherwise, 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 with `allowOverdraft`, `overdraftLimitEnabled`, and `overdraftLimit`. > [!NOTE] > > 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, such as 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/products/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. > [!TIP] > > **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. > [!WARNING] > > 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/products/midaz/v2/get-balance-at-timestamp) - Get the state of a specific balance at a given point in time. * [Retrieve Balance History by Account](/en/reference/products/midaz/v2/get-account-balances-at-timestamp) - 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/products/midaz/v2/create-additional-balance) - Create a new balance for an account by defining a unique key. * [List Balances](/en/reference/products/midaz/v2/get-all-balances) - Retrieve all balances by organization and ledger. * [Retrieve a Balance](/en/reference/products/midaz/v2/get-balance-by-id) - Get the balance of a specific account by its unique ID. * [Retrieve Balances by Account](/en/reference/products/midaz/v2/get-all-balances-by-account-id) - Get the balance for a specific account. * [Retrieve a Balance by Account Alias](/en/reference/products/midaz/v2/get-balances-by-alias) - Get the balance with a human-readable account alias (e.g., @user123). * [Retrieve a Balance of an External Account](/en/reference/products/midaz/v2/get-balances-external-by-code) - Retrieve the balance of an external account (e.g., `@external/BRL`). * [Update a Balance](/en/reference/products/midaz/v2/update-balance) - Update the permission flags and settings of a balance. * [Delete a Balance](/en/reference/products/midaz/v2/delete-balance) - Delete a balance entry from the system. Midaz refuses to delete a balance that owes [fee debt](/en/products/midaz/fees/fee-debt) (`0527`) or that other balances owe fee debt to (`0528`). > [!TIP] > > 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/products/midaz/operations) to trace transactions that involve multiple balances. * Combine multiple balances with [Accounting Routes](/en/products/midaz/transaction-routing-entities) to create flexible and scalable financial flows. # Production ready Source: https://docs.lerian.studio/en/products/midaz/best-practices Hub for Midaz production best practices covering common mistakes, scalability strategies, banking entity hierarchy, and hardening for high-volume workloads. For direct transactions, use Accounting Routes and enable route validation after you configure the route rules your Ledger needs. Lifecycle actions can follow a different shape. Cancellation releases held funds on the source side only. These best practices help you keep financial operations transparent, auditable, and compliant. ## Content *** This section contains the following pages: * [**Common mistakes to avoid**](/en/products/midaz/common-mistakes-to-avoid): The most common mistakes with Midaz and how to avoid them. * [**Scalability strategies**](/en/products/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/products/midaz/structuring-bank-operation): A strategic approach to entity hierarchy and account structure in a banking environment. * [**Midaz production best practices**](/en/products/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/products/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: > **[Go SDK](/en/products/midaz/midaz-sdk-go)** > > Use the official Go SDK to build Midaz integrations in Go. > **[TypeScript SDK](/en/products/midaz/midaz-sdk-typescript)** > > 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. * API-focused: SDKs connect to the public Midaz APIs. Your application keeps control of its data and business logic. ## 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/products/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. 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. ![Account balance flow](https://mintcdn.com/lerian-49cb71fc/SFzzdxyH5SN7w_fC/images/en/d2/account-balance-flow.svg?fit=max\&auto=format\&n=SFzzdxyH5SN7w_fC\&q=85\&s=1d9f29821d6e2e687c6d9bd8e4c5a89f) Account balance flow ## 1. Retrieve account operations Use the [List Operations by Account](/en/reference/products/midaz/v2/get-all-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 ``` ## 2. Apply query filters 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 | | `FEE_SETTLEMENT` | Settlement of a fee debt | | `FEE_REFUND` | Refund of a settled fee debt on a revert | 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 ``` ## 3. Transform operations into statement entries 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" } } ``` ## 4. Apply statement display rules ### Use `direction` to determine the sign | Direction | Display behavior | | - | - | | `credit` | Display as a positive amount | | `debit` | Display as a negative amount | > [!WARNING] > > 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. ## 5. Handle pagination 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. > [!TIP] > > 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/products/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. ## Batch writes for high-volume workloads *** 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/products/midaz/scalability-strategies). ## How it works *** The Bulk Recorder sits between the RabbitMQ consumer and the database layer. Instead of inserting each message at once, 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. ![Sequence diagram showing RabbitMQ delivering messages to the BulkCollector, which buffers them until the batch size or timeout is reached, then sends a chunked bulk INSERT to PostgreSQL and acknowledges each message back to RabbitMQ.](https://mintcdn.com/lerian-49cb71fc/Cb_meREQqa6luqbJ/images/en/d2/bulk-recorder-flow.svg?fit=max\&auto=format\&n=Cb_meREQqa6luqbJ\&q=85\&s=cf6291e64238dddc753da5b89a5caeba) Figure 1. End-to-end flow of the Bulk Recorder between RabbitMQ and PostgreSQL. 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 works only in a single-tenant deployment and 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 ``` ## 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` | | `BULK_RECORDER_MAX_ROWS_PER_INSERT` | Maximum operation rows per `INSERT`. Values above 1,000 have no effect. | `1000` | ### 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 ``` > [!WARNING] > > 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 ``` > [!TIP] > > 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 *** These mechanisms keep the Bulk Recorder safe: ### 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. ### 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. Each `INSERT` carries at most 1,000 rows. ## 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 debug transaction processing and want a simpler, message-by-message flow. # Closing a customer account Source: https://docs.lerian.studio/en/products/midaz/closing-a-customer-account Follow the eight-step flow to freeze, settle, close, 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. Then close each account. The close operation refuses an account that still holds funds. Archive the CRM records last. This guide covers the full closure flow, from the [Holder](/en/products/midaz/crm/holders) to its [Alias Accounts](/en/products/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. > [!WARNING] > > Follow the steps in order. Do not archive CRM records before you close the accounts. ## 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 | Close the Ledger Accounts and archive CRM records under retention policy. | > [!NOTE] > > 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}` and `/v2/.../accounts/{accountId}` for readability. ## Prerequisites *** Before you start, make sure you have: * The `holderId` of the customer to offboard. * The `accountId` values linked to that Holder across every Ledger in the Organization. Retrieve them with `GET /v2/organizations/{organization_id}/holders/{holder_id}/accounts`. The response is paginated, so retrieve every page. Add the optional `ledger_id` query parameter only when you need to narrow the list to one Ledger. * The `instrument_id` of each Alias Account, mapped to its corresponding `accountId`. List the Holder's instruments with `GET /v2/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. Step 5 needs `post` and `delete` access on the `accounts` resource. > [!WARNING] > > Closing an account cannot be undone. 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 /v2/organizations/{organization_id}/holders/{holder_id} ``` ```json theme={null} { "naturalPerson": { "status": "INACTIVE" } } ``` > [!NOTE] > > 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/products/midaz/balances) of the account. Then update each balance to disable receiving. If a balance has a non-zero `overdraftUsed`, repay it first: a balance that refuses inflows also refuses the repayment. **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 } ``` > [!WARNING] > > 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. > [!TIP] > > 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/products/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/products/midaz/v2/commit-transaction) or [Cancel a pending transaction](/en/reference/products/midaz/v2/cancel-transaction). > [!WARNING] > > The close operation in Step 5 refuses an account whose funds a pending transaction still holds. ### Step 4: Zero the balance Return any remaining funds to the customer (the account holder) and confirm `available = 0`, `onHold = 0`, and `overdraftUsed = 0` on every balance. * If a balance owes [fee debt](/en/products/midaz/fees/fee-debt), settle it first with [Collect open fee debts](/en/reference/products/midaz/v2/collect-a-balance-s-open-fee-debts). The close operation refuses a balance that owes fee debt (`0527`). * 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/products/midaz/v1/create-transaction-json). * **Confirm `available = 0`, `onHold = 0`, and `overdraftUsed = 0`** on every balance of every account in the Ledger before you proceed. You can check this with [Retrieve balances by account](/en/reference/products/midaz/v2/get-all-balances-by-account-id). > [!WARNING] > > The close operation refuses an account with any balance whose `available`, `onHold`, or `overdraftUsed` amount is not exactly zero. ### Step 5: Close the Ledger Accounts in Midaz After you zero the balances and clear pending activity, close each Ledger Account. ```http theme={null} POST /v2/.../accounts/{accountId}/close ``` The request takes no body. A successful request returns `204 No Content`, and every later read of the account shows the closing instant in `closedAt`. Repeat for every account linked to the Holder. See [Close an account](/en/reference/products/midaz/v2/close-account) for the full contract and the [Midaz error list](/en/reference/products/midaz/error-list) for each refusal. Then delete each closed account with `DELETE /v2/.../accounts/{accountId}`. Step 8 refuses to archive a Holder that still owns an account that is not deleted. See [Delete an account](/en/reference/products/midaz/v2/delete-account). > [!NOTE] > > Deleting an account is a logical removal. Its transactions remain available for audit and reporting. Midaz refuses every later movement on a closed account, and unblocking the account does not reopen it. An external account cannot be closed. ### 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 /v2/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} ``` ```json theme={null} { "bankingDetails": { "closingDate": "2026-06-09" } } ``` > [!NOTE] > > The `closingDate` field lives in the `bankingDetails` object of the Alias Account and uses `YYYY-MM-DD` format. See [Alias Accounts](/en/products/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 /v2/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} ``` > [!WARNING] > > 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/products/midaz/v2/delete-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 /v2/organizations/{organization_id}/holders/{holder_id} ``` > [!WARNING] > > 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/products/midaz/v2/delete-holder). ## Via Console *** Run the same eight-step closure flow from the [Midaz Console](/en/products/midaz/console/midaz-module). The Console covers most of the flow point-and-click, but blocking credits (Step 2) and closing the accounts (Step 5) require the API. Each step below notes the equivalent API step on this page. > [!WARNING] > > The order is the same as the API flow. Do not archive CRM records before you close the accounts. ### 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. **Step 1** From the **Holders** page, find the Holder to close. **Step 2** Click the three dots () in the **Actions** column, and select **Edit**. **Step 3** In the Holder form, set the **Status** to **Inactive**. **Step 4** Confirm the update. > [!NOTE] > > 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/products/midaz/console/crm-editing-a-holder). ### Step 2: Block credits on the accounts > [!WARNING] > > **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. Repay any non-zero `overdraftUsed` before this step. ### Step 3: Settle pending activity Confirm there are no in-flight movements before you zero any balance. **Step 1** 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. **Step 2** 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`, `onHold = 0`, and `overdraftUsed = 0` on every balance. **Step 1** 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/products/midaz/console/creating-a-transaction). **Step 2** Open each account and confirm **available = 0**, **onHold = 0**, and **overdraftUsed = 0** before you continue. > [!WARNING] > > The close operation refuses an account with any balance whose `available`, `onHold`, or `overdraftUsed` amount is not exactly zero. ### Step 5: Close the Ledger Accounts in Midaz > [!WARNING] > > **Closing requires the API.** Follow [Step 5: Close the Ledger Accounts in Midaz](#step-5-close-the-ledger-accounts-in-midaz) in the Via API section. Then delete each closed account: **Step 1** From the **Accounts** page, find the Account linked to the Holder, click the three dots () in the **Actions** column, and select **Delete**. **Step 2** A confirmation dialog will appear. Click **Confirm** to finalize the deletion. **Step 3** Repeat for every account linked to the Holder. See [Deleting an Account](/en/products/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. **Step 1** From the **Alias Accounts** page, find the alias account to update, click the three dots () in the **Actions** column, and select **Edit**. **Step 2** In the Alias Account form, set the **Closing Date** (in `bankingDetails`) to the official closure date using `YYYY-MM-DD` format. **Step 3** Confirm the update. > [!NOTE] > > BACEN account-lifecycle reporting needs an accurate closing date. See [Editing an Alias Account](/en/products/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. **Step 1** From the **Alias Accounts** page, find the alias account to archive, click the three dots () in the **Actions** column, and select **Delete**. **Step 2** A confirmation dialog will appear. Click **Confirm** to finalize. > [!WARNING] > > 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/products/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**. **Step 1** From the **Holders** page, find the Holder to archive, click the three dots () in the **Actions** column, and select **Delete**. **Step 2** A confirmation dialog will appear. Click **Confirm** to finalize. > [!WARNING] > > 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/products/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 before you close an account. The close operation refuses an account that holds funds. * **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.** The operations of a deleted account remain queryable for reconciliation and reporting. > [!TIP] > > 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/products/midaz/common-mistakes-to-avoid Avoid the typical pitfalls of Midaz implementations in 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. ## 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 retries with backoff for transient errors, and send the same `X-Idempotency` key on each retry. Log permanent failures. Make sure upstream systems, such as core banking applications, handle rejected transactions and notify the user. # Community support Source: https://docs.lerian.studio/en/products/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 mission-critical service. If you're using the Community version, this page outlines the channels where you can ask questions, report bugs, and share ideas. > [!CAUTION] > > If you're an **Enterprise** institution, use the [**Lerian Support Portal**](/en/products/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 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. ## Contribute *** If you’re ready to go a step further: * Fork the repo, make your changes, and submit a PR. * Share your own plugin ideas or integrations. All contributions are welcome, from fixing a typo to designing a new feature. ## Not sure where to start? *** You don’t need permission to get involved. # Midaz entities Source: https://docs.lerian.studio/en/products/midaz/core-entities See how Midaz core entities (Organizations, Ledgers, Assets, Portfolios, Accounts, Segments, and Balances) connect to support 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. ![Midaz core building blocks and how its interconnected domains fit together to support financial operations at scale](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/midaz-entities-v3.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=bda0ca5101ade6a55c127a3375af6e62) Figure 1. Midaz and its interconnected domains and modular structure. ## 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 processes 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. ### Accounts & Segments An account can belong to a portfolio. **Segments** group accounts, so you can isolate data by customer, product, region, or any other dimension. **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 balance amounts from ledger operations. You can add and delete balances and change their settings. ### 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/products/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. When route validation is enabled in the ledger settings, they enforce the configured transaction structure. (The API calls this `transactionRoute`.) This pair defines the transaction structure. When you enable route validation in the ledger settings, Midaz validates transactions against the configured route. ## Get help *** > [!TIP] > > **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/products/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 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 validates and updates all balances of a transaction in one atomic step. 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. > [!NOTE] > > 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/products/midaz/console/midaz-module)**. If you run an older version, you may still have the standalone Midaz Console. # Data model Source: https://docs.lerian.studio/en/products/midaz/data-model Reference the core tables and schemas that power Midaz, designed for developers and database administrators working close to the data. > [!TIP] > > **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/products/midaz/about-midaz). Explore the core tables that power Midaz. As Midaz evolves, it may add new fields and tables. Midaz keeps the tables on this page in two PostgreSQL databases: * **Onboarding database**: `organization`, `ledger`, `asset`, `account_type`, `account`, `portfolio`, and `segment`. * **Transaction database**: `balance`, `operation_route`, `operation`, `transaction_route`, and `transaction`. Midaz stores the `metadata` of each entity in MongoDB, not in these tables. Column names are database names and can differ from the API field names. ## Tables *** ### Table: `organization` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Organization. | | **parent\_organization\_id** | `uuid` | The unique identifier of the parent Organization. | | **legal\_name** | `text` | The legal name of the Organization. | | **doing\_business\_as** | `text` | The trade name of the Organization. | | **legal\_document** | `text` | The document of the Organization. | | **address** | `jsonb` | The address of the Organization. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **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: `ledger` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Ledger. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **name** | `text` | The name of the Ledger. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **settings** | `jsonb` | The [Ledger settings](/en/products/midaz/ledgers#ledger-settings). | | **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: `asset` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Asset. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **name** | `text` | The name of the Asset. | | **type** | `text` | The type of Asset (`currency`, `fiat`, `crypto`, `commodity`, `others`). | | **code** | `text` | The code used to refer to the Asset. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **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: `account_type` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Account Type. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **name** | `varchar(100)` | Name of the Account Type. | | **description** | `text` | Description of the Account Type. | | **key\_value** | `varchar(50)` | The key that identifies the Account Type in the Ledger. | | **default\_direction** | `varchar(16)` | The default [balance direction](/en/products/midaz/balance-overdraft) for Accounts of this type: `credit` or `debit`. | | **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: `account` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Account. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **asset\_code** | `text` | The code used to refer to the Asset. | | **name** | `text` | The name of the Account. | | **alias** | `text` | The alias that transactions use to address the Account. | | **type** | `text` | The type of account. | | **parent\_account\_id** | `uuid` | The unique identifier of the Parent Account. | | **entity\_id** | `text` | A reference to a record in an external system. | | **holder\_id** | `uuid` | The unique identifier of the Holder that owns the Account. | | **holder\_check\_skipped** | `boolean` | `true` when a `skip` flag bypassed the Holder check at creation. | | **portfolio\_id** | `uuid` | The unique identifier of the Portfolio. | | **segment\_id** | `uuid` | The unique identifier of the Segment. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **blocked** | `boolean` | `true` when the Account is blocked. | | **closed\_at** | `timestamptz` | Timestamp of closing, if applicable (UTC). | | **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: `portfolio` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Portfolio. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **entity\_id** | `text` | The unique identifier of the user responsible for the Portfolio. | | **name** | `text` | The name of the Portfolio. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **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: `segment` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Segment. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **name** | `text` | The name of the Segment. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **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** | `uuid` | The unique identifier of the Balance. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **account\_id** | `uuid` | The unique identifier of the Account. | | **alias** | `text` | The alias of the Account. | | **key** | `text` | The balance key. The default balance uses `default`. | | **asset\_code** | `text` | The code used to refer to the Asset. | | **available** | `decimal` | The available amount. | | **on\_hold** | `decimal` | Amount on hold/reserved. | | **version** | `bigint` | Balance version. | | **account\_type** | `text` | The type of account. | | **allow\_sending** | `boolean` | If true, indicates that sending transactions is permitted. | | **allow\_receiving** | `boolean` | If true, indicates that receiving transactions is permitted. | | **direction** | `varchar(16)` | The [balance direction](/en/products/midaz/balance-overdraft): `credit` or `debit`. | | **overdraft\_used** | `decimal` | The overdraft amount in use. | | **settings** | `jsonb` | The balance settings, such as the overdraft limit. | | **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: `operation_route` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Operation Route. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. Empty for a route that belongs to the Organization. | | **title** | `varchar(255)` | Short text summarizing the purpose of the operation. Used as an entry note for identification. | | **description** | `varchar(250)` | Detailed description of the Operation Route purpose and usage. | | **code** | `text` | The code of the Operation Route. | | **operation\_type** | `varchar(20)` | The entry's applicability: `source`, `destination`, or `bidirectional`. | | **account\_rule\_type** | `varchar(20)` | The rule that selects the account: `alias` or `account_type`. | | **account\_rule\_valid\_if** | `text` | The values that the account rule accepts. | | **accounting\_entries** | `jsonb` | The [accounting entries](/en/products/midaz/accounting-entries) of the route. | | **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: `operation` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Operation. | | **transaction\_id** | `uuid` | The unique identifier of the Transaction. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **account\_id** | `uuid` | The unique identifier of the Account. | | **account\_alias** | `text` | The alias for the account used in the operation. | | **balance\_id** | `uuid` | The unique identifier of the Balance. | | **balance\_key** | `text` | The key of the Balance. | | **description** | `text` | Description of the operation. | | **type** | `text` | The type of the operation, such as `DEBIT`, `CREDIT`, `ON_HOLD`, or `RELEASE`. | | **direction** | `varchar(10)` | The direction of the operation: `debit` or `credit`. | | **asset\_code** | `text` | The code used to refer to the Asset. | | **amount** | `decimal` | The amount of the operation. | | **available\_balance** | `decimal` | The available amount before the operation. | | **on\_hold\_balance** | `decimal` | The amount on hold before the operation. | | **available\_balance\_after** | `decimal` | The available amount after the operation. | | **on\_hold\_balance\_after** | `decimal` | The amount on hold after the operation. | | **balance\_version\_before** | `bigint` | The balance version before the operation. | | **balance\_version\_after** | `bigint` | The balance version after the operation. | | **balance\_affected** | `boolean` | `false` when the operation does not change the balance, as in an annotation transaction. | | **chart\_of\_accounts** | `text` | The name of the chart of accounts that the operation belongs to. | | **route** | `text` | **\[Deprecated]** Legacy route identifier, kept for backward compatibility. Use `route_id` instead. | | **route\_id** | `uuid` | Identifier of the route used by the operation (the current route field). | | **route\_code** | `text` | The code of the accounting rubric that Midaz resolved for the operation. | | **route\_description** | `text` | The description of the accounting rubric that Midaz resolved for the operation. | | **snapshot** | `jsonb` | System context of the operation, such as the overdraft use before and after it. | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **recorded\_at** | `timestamptz` | Timestamp of when the ledger recorded the operation (UTC). | | **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: `transaction_route` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the Transaction Route. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. Empty for a route that belongs to the Organization. | | **title** | `varchar(255)` | Short text summarizing the purpose of the transaction. Used as an entry note for identification. | | **description** | `varchar(250)` | A description for the Transaction Route. | | **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). | The `operation_transaction_route` table links each Transaction Route to its Operation Routes. ### Table: `transaction` | Column | Type | Description | | :- | :- | :- | | **id** | `uuid` | The unique identifier of the transaction. | | **parent\_transaction\_id** | `uuid` | The unique identifier of the parent transaction, such as the transaction that a revert reverses. | | **group\_id** | `uuid` | Links the transactions that one cross-ledger request creates. | | **organization\_id** | `uuid` | The unique identifier of the Organization. | | **ledger\_id** | `uuid` | The unique identifier of the Ledger. | | **description** | `text` | Description of the transaction. | | **route** | `text` | **\[Deprecated]** Legacy route identifier, kept for backward compatibility. Use `route_id` instead. | | **route\_id** | `uuid` | Identifier of the route used by the transaction (the current route field). | | **status** | `text` | The status code. | | **status\_description** | `text` | The description of the status. | | **amount** | `decimal` | The sent amount. | | **asset\_code** | `text` | The code used to refer to the Asset. | | **chart\_of\_accounts\_group\_name** | `text` | The name of the chart of accounts group used to categorize the operations of a transaction. | | **body** | `jsonb` | Internal copy of the transaction request. | | **fees\_skipped** | `boolean` | `true` when a `skip` flag bypassed the fee calculation. | | **tracer\_skipped** | `boolean` | `true` when a `skip` flag bypassed Tracer. | | **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). | ## Data model *** *Figure 1* shows the core Midaz tables and their relationships. Use it to guide data handling and integration. ![Core Midaz database tables and the relationships between them, as a reference for data handling and integration](https://mintcdn.com/lerian-49cb71fc/0kJvev-xNu0CYwa1/images/en/d2/midaz-data-model-v3.svg?fit=max\&auto=format\&n=0kJvev-xNu0CYwa1\&q=85\&s=a5eb57d19d7615145bcf1d20b23c419c) Figure 1. Midaz tables and their relations. # Event publisher Source: https://docs.lerian.studio/en/products/midaz/event-publisher Publish Midaz transaction lifecycle CloudEvents to Kafka-compatible brokers for downstream integrations. ## Uses for transaction lifecycle events *** Transaction lifecycle events let downstream systems react without coupling to the transaction API. Use them for notifications, reconciliation, analytics, and compliance workflows. For the complete catalog and consumer contract, see [Receiving Midaz events](/en/products/midaz/receiving-midaz-events). ## Enable transaction lifecycle events *** Midaz publishes lifecycle CloudEvents to a Kafka-compatible broker. Publication is off by default. Configure the ledger application with a broker list and its CloudEvents source: ```env theme={null} STREAMING_ENABLED=true STREAMING_BROKERS= STREAMING_CLOUDEVENTS_SOURCE=ledger ``` You must set `STREAMING_CLOUDEVENTS_SOURCE` when you enable streaming. The ledger, CRM, and Fees binary uses `ledger`. The transaction facts publish to `lerian.streaming.ledger`. If you enable streaming without `STREAMING_BROKERS`, Midaz refuses to start. When streaming is off, Midaz uses a no-op emitter. ## Lifecycle events *** Midaz emits these public facts for eligible persisted transaction state changes: | Fact | When Midaz emits it | | - | - | | `transaction.posted` | A newly created transaction is `APPROVED` and has no parent transaction. | | `transaction.reverted` | A newly created `APPROVED` transaction has a parent transaction. | | `transaction.committed` | A transaction status update reaches `APPROVED`. | | `transaction.canceled` | A transaction status update reaches `CANCELED`. | Midaz does not emit a lifecycle fact for a newly created `PENDING` transaction, a `NOTED` transaction, or a no-op state change. A pending transaction emits only when a later status update commits or cancels it. When one request moves value across ledgers, Midaz also emits one fact for the whole movement: `transaction_group.posted`, `transaction_group.committed`, `transaction_group.canceled`, or `transaction_group.reverted`. ## Consume events *** Subscribe to `lerian.streaming.ledger` and select the facts you need using the CloudEvents headers. Do not infer event type from the Kafka topic: Midaz uses one application topic for ledger, CRM, and Fees facts. Use the producer manifest as the deployment-time contract: ```http theme={null} GET /v1/streaming/manifest ``` The manifest remains available even when publication is off. The [Midaz event reference](/en/reference/events/midaz) lists the event payloads. > [!NOTE] > > Event publication is best effort and does not make the broker write transactional with the ledger mutation. Consumers must be idempotent and deduplicate on `(ce-source, ce-id)`. # FAQ Source: https://docs.lerian.studio/en/products/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. > **Is there a maximum number of records per page in API listings? Can I increase this limit?** > > Yes. In Midaz, 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. Most listings accept 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. ## Tenants on Lerian Cloud *** These questions cover data isolation and tenant scoping on Lerian Cloud. > **Is my data isolated from other institutions in SaaS?** > > Yes. Your data is isolated from every other tenant, with its own credentials. The platform resolves your tenant from the JWT on every request. There is no way to access another tenant's data through the API. > **Do I need to pass a tenant ID in my API requests?** > > 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. > **Can I have multiple Organizations under a single tenant?** > > 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. > **Is the API different between SaaS and self-hosted deployments?** > > No. The API surface is identical: same endpoints, same payloads, same responses. 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 > **Do different Organizations communicate with each other?** > > Each Organization operates independently. A cross-ledger transaction can move value between Ledgers of different Organizations in the same tenant. > **Can I use a single license across multiple Organizations?** > > See [Lerian's License](/en/start-here/evaluate-and-deploy/lerians-license) for how each license applies to Organizations. > **Can an Organization have multiple Plugins?** > > Yes. An Organization can have more than one Plugin. > **Can an Organization have multiple Ledgers?** > > Yes. An Organization can manage multiple Ledgers. > **Can I create transactions between a Parent Organization and a Child Organization?** > > You can create a **Parent Organization** and a **Child Organization**. Each Organization keeps its own Ledger and operates independently. To move value between their Ledgers, send one cross-ledger transaction. Every participating Ledger must set `settings.crossLedger.enabled` to `true`, and none can have `accounting.validateRoutes` set to `true`. You can also orchestrate the transfer with these steps: > > **Step 1** > > 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. > > **Step 2** > > 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. ### Ledgers > **Do different Ledgers communicate with each other?** > > Ledgers keep separate books. A cross-ledger transaction moves value between Ledgers that set `settings.crossLedger.enabled` to `true`. > **How can I make transactions between Ledgers?** > > Send one `POST /v2/transactions/direct` or `POST /v2/transactions/hold` request with legs in each Ledger. Every participating Ledger must set `settings.crossLedger.enabled` to `true`. Midaz creates one balanced transaction per Ledger. You can also move the amount through an External Account in two steps: > > **Step 1** > > Ledger A -> External Account. > > **Step 2** > > External Account -> Ledger B. > **Do I need a separate Ledger for each Plugin?** > > No. A single Ledger can support multiple Plugins. For example, one Ledger can handle both Exchange and Pix Plugins. ### Assets > **Can an Asset be linked to multiple Accounts?** > > Yes. Many Accounts can use the same Asset. Each Asset also links to an External Account. Midaz creates that External Account automatically when you create the Asset. > **What types of Assets can I use?** > > 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. > * *commodity*: Tangible goods like gold, soybean, and oil. > * *others*: Custom Assets, including loyalty points and tokenized securities. ### Portfolios > **How does a Portfolio work?** > > A **Portfolio** groups accounts that belong to the same entity (**CPF/CNPJ**). For example, one CPF with two different `segmentId` values has two matching accounts. You create a Portfolio for that CPF to link both accounts under one structure. ### Accounts > **Can an Account be associated with multiple Assets?** > > No. Each Account links to a single Asset. You cannot change this link. > **What is an External Account?** > > An External Account receives funds from outside the Ledger. It brings money into the system. > **How can I create an External Account?** > > Midaz creates an **External Account** automatically when you create an Asset. You can also create one yourself: send `type` as `external` with an `alias`. > **Can an Account be linked to several Segments?** > > No. Each account links to only one Segment (`segmentId`). > **Is there a limit to how many Accounts I can create in Midaz?** > > No. You can create as many Accounts as you need. Midaz sets no limit on the number of Accounts. > **What is the process for adding funds to an account or performing a cash-in using money coming from outside the Ledger (Midaz) environment?** > > 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 > **What is the minimum structure of a Transaction?** > > 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. > **Is it possible to generate a PDF transfer receipt containing the details of a completed transaction?** > > 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 > **How can I create an Entity?** > > The `entityId` field accepts external IDs of up to 256 characters. Midaz does not validate their format. You can use the IDs that already exist in your database and integrate them into your system. ### Idempotency > **Can I reuse an idempotency key across endpoints?** > > No. Scope each key to a single operation and endpoint. > **What happens if I change the TTL on a retry?** > > Midaz uses only the TTL from the first request. A later change has no effect. > **Will the replayed response always be identical?** > > 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`. > **What's the default TTL if I don't send X-TTL?** > > The default TTL is **300 seconds** (5 minutes). Send the `X-TTL` header to set a custom value in seconds. ### Accounting in Midaz > **How can I reflect my own Chart of Accounts in Midaz?** > > Midaz lets you mirror your organization's official **Chart of Accounts** in the platform. You configure two core features: > > * [Account Types](/en/products/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 account type validation (`validateAccountType`), the `type` field in the Accounts API must match a registered value. > * [Accounting Routes](/en/products/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 every transaction against your Chart of Accounts when you enable route validation (`validateRoutes`). 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. > **What are Plugins?** > > 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. > **How are plugins distributed?** > > After you contract a plugin, Lerian provides it. In the [BYOC model](/en/start-here/evaluate-and-deploy/deployment-models), the plugin runs in your infrastructure, next to your Midaz instance. Applications connect to each plugin according to its function. > **What plugin options does Lerian offer?** > > 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 > **What is Fees Engine?** > > 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/products/midaz/fees/fees-engine-overview). It works across three main domains: > > * **Fee Packages (`/v2/organizations/{organization_id}/ledgers/{ledger_id}/packages`):** defines charging rules per transaction (flat fee, percentage, or whichever is greater). > * **Billing Packages (`/v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages`):** defines periodic charges based on transaction volume or account maintenance. > * **Calculation and estimation:** the Ledger evaluates fees during the transaction flow. Use `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/estimates` for a package-specific preview and `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing/calculate` for periodic billing. > **How does Fees Engine fit into the Lerian ecosystem?** > > 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. > **What do I need to send in every request to Fees Engine?** > > Fees endpoints are organization-scoped through the organization ID in the `/v2` URL path. The platform resolves tenant context from the authenticated request; send the authorization material required by your Access Manager configuration. > > ``` > Authorization: Bearer *** > ``` > **What database does Fees Engine use for storage?** > > 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. > **What Midaz version provides the integrated Fees module?** > > Midaz v4 exposes Fees inside the unified Ledger on `/v2`. ### Fee Packages > **What is a Fee Package?** > > 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/products/midaz/fees/using-fee-engine). > **How do I create a Fee Package?** > > Send a `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/packages` with the following body. See the [Create Package API reference](/en/reference/products/midaz/v2/create-package) for full details. > > ```json theme={null} > { > "feeGroupLabel": "Digital Account Fees", > "segmentId": "{segment_id}", > "minimumAmount": "100.00", > "maximumAmount": "50000.00", > "transactionRoute": "{transaction_route_id}", > "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" > } > } > } > ``` > **Can a package be temporarily disabled?** > > 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. > **How does the package scope work (minimumAmount / maximumAmount)?** > > 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. > **Can I filter a package by transaction route?** > > Yes. Set the `transactionRoute` field to a transaction route ID. Fees Engine then considers the package only for transactions that carry that `routeId`. > **What are waivedAccounts?** > > These are account aliases that the package **exempts** from fees. When every source account of a transaction is in `waivedAccounts`, Fees Engine does not apply the package fees. A waived account does not pay a package fee. > > ```json theme={null} > "waivedAccounts": ["vip-account", "employee-account"] > ``` > > This package does not charge these accounts. > **Do listing endpoints have pagination?** > > Yes. The listing endpoints (`GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/packages`, `GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages`) support the `limit` and `page` query parameters for pagination. > > ``` > GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/packages?limit=20&page=2 > ``` ### Calculation Models > **What calculation models are available?** > > The `applicationRule` field inside `calculationModel` defines how Fees Engine calculates the fee. See [Calculation Models](/en/products/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` | Evaluates 2 or more calculations; applies the **greater** result | > **How do I configure a flat fee (flatFee)?** > > 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. > **How do I configure a percentage fee (percentual)?** > > 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. > **How does maxBetweenTypes work?** > > `maxBetweenTypes` requires **2 or more calculations**. Fees Engine calculates each 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**. > **Can I mix multiple percentages in maxBetweenTypes?** > > Yes. You can include any combination of `flat` and `percentage`. Fees Engine evaluates all of them and applies the greatest. `flatFee` and `percentual` require exactly 1 calculation. Only `maxBetweenTypes` accepts 2 or more. ### Important Fields > **What is referenceAmount and how does it affect calculation?** > > 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. > > > [!NOTE] > > > > The fee with `priority: 1` runs first, so it **must** use `originalAmount`. No prior fees exist to consider. > **What is isDeductibleFrom and when should I use it?** > > When `isDeductibleFrom: true`, Fees Engine deducts the fee from the amount the sender sends. The recipient receives the discounted amount. > > 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` > **How does the priority field work?** > > 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. > **What is creditAccount?** > > 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" > ``` > **What are routeFrom and routeTo inside a fee?** > > 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 > **What are Billing Packages?** > > Billing Packages are **periodic** charge packages, independent of per-transaction fee calculation. See [Billing Package examples](/en/products/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. > **When should I use volume billing?** > > 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" > } > ``` > **When should I use maintenance billing?** > > 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": "{segment_id}" > } > } > ``` > > > [!NOTE] > > > > `accountTarget` must have exactly **one** of the three fields: `segmentId`, `portfolioId`, or `aliases` (maximum 100 aliases). > **How do tiers work in volume billing?** > > 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. For multiple tiers, the last tier must omit `maxQuantity`. A single bounded tier is accepted. A positive billable count outside that tier fails the entire calculation call. > > **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" } > ] > ``` > **What is freeQuota?** > > 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. > **What are discountTiers?** > > These are discount tiers for volume billing. They reduce the charged amount based on extra criteria. They complement the logic of the main `tiers`. > **What is countMode in volume billing?** > > 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 billing > **How are transaction fees calculated in Midaz v4?** > > The Ledger evaluates fees in the transaction flow when a matching package applies. Use `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/estimates` for a package-specific preview. > **Which v4 endpoints configure fees?** > > Configure transaction fee packages at `/v2/organizations/{organization_id}/ledgers/{ledger_id}/packages` and periodic billing packages at `/v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages`. > **How do I calculate periodic billing?** > > Call `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing/calculate` after you configure billing packages. It processes the configured rules for that Ledger. ### Common Errors > **What does error 0188 "originalAmount is required when priority is one" mean?** > > 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" > } > ``` > **How do I fix error 0205 "originalAmount is required when isDeductibleFrom is true"?** > > Fees with `isDeductibleFrom: true` can only use `referenceAmount: "originalAmount"`. Update the field: > > ```json theme={null} > { > "isDeductibleFrom": true, > "referenceAmount": "originalAmount" > } > ``` > **Why do I get error 0208 "calculation value flat invalid"?** > > 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. > **When does error 0207 "calculation value percentage invalid" occur?** > > 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. > **How do I fix error 0218 "tiers have a gap"?** > > 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" } > ``` > **Error 0229: "Exactly one of segmentId, portfolioId, or aliases must be provided"** > > In maintenance billing, the `accountTarget` field accepts only **one** of the three options. Do not combine fields: > > ```json theme={null} > // ❌ Wrong > "accountTarget": { > "segmentId": "{segment_id}", > "portfolioId": "{portfolio_id}" > } > > // ✅ Correct > "accountTarget": { > "segmentId": "{segment_id}" > } > ``` > **Is there a limit on aliases in accountTarget?** > > Yes. The `aliases` field accepts a maximum of **100 aliases** per maintenance Billing Package. > **Errors 0189 and 0191: flatFee needs exactly 1 calculation of type flat** > > 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" }] > } > ``` > **Errors 0189 and 0190: percentual needs exactly 1 calculation of type percentage** > > Like `flatFee`, the `applicationRule: "percentual"` accepts exactly 1 calculation of type `percentage`. > **Error 0201: "applicationRule maxBetweenTypes must have more than 1 calculation"** > > `maxBetweenTypes` requires at least 2 calculations, because it needs values to compare. > **What should I check when the package is not applied to the transaction?** > > Diagnostic checklist (see also [Best Practices](/en/products/midaz/fees/fees-engine-best-practices)): > > * **`enable`:** is the package active (`enable: true`)? > * **Ledger:** did you create the package in the ledger of the transaction? > * **`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? > **Can a deleted record be recovered?** > > Fees Engine soft-deletes records. It marks them with `deletedAt` and does not remove them from the database. The API has no restoration endpoint. Contact the Lerian team if you need to recover a deleted record. # Foundation Source: https://docs.lerian.studio/en/products/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 *** * [**Organizations**](/en/products/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/products/midaz/ledgers): Ledgers track all transactions and operations. Organizations can use multiple Ledgers to segregate financial operations by business line, geography, or regulatory requirement. * [**Assets**](/en/products/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. # Getting help Source: https://docs.lerian.studio/en/products/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. ## Content *** * [**Community support**](/en/products/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/products/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. # Ledgers Source: https://docs.lerian.studio/en/products/midaz/ledgers Use Ledgers to track the Transactions and Operations of each Organization and to orchestrate cross-ledger transfers through the External Account of each Ledger. A **Ledger** keeps a precise record of all the Transactions and Operations of an Organization. A Ledger tracks every financial event: deposits, withdrawals, transfers, and fees. 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. * Use multiple Ledgers only when you need to segregate data or operations, for example internal treasury operations versus customer accounts. > [!WARNING] > > By default, a transaction **cannot** move funds between Ledgers. A transaction can span Ledgers only when every Ledger it touches enables the `crossLedger.enabled` setting. ### How to orchestrate cross-Ledger transactions When every Ledger involved enables `crossLedger.enabled`, send one `direct` or `hold` transaction through the v2 API. Each leg names the `organizationId` and `ledgerId` of its account. Midaz splits the transfer into one balanced transaction per Ledger: 1. **Debit from the source Ledger:** The account in Ledger 1 sends the amount to the External Account of Ledger 1 (`@external/`). 2. **Credit to the destination Ledger:** The External Account of Ledger 2 sends the amount to the target account. The transactions share one `groupId`. In a `direct` transaction, they succeed or fail together. ## Key characteristics *** * All Accounts and Transactions exist within a Ledger. * 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. For example, a company 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. Every movement between Ledgers passes through the External Account of each Ledger (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, with 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/products/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/products/midaz/accounts) / [Account Type](/en/products/midaz/account-types)** | An individual balance and its rules | A single balance needs its own statement, nature, or restrictions. | 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. > [!NOTE] > > 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. ## 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 | Setting | Default | Description | | - | - | - | | `accounting.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. | | `accounting.validateRoutes` | `false` | When enabled, every transaction must include an associated transaction route. Each operation is also validated against its operation route rules. | | `accounting.requireHolder` | `false` | When enabled, each account that you create through the v2 API must name an existing holder. | | `overrides.allowFeeSkip` | `false` | When enabled, a transaction request can set `skip.fees` to `true` to post without the fee calculation. If the setting is disabled, Midaz refuses that request with error `0490`. | | `overrides.allowTracerSkip` | `false` | When enabled, a transaction request can set `skip.tracer` to `true` to skip the Tracer call. If the setting is disabled, Midaz refuses that request with error `0490`. | | `overrides.allowHolderSkip` | `false` | When enabled, an account creation request can skip the holder check. | | `tracer.mode`, `tracer.failPosture`, `tracer.timeoutMs` | `off`, `open`, `250` | Control how the Ledger calls Tracer. See the [Tracer integration guide](/en/products/tracer/integration-guide#midaz-ledger-reservation-seam). | | `crossLedger.enabled` | `false` | When enabled, the Ledger can take part in a transaction that spans Ledgers. | > [!TIP] > > Start with `validateAccountType` and `validateRoutes` 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. 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. > [!WARNING] > > If you enable validation on a Ledger that already processes transactions, existing records do not change. Validation applies to the requests that you submit after you enable the setting. This includes a commit, cancel, or revert of an earlier transaction. ### Managing settings via API Use the Ledger Settings endpoints to retrieve and update these configurations: * [Retrieve Ledger Settings](/en/reference/products/midaz/v2/get-ledger-settings): view the current settings for a Ledger. * [Update Ledger Settings](/en/reference/products/midaz/v2/update-ledger-settings): change one or more settings with a partial update (deep merge). > [!NOTE] > > 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/products/midaz/v2/list-ledgers) - View the details of all Ledgers. * [Retrieve a Ledger](/en/reference/products/midaz/v2/get-ledger-by-id) - Get details of a specific Ledger. * [Create a Ledger](/en/reference/products/midaz/v2/create-ledger) - Register a new Ledger. * [Update a Ledger](/en/reference/products/midaz/v2/update-ledger) - Edit the details of an existing Ledger. * [Delete a Ledger](/en/reference/products/midaz/v2/delete-ledger) - Mark a Ledger as deleted. > [!NOTE] > > When you delete a Ledger, Midaz does not remove it permanently. > > Instead, Midaz sets a `deletedAt` timestamp. Midaz then excludes the Ledger from every read, and you can no longer use it. Its `status.code` does not change. This preserves historical data for audit trails. Midaz rejects Ledger deletion in a production environment. ### 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/products/midaz/console/managing-ledgers-via-console) > [!TIP] > > 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/products/midaz/console/midaz-console-onboarding) guide. # Metadata indexes Source: https://docs.lerian.studio/en/products/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. ## Metadata filters on large collections *** 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. Indexes help 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, a query that filters by the metadata key can use 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/products/midaz/v2/create-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/products/midaz/v2/get-all-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/products/midaz/v2/delete-metadata-index) endpoint: ``` DELETE /v1/settings/metadata-indexes/entities/{entity_name}/key/{index_key} ``` Set `{index_key}` to the metadata key, such as `tier`, not to the index name. > [!CAUTION] > > 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. Keep the index count deliberate: list the indexes and delete any with low or zero `accesses`. > [!TIP] > > 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/products/midaz/v2/create-metadata-index): API reference. * [List Metadata Indexes](/en/reference/products/midaz/v2/get-all-metadata-indexes): API reference. * [Delete a Metadata Index](/en/reference/products/midaz/v2/delete-metadata-index): API reference. # Midaz for academic records Source: https://docs.lerian.studio/en/products/midaz/midaz-for-academic-record-management Use Midaz to record enrollments, grades, and achievements as immutable Ledger transactions, with 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 its accounts or amounts after you record it. * **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 the operations of a student's accounts for a complete 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 return grade totals for GPA calculations | | Audit preparation takes weeks of data gathering | Query transactions for any date range through the API | ## 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 academic metric, such as `ENROLLMENT`, `GRADE`, `ATTENDANCE`, and `ACHIEVEMENT`. Asset codes use uppercase letters only. * **Student Portfolios:** For each student, create a Portfolio. The Portfolio is the academic record and holds all relevant academic metrics. * **Accounts for academic metrics:** Create accounts for the academic metrics inside each student's Portfolio. Each account holds one Asset. 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. Record the course or activity in each transaction's metadata: * **Course enrollment**: When a student enrolls in a course, create a transaction. The transaction moves one `ENROLLMENT` unit from `@external/ENROLLMENT` to the student's "Course Enrollments" account. * **Grade assignment**: When an instructor assigns a grade, create a transaction. The transaction moves the grade, in `GRADE`, from `@external/GRADE` to the student's grade account. * **Attendance marking**: To mark attendance, move `ATTENDANCE` from `@external/ATTENDANCE` 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 operations of the Grades Account. Use the available balance to calculate the student's GPA. # Midaz for multi-account customers Source: https://docs.lerian.studio/en/products/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 show 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. ## Each balance as its own account *** For **product and operations teams**, each balance becomes its own account. The Ledger then enforces segregation rules, such as a blocked outflow or a benefit-only spend. 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. ![One customer, many accounts: reference architecture](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/flowchart-multiaccounts.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=4bc292432ee995aeabaa2061bfee73ff) Reference architecture: one customer, many accounts. > [!NOTE] > > When balance, ledger, statement, or operational rule differs by destination, point each identifier at a distinct account. Do not treat an external identifier as 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/products/midaz/crm/holders) | Identity behind the accounts (`NATURAL_PERSON` or `LEGAL_PERSON`), keyed by `document`. Owns each account through its `holderId`. Holds no balance. | | Each balance position | [**Account**](/en/products/midaz/accounts) | Source of truth for balance, entries, and statement. | | The nature of each balance | [**Account Type**](/en/products/midaz/account-types) | Classifies an account (main, benefit, blocked) and enables route validation. | | All accounts of one customer | [**Portfolio**](/en/products/midaz/portfolios) | Groups a customer's accounts to view the total relationship. | | External address → account | [**Account alias**](/en/products/midaz/accounts#account-aliases) + [**`entityId`**](/en/products/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/products/midaz/crm/crm-getting-started) | Attaches branch, account number, and regulatory fields. Links a Holder to a specific account. | > [!TIP] > > 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** (v4.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 new accounts must use a registered type (see Step 1) | | **CRM** | Part of the ledger binary. Use it to attach identity, banking, and regulatory context | ## 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. **1. Enable Account Type validation** Turn on validation so new accounts must declare a registered type. ```json theme={null} PATCH /v2/organizations/{org_id}/ledgers/{ledger_id}/settings { "accounting": { "validateAccountType": true } } ``` Settings take effect immediately. You do not need to redeploy. **2. Register the Account Types** Create one Account Type per balance nature. The `keyValue` is what each account's `type` field must match. ```json theme={null} POST /v2/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. The type alone restricts nothing. With route validation on, transaction routes can condition its outflows. To block them, set `allowSending` to `false` on its balance. **3. Register the customer as a Holder** Create one Holder for the customer. The same Holder owns all three accounts and keeps identity in one place. > [!NOTE] > > 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/products/midaz/crm/crm-getting-started) for the full schema. ```bash theme={null} curl -X POST http://localhost:3002/v2/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 `id`. It is the `holderId` for the next steps. **4. Create the three accounts** 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). The `holderId` makes the Holder the owner of the account. You cannot change the owner later. ```json theme={null} POST /v2/organizations/{org_id}/ledgers/{ledger_id}/accounts { "name": "Main account - 12345678900", "assetCode": "BRL", "alias": "@cust_12345678900_main", "entityId": "0001/12345-1", "holderId": "", "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`. Use the same `holderId` for both. > [!TIP] > > The `entityId` is where you store the external address that other systems use to reach this account. It is your map between Midaz and your source system. **5. Link each account to the Holder** Create one Instrument per ledger account to attach banking and regulatory context. The Instrument remains inside the unified Ledger binary while keeping customer-facing details outside the transactional account and balance model. ```bash theme={null} curl -X POST http://localhost:3002/v2/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" } }' ``` 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. You now have one customer, three accounts, and 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. **1. External event** A transaction, query, or settlement arrives with an external identifier. **2. Middleware resolves the 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). **3. Midaz posts to the right account** 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. | > [!TIP] > > 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. ## Outcomes of many accounts per customer *** * **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**: 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** (v4.x.x+) | Organization, Ledger, and a registered asset | | **Account Type validation** | Enabled per-Ledger via the [Ledger Settings API](/en/products/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** | A Holder per customer, plus an optional Instrument per ledger account | ## Next steps *** > **[Accounts](/en/products/midaz/accounts)** > > The core financial unit: aliases, `entityId`, and external accounts. > **[Account Types](/en/products/midaz/account-types)** > > Classify accounts and enforce their nature with route validation. > **[Portfolios](/en/products/midaz/portfolios)** > > Group a customer's accounts to view the total relationship. > **[CRM: Holders & Instruments](/en/products/midaz/crm/holders)** > > Centralize identity and attach banking and regulatory context. # Midaz for supply chains Source: https://docs.lerian.studio/en/products/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 asks where a material came from and where it went. 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. ## 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 | | 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 record every delivery and movement | | Regulatory audits require weeks of data preparation | Query transactions for any date range through the API | ## Using Midaz *** * **Assets**: Set up each supply chain item as an Asset. Asset codes use uppercase letters only, such as `STEEL` or `WIDGET`. Assets cover everything from raw materials to final products. * **Portfolios**: Create a portfolio for each warehouse. A portfolio groups the accounts that track material and goods flow. * **Accounts**: Within each portfolio, create an account for each item at each supply chain stage. Each account holds one Asset. 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 item's external account, such as `@external/STEEL`, to the warehouse's Raw Materials account. Record the supplier in the transaction metadata. * **Production**: During production, a transaction moves the amount from the Raw Materials account to the In-Production account. * **Production completion**: When production finishes, one transaction per consumed material moves it from its In-Production account to the material's external account. Another transaction moves the finished products from the product's external 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. # Getting started with Midaz Source: https://docs.lerian.studio/en/products/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.27.0+ | `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` | > [!NOTE] > > 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. > [!WARNING] > > Docker must run before this step. `make set-env` also generates the `LCRYPTO_*` CRM keys through a Docker one-shot container. The command 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 the containers are up. Check their status with: ```bash Terminal theme={null} docker compose -f components/infra/docker-compose.yml ps ``` > [!TIP] > > 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, runs the ledger and Tracer migration containers, and then starts the ledger and Tracer services. 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. > [!TIP] > > For the complete endpoint specification, see [Create an Organization](/en/reference/products/midaz/v2/create-organization). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v2/organizations \ -H "Content-Type: application/json" \ -d '{ "legalName": "Acme Corp", "legalDocument": "12345678000100", "status": { "code": "ACTIVE" }, "address": { "country": "BR" } }' ``` > [!NOTE] > > 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. > [!TIP] > > For the complete endpoint specification, see [Create a Ledger](/en/reference/products/midaz/v2/create-ledger). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v2/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. > [!TIP] > > For the complete endpoint specification, see [Create an Asset](/en/reference/products/midaz/v2/create-asset). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v2/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). > [!TIP] > > For the complete endpoint specification, see [Create an Account](/en/reference/products/midaz/v2/create-account). Midaz created the source account `@external/BRL` when you created the BRL asset. Create a destination account: ```bash Terminal theme={null} curl -X POST http://localhost:3002/v2/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 *** A transaction 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. > [!TIP] > > For the complete endpoint specification, see [Create a Transaction using the v2 direct model](/en/reference/products/midaz/v2/create-transaction-direct). ```bash Terminal theme={null} curl -X POST http://localhost:3002/v2/transactions/direct \ -H "Content-Type: application/json" \ -d '{ "description": "First transaction", "asset": "BRL", "amount": "1000", "debits": [ { "alias": "@external/BRL", "organizationId": "{organization_id}", "ledgerId": "{ledger_id}", "amount": "1000" } ], "credits": [ { "alias": "@customer-001", "organizationId": "{organization_id}", "ledgerId": "{ledger_id}", "amount": "1000" } ] }' ``` > [!NOTE] > > This transaction sends `"1000"` from `@external/BRL` to `@customer-001`. Midaz stores monetary values as decimal strings and does not apply an asset-specific scale. > > Your integration defines the display and rounding rules for BRL amounts. Do not parse monetary values as binary floating point. ## Step 10: Verify the balance *** Check the destination account balance to confirm the transaction. > [!TIP] > > For the complete endpoint specification, see [Retrieve a Balance by Account Alias](/en/reference/products/midaz/v2/get-balances-by-alias). ```bash Terminal theme={null} curl http://localhost:3002/v2/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 run `make restart`. Then access: * **API docs**: `http://localhost:3002/docs` * **OpenAPI spec**: `http://localhost:3002/openapi.json` (or `/openapi.yaml`) ## Observability *** Midaz ships with a preconfigured Grafana instance integrated with OpenTelemetry. * **Grafana dashboard**: `http://localhost:3100` * **Default credentials**: `midaz` / `lerian` The local ledger configuration ships with telemetry off. To send telemetry to Grafana, set `ENABLE_TELEMETRY=true` in `components/ledger/.env` and run `make restart`. ## Stopping Midaz *** To stop all services: ```bash Terminal theme={null} make down ``` To remove the Midaz containers and volumes, run the command below. It also runs `docker system prune -f` and `docker volume prune -f`, which affect every project on your machine: ```bash Terminal theme={null} make clean-docker ``` ## Next steps *** > [!TIP] > > New to Midaz? Start with [Midaz entities](/en/products/midaz/core-entities) to understand organizations, ledgers, accounts, and transactions. > **[Creating transactions](/en/products/midaz/transactions-overview)** > > Learn the different ways to create transactions, including JSON, inflow, and outflow, and when to use each. > **[Deploy to production](https://github.com/LerianStudio/helm/blob/main/charts/midaz/README.md#install-midaz-helm-chart)** > > Deploy Midaz to Kubernetes using the official Helm chart. > **[Setting up CRM](/en/products/midaz/crm/crm-getting-started)** > > Manage holders and alias accounts to connect real-world identities to your Midaz accounts. > **[Extend with plugins](/en/products/about-plugins)** > > Add Pix and other capabilities to your Midaz deployment. # Midaz in a global portfolio Source: https://docs.lerian.studio/en/products/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 (A unit of value tracked in the ledger: traditional currencies (BRL, USD), digital currencies (BTC), or custom units like loyalty points. Every account is linked to exactly one asset.); accounts in different currencies grouped under one Portfolio (A grouping of accounts belonging to the same account holder, business unit, or purpose. It ties together checking, savings, and investment accounts for a unified view.) | | 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 | Filter the account list by portfolio through the API | | 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. # Midaz in banking Source: https://docs.lerian.studio/en/products/midaz/midaz-in-banking Model retail, corporate, and personalized banking in Midaz with Portfolios, Segments, and dedicated settlement Accounts. Midaz supports three banking models: **retail banking**, **corporate banking**, and **personalized banking services**. It uses portfolios (A grouping of accounts belonging to the same account holder, business unit, or purpose. It ties together checking, savings, and investment accounts for a unified view.) and segments (A classification applied to accounts that share characteristics, like account holder tiers (VIP, Student) or regional divisions. Rules and pricing can differ by segment.) to model each one. ## What changes with Midaz *** | Without Midaz | With Midaz | | :- | :- | | Customer accounts scattered across legacy systems with no unified view | Single ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.) with portfolios grouping all accounts per customer | | Adding new products (savings, credit) requires core system changes | New account types and segments configured via API, with no code changes to the ledger | | Fee and interest tracking mixed with operational accounts | Dedicated internal accounts for fee income, interest expense, and settlement (The process of finalizing a financial transaction by transferring actual funds between institutions. Settlement accounts act as clearing points where money is held temporarily before reaching its final destination.) | | 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 (A sub-account under a parent account, used for budgeting or organizational purposes. Each tracks its own balance.) and multi-portfolio support for subsidiaries | ## Adding products without changing the core ledger *** 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 (A unit of value tracked in the ledger: traditional currencies (BRL, USD), digital currencies (BTC), or custom units like loyalty points. Every account is linked to exactly one asset.):** 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 (A sub-account under a parent account, used for budgeting or organizational purposes. Each tracks its own balance.) for budgeting:** Customers create sub-accounts under their savings account for better money management. #### Example: ATM withdrawal John Doe withdraws \$100 from an ATM: **1. Debit** \$100 from John’s checking account. **2. Credit** \$100 to an "ATM Cash Dispensed" internal account. **3. Fee** Midaz debits \$3 from John’s account and credits the Fee Income account. ## 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 under one parent account. * **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. Credit** \$1,000,000 to ACME’s loan account. **2. Debit** \$1,000,000 from the bank’s Loans Receivable account. **3. ACME uses funds** * \$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). ## 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 filters the account list by portfolio. * **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 **Step 1** A new customer signs up as a student. **Step 2** You assign their account to the "Student" segment. **Step 3** The Fees Engine waives their monthly fees. **Step 4** When the customer reaches a set age, you move the account to a different segment. # Midaz models Source: https://docs.lerian.studio/en/products/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/start-here/evaluate-and-deploy/deployment-models) model. You deploy and operate the platform in your own infrastructure. Enterprise institutions 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/start-here/evaluate-and-deploy/deployment-models). ## Community model *** The **Community** model gives you full access to Midaz's core features at no cost, with community support. ### What the Community model includes * Free access to [Midaz's core features](/en/products/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: ### What the Enterprise model adds * **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. > [!NOTE] > > Plugins are available only to Enterprise clients. > > If you want access on another plan, [get in touch](https://lerian.studio/contact). ## Choosing the right model *** ### 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/products/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 one primary with replicas across multiple zones. Midaz does not support Redis Cluster mode for transactions. * Enable automatic failover with a Sentinel topology via `REDIS_MASTER_NAME`, or with a managed service with cluster mode disabled. Set `REDIS_HOST` to one address, or set `REDIS_MASTER_NAME` for Sentinel. Without `REDIS_MASTER_NAME`, a comma-separated `REDIS_HOST` selects Cluster mode. A managed service (ElastiCache, Memorystore) is the safer default. ### MongoDB * Use replica sets with members across zones. * Monitor role transitions and lag. * Schedule regular backups. * Use managed services such as MongoDB Atlas or AWS DocumentDB. ## Messaging infrastructure *** Midaz runs two messaging surfaces. Kafka-compatible streaming and asynchronous transaction processing are off by default. The legacy RabbitMQ overdraft and audit publishers are enabled at runtime unless you explicitly set their flags to `false`. The bundled example configuration sets those flags to `false`: * **RabbitMQ** carries the optional write-behind pipeline that stores confirmed transactions in PostgreSQL and MongoDB (`RABBITMQ_TRANSACTION_BALANCE_OPERATION_*`, enabled with `RABBITMQ_TRANSACTION_ASYNC=true`) plus legacy outbound overdraft and audit exchanges. Use a managed RabbitMQ service such as AWS MQ or CloudAMQP in production. * **RedPanda** is the event backbone. Set `STREAMING_ENABLED=true`, `STREAMING_BROKERS`, and `STREAMING_CLOUDEVENTS_SOURCE` set to exactly `ledger` for ledger, CRM, and Fees, or `tracer` for Tracer. Facts publish to `lerian.streaming.ledger` or `lerian.streaming.tracer`. Route by CloudEvents headers. Transaction-lifecycle facts publish through this surface. Optional PostgreSQL read replicas (`DB_ONBOARDING_REPLICA_*` and `DB_TRANSACTION_REPLICA_*`) serve reads. Transactional reads go to the primary by default. ## 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. > [!NOTE] > > Several 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). ## Aligning infrastructure to the Midaz architecture *** 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. ## What’s next? *** * Read the [Midaz deployment guide](/en/products/midaz/deployment). * [Contact our team](https://lerian.studio/contact) for tailored support. # Midaz SDK for Go Source: https://docs.lerian.studio/en/products/midaz/midaz-sdk-go Build Go applications on top of Midaz with the official Go SDK: typed pagination, structured errors, retries, and OpenTelemetry observability. The **Midaz SDK for Go** is the Go client for the Midaz ledger and Tracer APIs. It gives you typed access to each service (Organizations, Ledgers, Accounts, Transactions, and more) with one surface for authentication, pagination, errors, retries, logging, and observability. ## Getting started *** ### Step 1 – Install Go Install Go before you use the SDK. The SDK declares **Go 1.26** in `go.mod`. Its public API uses `iter.Seq2` and `log/slog`. **Step 1** Go to the [official Go website](https://golang.org/dl/). **Step 2** Download the installer for your OS (Windows, macOS, or Linux). **Step 3** [**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 add the SDK to your `go.mod` and `go.sum` files: > [!WARNING] > > **The module path requires the `/v6` suffix.** If you omit it, Go resolves an old v1 release. Import `github.com/LerianStudio/midaz-sdk-golang/v6`. ```bash Bash theme={null} go get github.com/LerianStudio/midaz-sdk-golang/v6 ``` > [!TIP] > > In 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 builds a client against your local Midaz stack with anonymous authentication, lists organizations, then creates a new one. > [!NOTE] > > 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/start-here/getting-started) to spin one up before running the snippet. ```go Go expandable theme={null} package main import ( "context" "fmt" "log" midaz "github.com/LerianStudio/midaz-sdk-golang/v6" "github.com/LerianStudio/midaz-sdk-golang/v6/models" ) func main() { // Build a client. The SDK 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.V2.Organizations.List(ctx, models.OrganizationsListOpts{ PageListOpts: models.PageListOpts{Limit: 5}, }) if err != nil { log.Fatalf("list organizations: %v", err) } for _, org := range page.Items { fmt.Printf("- %s (%s)\n", org.LegalName, org.ID) } // Create a new organization. midaz.CreateOrganizationInput is the same // type as models.CreateOrganizationInput, re-exported on the midaz package. dba := "Example Inc." org, err := c.V2.Organizations.Create(ctx, &midaz.CreateOrganizationInput{ LegalName: "Example Corporation", LegalDocument: "123456789", DoingBusinessAs: &dba, // optional fields are *string Address: midaz.Address{ Line1: "123 Main St", City: "New York", State: "NY", ZipCode: "10001", Country: "US", }, }) if err != nil { log.Fatalf("create organization: %v", err) } fmt.Printf("Organization created: %s\n", org.ID) } ``` This gives you access to: * The Midaz client for calling each API service. * Built-in data models (like `CreateOrganizationInput`). * Auth via **Access Manager** (production) or **Anonymous** (local development). * A typed configuration system that fails at construction time. > [!TIP] > > For the full setup, see the [Authentication](#authentication) section. ### Step 5 – Run the project Run the following command: ```bash Bash theme={null} go run main.go ``` ## SDK architecture *** #### Layered design | Layer | What it handles | | :- | :- | | **Client** | The main entry point. `midaz.New(...)` wires authentication, retries, and observability. | | **Services** | Access to each Midaz domain, grouped by the API version that serves it. | | **Models** | The data structures of the Midaz domain. The common ones are re-exported on the `midaz` package. | | **Utility packages** | Helpers for config, errors, observability, retries, idempotency, and more. | ### Services Midaz serves two ledger API versions: `/v1`, which is deprecated, and `/v2`. The SDK groups the ledger services by the version that serves them. **Build against `c.V2`.** A resource that one version does not serve is a compile error, not a 404. ```go Go theme={null} orgs, err := c.V2.Organizations.List(ctx, opts) account, err := c.V2.Accounts.Get(ctx, orgID, ledgerID, accountID) tx, err := c.V2.Transactions.CreateDirect(ctx, orgID, ledgerID, input) // /v1 only: rate, err := c.V1.AssetRates.GetAssetRate(ctx, orgID, ledgerID, externalID) ``` | Group | Services | | :- | :- | | `c.V2` and `c.V1` | `Organizations`, `Ledgers`, `Accounts`, `AccountTypes`, `Assets`, `Balances`, `Operations`, `Portfolios`, `Segments`, `OperationRoutes`, `TransactionRoutes`, `Transactions`, `MetadataIndexes` | | `c.V2` only | `Holders`, `Instruments`, `Encryption`, `Composition`, `ProtectionAudit`, `BillingPackages`, `FeePackages`, `FeeEstimates`, `BillingCalculations` | | `c.V1` only | `AssetRates` | | Tracer, on the client | `c.Rules`, `c.Limits`, `c.Validations`, `c.Reservations`, `c.AuditEvents` | `Transactions` has a different contract on each version. On `/v1`, you create a transaction through one of four creation endpoints with a nested `send` body. On `/v2`, `c.V2.Transactions` creates transactions with a flat body through `CreateDirect`, `CreateHold`, `CreateBlock`, and `CreateUnblock`. It returns `models.TransactionV2`. > [!NOTE] > > A `201` response means that Midaz applied the transaction to balances. ### Models Each model type represents a business concept. You use them in each service call. The most common model types are re-exported on the `midaz` package. `midaz.Account` and `models.Account` are the same type, so most code needs only 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. | > [!TIP] > > For a type that is not re-exported, such as `models.TransactionV2`, import `github.com/LerianStudio/midaz-sdk-golang/v6/models` directly. The `midaz` aliases keep the type identity, so the two import paths work together. ### Utility packages The `pkg` folder of the SDK holds utility packages. **Core** packages serve concerns that apply to the whole SDK. **Helper** packages give domain-specific or low-level utilities. #### Core packages | Package | What it solves | | :- | :- | | `auth` | Access Manager OAuth and the token lifecycle. | | `config` | Config handling, environment loading, and custom service URLs. | | `concurrent` | Tools for batching, rate-limiting, and worker pools. | | `errors` | Structured error types, classifiers, and the `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, such as idempotency keys. | | `validation` | Input validation with 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 helpers, such as the settlement wait. | | `utils` | General-purpose helpers used across the SDK. | | `version` | SDK version metadata and identification. | ## Authentication *** The SDK requires exactly one authentication source at construction time. If you call `midaz.New(...)` with neither, it returns a typed configuration error. You have two choices: * **`midaz.WithAccessManager(...)`**: OAuth through the Lerian Access Manager. Use it for any non-local stack. * **`midaz.WithAnonymous()`**: no authentication. Use it only for a local Midaz stack with auth disabled. If you apply both options, the last one wins. ### Production: Access Manager Put your Access Manager credentials in `midaz.WithAccessManager`. The SDK requests a token from your Access Manager, attaches it to each API call, and refreshes it when it expires. > [!WARNING] > > 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" midaz "github.com/LerianStudio/midaz-sdk-golang/v6" ) func main() { // Configure Access Manager. Load ClientID and ClientSecret from // environment variables; do not hardcode them. c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentProduction), midaz.WithAccessManager(midaz.AccessManager{ Address: "https://auth.example.com", 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.V2.Organizations, c.V2.Ledgers, c.V2.Accounts, ... } ``` ### 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 with 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 ``` > [!NOTE] > > `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/v6/pkg/config" cfg, err := config.NewConfig(config.FromEnvironment()) if err != nil { log.Fatalf("config: %v", err) } c, err := midaz.New(midaz.WithConfig(cfg)) ``` > [!NOTE] > > Environment loading is **explicit**. Put `config.FromEnvironment()` in the option chain. The SDK does not read environment variables during construction without it. > [!TIP] > > For the full auth walkthrough, see the [Authentication guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/auth.md) in the SDK repo. ## Tenant scope *** **Tenant scope comes from the Access Manager / JWT claims** used to obtain the token. The SDK has no tenant configuration and does not send a tenant header. 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 *** Most list endpoints come in three methods. Pick the one that matches your use case. | Method | Returns | Use when | | :- | :- | :- | | `List` | `*models.ListResponse[T]` (one page) | You want exactly one page and decide when to advance. | | `All` | `iter.Seq2[T, error]` | You want each item. The SDK handles paging internally. | | `Pages` | `iter.Seq2[*models.ListResponse[T], error]` | You need page-level metadata for checkpointing or batching. | ### Typed list options Each list method takes a typed opts struct. The struct embeds one of two base structs, based on how the endpoint paginates: | Pagination shape | Endpoints | Base struct | | :- | :- | :- | | **Page-based** | Organizations, Ledgers, Assets, Portfolios, Segments, Accounts, AccountTypes, FeePackages, BillingPackages | `models.PageListOpts{Limit, Page, SortDirection, StartDate, EndDate}` | | **Cursor-based** | Transactions, Balances, Holders, Operations, OperationRoutes, TransactionRoutes, AssetRates | `models.CursorListOpts{Limit, Cursor, SortDirection, StartDate, EndDate}` | Some endpoints also expose a typed `Filters` sub-struct. If you set a field of the wrong shape (for example, `Page` on a cursor endpoint), the code does not compile. > [!WARNING] > > The transaction list does not filter by `Status`, `AssetCode`, `Reference`, `SourceAccount`, `DestinationAccount`, or `Route`. If you set one of these filters on `List`, the SDK refuses the call with an error. `Count` accepts `Status` and `Route`. ### Iterate every item with `All` The simplest shape is a `range` loop over each item across each page. The SDK fetches the pages internally. ```go Go theme={null} opts := models.AccountsListOpts{ PageListOpts: models.PageListOpts{Limit: 100}, Filters: models.AccountsFilters{ Status: "ACTIVE", AssetCode: "USD", }, } for account, err := range c.V2.Accounts.All(ctx, orgID, ledgerID, opts) { if err != nil { return fmt.Errorf("list accounts: %w", err) } process(account) } ``` ### Iterate page envelopes with `Pages` When you need page-level metadata (for checkpointing, batching, or stopping mid-page), iterate over `Pages`. Each entry is a `*models.ListResponse[T]` with its `Pagination` block. ```go Go theme={null} opts := models.TransactionsListOpts{ CursorListOpts: models.CursorListOpts{Limit: 50}, } for page, err := range c.V2.Transactions.Pages(ctx, orgID, ledgerID, opts) { if err != nil { return fmt.Errorf("page iter: %w", err) } log.Printf("items=%d next_cursor=%q", len(page.Items), page.Pagination.NextCursor) for _, tx := range page.Items { process(tx) } if shouldStop(page) { break // The SDK stops paging. } } ``` See the [Pagination guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/pagination.md) in the SDK repo for `NextCursor` handling and the page-vs-cursor decision table. ## Error handling *** The SDK service layer returns errors as `*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 for branching or telemetry. | | `Operation` | The SDK operation that produced the error. | | `Resource` | The resource family the error refers to, when relevant. | You can branch with typed predicates, read fields with `errors.As`, or use the `Retryable()` method to drive retry policy. ### Branch with typed predicates The SDK has a set of `Is*` predicates, so you can match errors without reading internals: ```go Go expandable theme={null} import ( "errors" "fmt" sdkerrors "github.com/LerianStudio/midaz-sdk-golang/v6/pkg/errors" ) acc, err := c.V2.Accounts.Get(ctx, orgID, ledgerID, accountID) if err != nil { switch { case sdkerrors.IsNotFoundError(err): return fmt.Errorf("account not found: %w", err) case sdkerrors.IsAuthError(err): 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) } // Read 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 derives from `Category`. Use it instead of 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 call 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/v6/pkg/errors" resp, err := httpClient.Do(req) if err != nil { return sdkerrors.ClassifyTransportError("PaymentService.Charge", err) } ``` ### Local validation: FieldErrors Local input validation returns `*pkg/validation.FieldErrors`, a collection of per-field errors that the SDK finds before any HTTP call. Use `errors.As` to read them. ```go Go theme={null} import ( "errors" "fmt" "github.com/LerianStudio/midaz-sdk-golang/v6/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) } } ``` > [!TIP] > > For the full category map, each code, and retry boundaries, see the [Error handling guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/errors.md) in the SDK repo. ## Retries *** The default policy retries 3 times with exponential backoff and 25% jitter. It retries transport errors and the statuses `408`, `425`, `429`, `500`, `502`, `503`, and `504`. The SDK retries `POST`, `PUT`, `PATCH`, and `DELETE` only when the request has an `X-Idempotency` header. ```go Go theme={null} import ( "time" "github.com/LerianStudio/midaz-sdk-golang/v6/pkg/retry" ) c, err := midaz.New( midaz.WithEnvironment(midaz.EnvironmentLocal), midaz.WithAnonymous(), midaz.WithRetryOptions( retry.WithMaxRetries(5), retry.WithInitialDelay(200*time.Millisecond), ), ) ``` Use `midaz.WithCustomRetryPolicy` for your own retry predicate. To turn retries off, use `midaz.WithoutRetries()`. ## Logging *** **`*slog.Logger` is the logger surface of the SDK.** Wire it with `midaz.WithLogger(...)`. The SDK is **silent by default**: it uses `slog.DiscardHandler` until you opt in. You decide the handler, the level, and the destination. ```go Go expandable theme={null} package main import ( "context" "log" "log/slog" "os" midaz "github.com/LerianStudio/midaz-sdk-golang/v6" ) 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 log lines now go to your handler. } ``` charmbracelet/log, zap, and zerolog integrate through `slog.Handler`. > [!TIP] > > The [Logging guide](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/logging.md) has adapter recipes for common logging libraries. ## Observability *** One observability provider gives you spans, metrics, and OpenTelemetry-correlated logs through one wiring point. Pass a provider that you build, or pass options that the SDK assembles for you. ### Wire a provider ```go Go expandable theme={null} package main import ( "context" "log" midaz "github.com/LerianStudio/midaz-sdk-golang/v6" "github.com/LerianStudio/midaz-sdk-golang/v6/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/v6/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 W3C `traceparent` propagation. Business log records carry safe IDs only: no payloads, names, addresses, or auth headers. You can also wrap a block of business logic in a span with `Client.Trace`: ```go Go theme={null} err = c.Trace("create-organization", func(ctx context.Context) error { _, err := c.V2.Organizations.Create(ctx, input) return err }) ``` > [!WARNING] > > `WithObservabilityOptions` and `WithObservabilityProvider` **replace** the installed provider. They do not merge. To start from a baseline, put `observability.WithDevelopmentDefaults` or `observability.WithProductionDefaults` first in the option chain. > [!TIP] > > 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**. The SDK sends an `X-Idempotency: ` header on each unsafe HTTP request (POST, PUT, PATCH, DELETE). A retry after a transient failure then does not create a resource twice. You can override the generated key per call when you need a stable key of your own. This is typical for saga steps, outbox rows, or UI submissions. ### Set a stable key per request ```go Go theme={null} import "github.com/LerianStudio/midaz-sdk-golang/v6/pkg/sdkctx" ctx := sdkctx.WithIdempotencyKey(context.Background(), "tx-2026-05-06-001") tx, err := c.V2.Transactions.CreateDirect(ctx, orgID, ledgerID, input) ``` ### Suppress idempotency for one call For rare administrative calls, suppress the header per request: ```go Go theme={null} ctx := sdkctx.WithoutAutoIdempotency(context.Background()) ``` Without the header, the SDK does not retry that unsafe request. ### Disable globally To turn auto-idempotency off everywhere, 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 set `MIDAZ_IDEMPOTENCY=false` when you use `config.FromEnvironment()`. ## Environment variables *** The SDK reads these variables only when `config.FromEnvironment()` is in your config option chain. | Variable | Description | | :- | :- | | `MIDAZ_ENVIRONMENT` | Target environment (`local`, `development`, `production`). | | `MIDAZ_BASE_URL` | Base URL for the Midaz services, used when the service-specific URLs are not set. Do not add a `/v1` or `/v2` suffix. | | `MIDAZ_LEDGER_URL` | Ledger API URL. It serves the onboarding and transaction endpoints. | | `MIDAZ_TRACER_URL` | Tracer API URL. | | `MIDAZ_TRACER_API_KEY` | Optional API key for Tracer. When you do not set it, Tracer calls use the ledger bearer token. | | `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. | | `MIDAZ_IDEMPOTENCY` | Enable automatic idempotency keys for unsafe requests (`true` or `false`). | | `MIDAZ_ERROR_EXPOSE_BODY` | Include the raw response body in `*pkg/errors.Error`. | | `MIDAZ_ACCESS_MANAGER_ALLOW_INSECURE_HTTP` | Allow plain HTTP to the Access Manager. The SDK rejects it when `MIDAZ_ENVIRONMENT=production`. | | `MIDAZ_ALLOW_INSECURE_HTTP` | Allow plain HTTP to the ledger and Tracer URLs on non-loopback hosts. The SDK rejects it when `MIDAZ_ENVIRONMENT=production`. | 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 *** > [!TIP] > > The SDK has a numbered set of focused tutorials (examples 01–10), plus advanced and reference examples. Each numbered example teaches one concept. 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. | | `02-auth` | Access Manager authentication (production setup). | | `03-end-to-end` | Creating a transaction on `/v2` with `CreateDirect`. | | `04-listing-cursor` | Cursor-based pagination with `iter.Seq2`. | | `05-listing-pages` | Page-based pagination with `List`, `All`, and `Pages`. | | `06-idempotency` | Auto, explicit, and suppressed idempotency modes. | | `07-retries` | Default policy, custom policy, disabled retries. | | `08-logging-slog` | `*slog.Logger` integration. | | `09-testing-with-mocks` | A narrow interface of your own plus a hand-written mock. | | `10-observability-otel` | Full OpenTelemetry surface (tracing + metrics + logs). | The repository also includes reference examples: `concurrency`, `configuration`, `context`, `tracing`, `tracing-server`, `pkg-validation-demo`, `mass-demo-generator`, and `workflow-with-entities`. ## Explore the APIs *** For more information about the APIs, refer to the following links: > **[External API mapping](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/mapping/external_apis.md)** > **[Internal API mapping](https://github.com/LerianStudio/midaz-sdk-golang/blob/main/docs/mapping/internal_apis.md)** > **[Godoc documentation](https://pkg.go.dev/github.com/LerianStudio/midaz-sdk-golang/v6)** # Midaz SDK for TypeScript Source: https://docs.lerian.studio/en/products/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 interface over the Midaz financial services platform. The SDK works with Organizations, Ledgers, Accounts, Transactions, and more. ### 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. * **Error handling**: Clear error signals. * **Observability included**: Tracing, metrics, and logs, off by default. * **Layered architecture**: Clean separation between client, entities, API, and models. * **Automatic retries**: Retries for transient failures. * **Concurrency controls**: Built-in tools to run tasks in parallel with controlled throughput. * **Strict validation**: Catch invalid input early with clear error messages. ## Getting started *** ### Prerequisite * The Midaz SDK for TypeScript **requires** Node.js **v18.18 to v24** and 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().withBaseUrls({ onboarding: 'http://localhost:3002', transaction: 'http://localhost:3002', }) ); ``` > [!TIP] > > We offer an [Access Manager plugin](/en/platform/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 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' ); ``` The `withEnvironment()` presets use example URLs. Set your Midaz address with `withBaseUrls()` or the `MIDAZ_ONBOARDING_URL` and `MIDAZ_TRANSACTION_URL` environment variables. Both URLs point to your Midaz ledger. ### 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 `code` 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', '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`, `assetCode`, and account `type` to the builder `const accountInput = createAccountBuilder('Savings Account', 'USD', 'savings')`. Then you add any other properties with `with*` methods. ### Create a Transaction Create transactions with a `CreateTransactionInput` object and `createTransaction`. **Example:** ```typescript TypeScript expandable theme={null} import type { CreateTransactionInput } from '@lerianstudio/midaz-sdk'; const transactionInput: CreateTransactionInput = { code: 'payment_001', chartOfAccountsGroupName: 'PAYMENTS', description: 'Monthly payment', metadata: { purpose: 'Monthly payment' }, send: { asset: 'USD', value: '100.00', source: { from: [{ account: 'source-account-alias', amount: { asset: 'USD', value: '100.00' } }], }, distribute: { to: [{ account: 'destination-account-alias', amount: { asset: 'USD', value: '100.00' } }], }, }, }; const transaction = await client.entities.transactions.createTransaction('org_123', 'ledger_456', transactionInput); ``` In this code, `send` holds the amount. `source.from` lists the accounts that send it, and `distribute.to` lists the accounts that receive it. Each `account` is an account alias. Amounts are decimal strings. ### Clean up resources ```typescript TypeScript theme={null} await client.destroy(); ``` ### 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: '/v1/login/oauth/access_token', // Optional, this is the default refreshThresholdSeconds: 300, // Optional, defaults to 300 (5 minutes) }) .withEnvironment('sandbox') .withApiVersion('v1') ); // The SDK handles token acquisition and renewal // You can now use the client as normal const organizations = await client.entities.organizations.listOrganizations(); // Clean up resources when done await client.destroy(); ``` ## SDK architecture *** The Midaz SDK uses a multi-layered service architecture. It has three layers, shown in *Figure 1*. * **Client interface**: This is the main entry point for SDK users. It manages configuration such as authentication and environments. It gives access to the entity services through `client.entities`. * **Entity services layer**: This layer holds domain-specific services, such as Accounts, Assets, and Transactions. Most services offer create, get, update, delete, and list methods. Some services also add specialized operations for their entity. * **Core services layer**: All entity services use these foundational utilities. They handle HTTP requests, input validation, error processing, observability, and configuration. ![Layered architecture of the Midaz SDK for TypeScript, with the client interface over the entity services layer over the shared core services layer](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/sdk-typescript-architecture.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=071bbf4a7b5211949f85b0a406301a77) Figure 1. The layered architecture of the Midaz SDK for TypeScript. 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 and integration testing. > [!TIP] > > For more information about the architecture, see the following pages: > > * [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. * Hide internal complexity for better readability. ### 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. > [!TIP] > > For more information, see the [Builder Pattern in Midaz SDK](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/core-concepts/builder-pattern.md) page. ## Working with entities *** Each entity service covers a distinct part of the financial domain, such as accounts, assets, or transactions. These services read and write data for each type of entity. They also offer specialized features for each use case. | 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) | Group accounts within a Ledger. | | [**Portfolios**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/portfolios.md) | Group accounts into collections. | | [**Balances**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/balances.md) | Retrieve and update account balances. | | **Transactions** | 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. | | [**Asset Rates**](https://github.com/LerianStudio/midaz-sdk-typescript/blob/main/docs/entities/asset-rates.md) | Retrieve, create, and update exchange rates between assets. | | **Account Types** | Create, retrieve, update, and delete account types within a Ledger. | | **Transaction Routes** | Create, retrieve, update, and delete transaction routes within a Ledger. | | **Operation Routes** | Create, retrieve, update, and delete operation routes within a Ledger. | You access each service through the SDK client. They follow a consistent structure, so you build and maintain financial features more easily. > [!TIP] > > For more information, see the [Entities pages](https://github.com/LerianStudio/midaz-sdk-typescript/tree/main/docs/entities). ## Using utilities *** The SDK provides utility modules for common operations: performance, error handling, configuration, and observability. | 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. | | [**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** | Offers 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. | > [!TIP] > > For more information, see the [Utilities pages](https://github.com/LerianStudio/midaz-sdk-typescript/tree/main/docs/utilities). ## Error handling *** The Midaz SDK for TypeScript helps you handle errors clearly and consistently. When an SDK operation fails, the SDK throws an error. An API error response throws an `HttpError` with these fields: * `status`: The HTTP status code. * `statusText`: The HTTP status text. * `response`: The error body from Midaz. Invalid input throws a `ValidationError` before the request. Use `processError()` to read both types in one shape. Handle an error like this: ```typescript TypeScript theme={null} import { processError } from '@lerianstudio/midaz-sdk'; try { await client.entities.transactions.createTransaction('org_123', 'ledger_456', transactionInput); } catch (err) { const info = processError(err); console.error(`Error (${info.statusCode}): ${info.message}`); } ``` ### Best practices * **Validate input** before you call SDK methods. * **Check your auth** when you get status `401` or `403`. * **Retry** on transient issues like status `500` or `503`. * **Use `statusCode` and `message`** to show debug info in development logs. ## 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). ## 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/products/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. A posted transaction has at least one debit and one credit operation. A pending transaction records only its source side until you commit it. * 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 contains 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. ## 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. > [!NOTE] > > Every operation has a **`direction`** (`debit` or `credit`) and a **`type`**. The `direction` is the debit or credit side 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/products/midaz/transactions#blocking-and-unblocking-funds). The label overrides only the operation `type`, never its direction or amount. > * `FEE_SETTLEMENT` and `FEE_REFUND`: the settlement of a [fee debt](/en/products/midaz/fees/fee-debt), and its refund on a revert. > > 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/products/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, destination, or bidirectional) and a descriptive annotation. It is a validation layer between transaction input and ledger structure. Each operation you submit must meet the account requirements and business rules. > [!NOTE] > > 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. > [!CAUTION] > > 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 and update its description and metadata. * [List Operations by Account](/en/reference/products/midaz/v2/get-all-operations-by-account): View all Operations linked to a specific Account. * [Retrieve an Operation by Account](/en/reference/products/midaz/v2/get-operation-by-account): Get details of a specific Operation for that Account. * [Update an Operation](/en/reference/products/midaz/v2/update-operation): Change the description and metadata of an Operation. # Organizations Source: https://docs.lerian.studio/en/products/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. > [!NOTE] > > **On Lerian Cloud**: Midaz scopes every Organization to your tenant automatically. When you list Organizations, you see only the ones in your account. 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/products/midaz/ledgers) and stays operationally independent. The parent-child link records how the entities relate. ### Organization structure * **Organization > Ledgers:** An Organization can own several 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. > [!NOTE] > > 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. > [!TIP] > > 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/products/midaz/v2/create-organization): Register a new Organization. * [List Organizations](/en/reference/products/midaz/v2/list-organizations): View all Organizations in your workspace. * [Retrieve an Organization](/en/reference/products/midaz/v2/get-organization-by-id): Get details of a specific Organization. * [Update an Organization](/en/reference/products/midaz/v2/update-organization): Edit an existing Organization. * [Delete an Organization](/en/reference/products/midaz/v2/delete-organization): Mark an Organization as deleted. > [!NOTE] > > When you delete an Organization, Midaz does not remove it from the system. > > Midaz sets a `deletedAt` timestamp instead. Midaz then excludes the Organization from every read, and you can no longer use it. Midaz leaves its `status.code` unchanged, which preserves historical data for audit trails. Midaz rejects Organization deletion in a production environment. ### 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/products/midaz/console/managing-organizations) > [!TIP] > > 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/products/midaz/console/midaz-console-onboarding) guide. # Portfolios Source: https://docs.lerian.studio/en/products/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 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/products/midaz/v2/list-portfolios): View all Portfolios in a Ledger. * [Retrieve a Portfolio](/en/reference/products/midaz/v2/get-portfolio-by-id): Get details of a specific Portfolio. * [Create a Portfolio](/en/reference/products/midaz/v2/create-portfolio): Register a new Portfolio. * [Update a Portfolio](/en/reference/products/midaz/v2/update-portfolio): Edit an existing Portfolio. * [Delete a Portfolio](/en/reference/products/midaz/v2/delete-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/products/midaz/console/managing-portfolios) # Receiving Midaz events Source: https://docs.lerian.studio/en/products/midaz/receiving-midaz-events Consume Midaz events from Kafka, understand the legacy RabbitMQ surface, and check the current Streaming Hub compatibility boundary. Midaz emits CloudEvents for ledger, CRM, Fees, and Tracer business facts. The public integration contract is the **Kafka** surface. Legacy RabbitMQ exchanges remain outside the public catalog. ## Current event surface: Kafka *** Enable publication on each producing service: ```env theme={null} STREAMING_ENABLED=true STREAMING_BROKERS= STREAMING_CLOUDEVENTS_SOURCE=ledger ``` Set `STREAMING_CLOUDEVENTS_SOURCE` when you enable streaming. Use `ledger` for the ledger, CRM, and Fees binary. Use `tracer` for Tracer. Each service rejects any other value at startup. Midaz uses one v3 application fact topic per producing service: * Ledger, CRM, and Fees: `lerian.streaming.ledger` * Tracer: `lerian.streaming.tracer` Examples: * `organization.created` rides `lerian.streaming.ledger` * `transaction.posted` rides `lerian.streaming.ledger` * `fee_charge.applied` rides `lerian.streaming.ledger` * `rule.created` rides `lerian.streaming.tracer` The CloudEvents type is source-qualified as `studio.lerian...`. Headers `ce-resourcetype` and `ce-eventtype` carry the dispatch key separately. The `ce-tenantid` header is always present for Midaz. It carries your tenant ID, or the literal `default` when the event has no tenant. The ledger and Tracer each serve their public JSON business-event catalog and topic mapping at: ```http theme={null} GET /v1/streaming/manifest ``` The manifest route remains available even when you disable publication. Use it as the deployment-time contract instead of guessing topics. The [Midaz event reference](/en/reference/events/midaz) and [Tracer event reference](/en/reference/events/tracer) list every public business event and payload. ### Delivery semantics Midaz publishes these business events directly after the state change. It does not persist them in a streaming outbox. Midaz logs publication failures and does not return them to the business command. Consumers must still be idempotent and deduplicate on `(ce-source, ce-id)` because broker delivery can repeat a published record. Money values use decimal strings. Never parse them as binary floating point. ### Consumer checklist 1. Read the producer manifest. 2. Subscribe to the producer's application topic. 3. Route on `ce-resourcetype` and `ce-eventtype`, not on payload shape. 4. Validate `ce-schemaversion` before decoding. 5. Persist your deduplication key before acknowledging the record. ## Legacy RabbitMQ surface *** Midaz also maintains legacy RabbitMQ exchanges for selected overdraft and audit events. This is not the complete Kafka catalog. The bundled example configuration ships these outbound exchanges disabled. If you enable them, you own queue bindings, `ack`/`nack`, dead-lettering, retries, and consumer isolation. The internal async transaction balance-operation pipeline controlled by `RABBITMQ_TRANSACTION_ASYNC` is not a public event feed. Use the Kafka surface for new integrations unless you explicitly depend on a legacy RabbitMQ contract. ## Streaming Hub compatibility *** Streaming Hub follows the v3 Midaz application topics `lerian.streaming.ledger` and `lerian.streaming.tracer`. A subscription can therefore receive Midaz facts when you enable the producer, hub ingest, and destination runtime. Match on the event key from `ce-resourcetype` and `ce-eventtype`. Do not expect a per-event Kafka topic. ## Decision *** | Need | Use | | - | - | | Public Midaz business-event catalog | Direct Kafka consumption or Streaming Hub | | Existing compatibility integration on selected exchanges | Legacy RabbitMQ | | Managed webhook, pull, SQS, RabbitMQ, or EventBridge delivery | Streaming Hub subscription | # Recommended workflow Source: https://docs.lerian.studio/en/products/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. 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: ![Workflow for setting up and managing ledgers when onboarding to Midaz](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/setup-ledgers.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=b6880cb23b58e3a4c0883d387bf0dffa) Figure 1. Workflow for setting up and managing ledgers. ### Step 1: [Set up the Organization](/en/reference/products/midaz/v2/create-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. ### Step 2: [Create the Ledgers](/en/reference/products/midaz/v2/create-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 is the base layer for all financial activity. ### Step 3: [Define Asset types](/en/reference/products/midaz/v2/create-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/products/midaz/v2/create-account-type) To enforce account classification rules, define your **Account Types**. These are categories, for example `user_wallet`, `treasury`, or `revenue`. You reference them later during account creation or transaction validation. > [!NOTE] > > 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/products/midaz/v2/create-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/products/midaz/v2/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. > [!WARNING] > > 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 `routeId` 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 Access Manager to assign permissions by resource and action. * 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. ![Workflow for configuring accounts, portfolios, and segments to structure customer data and product entitlements in Midaz](https://mintcdn.com/lerian-49cb71fc/SFzzdxyH5SN7w_fC/images/en/d2/accounts-portfolios-segments.svg?fit=max\&auto=format\&n=SFzzdxyH5SN7w_fC\&q=85\&s=12a217ce289cfa30aa50c40ba9c45b86) Figure 2. Workflow for configuring Accounts, Portfolios, and Segments. ### Step 1: [Create a customer Portfolio](/en/reference/products/midaz/v2/create-portfolio) Create a **Portfolio** for each new customer. Portfolios are the logical containers for accounts. Link each portfolio to your CRM or internal system through its `entityId` field. ### Step 2: [Set up customer Accounts](/en/reference/products/midaz/v2/create-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/products/midaz/v2/create-account-type), the `type` field must match one of the pre-registered values. * Use account aliases to simplify downstream operations. > [!TIP] > > Account Types standardize account categorization across your ecosystem. They improve routing, reporting, and integrations. ### Step 3: [Apply segmentation](/en/reference/products/midaz/v2/create-segment) Use Segments to categorize accounts (for example, `standard`, `vip`, `enterprise`). Fee packages can target a segment, so each segment can get its own fees. ### 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/products/midaz/v2/create-transaction-direct) 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 accounts if segmentation changes. ## Implementing double-entry transaction management *** Midaz guarantees ledger integrity through strict enforcement of double-entry logic. ![Workflow for implementing double-entry transaction management](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/double-entry-transaction-management.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=dec47b42992268288fda31e26bab0337) Figure 3. Workflow for implementing double-entry transaction management ### Step 1: Structure the transaction Structure your **Transaction** object: * After you configure your Transaction Route, insert its ID into the `routeId` 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. When a retry repeats the key with the same request, Midaz 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. ### 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. Midaz keeps atomicity across all legs. Either the full transaction posts, or nothing does. # Rest API Source: https://docs.lerian.studio/en/products/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. ## REST design, JSON format, and versioning *** * **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. > [!TIP] > > For technical details on the available endpoints, see the [API Reference](/en/reference/introduction). # Scalability strategies Source: https://docs.lerian.studio/en/products/midaz/scalability-strategies Scale Midaz to millions of Accounts and Transactions per day with 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/products/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 If your bank processes 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. ### Lock-oriented Operations Midaz applies the balance changes of each transaction in a single Redis Lua script. The script checks every balance against live state before it writes, so a refused transaction changes no balance. 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 The Midaz ledger runs as one service that holds onboarding, transactions, CRM, and Fees. You scale it by adding instances. Start small and add instances as demand grows. For high loads, deploy several instances of the ledger 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. ### 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, so you deploy the parts you need. ## Handling multi-currency and multi-entity Operations *** ### Multi-currency strategies Midaz supports multiple currencies with a separate account for each currency. Each transaction moves one asset. 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 same ledger instances process the requests of every organization, 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 Create the indexes you need explicitly through the Metadata Index API. By default, Midaz does not index custom metadata fields. Clients fill these fields as flat 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. With an index, MongoDB finds the documents fast and cuts query response time. **Recommendations:** * Index only the metadata fields you query 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/products/midaz/v2/create-metadata-index) * [List Metadata Indexes](/en/reference/products/midaz/v2/get-all-metadata-indexes) * [Delete a Metadata Index](/en/reference/products/midaz/v2/delete-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 targets high-scale operations, but real-world tests help you tune your setup. # Segments Source: https://docs.lerian.studio/en/products/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/products/midaz/v2/list-segments): View all Segments in a Ledger. * [Retrieve a Segment](/en/reference/products/midaz/v2/get-segment-by-id): Get the details of a specific Segment. * [Create a Segment](/en/reference/products/midaz/v2/create-segment): Register a new Segment. * [Update a Segment](/en/reference/products/midaz/v2/update-segment): Edit an existing Segment. * [Delete a Segment](/en/reference/products/midaz/v2/delete-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/products/midaz/console/managing-segments) # Structuring bank operations with Midaz Source: https://docs.lerian.studio/en/products/midaz/structuring-bank-operation Design Organizations, Ledgers, Portfolios, and Segments for retail and corporate banking, with 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. ## 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. > [!NOTE] > > 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. > [!NOTE] > > Use multiple ledgers only when necessary to keep complexity low. A transaction can move funds between ledgers only when each ledger enables `crossLedger.enabled`. See [Ledgers](/en/products/midaz/ledgers). ## Account structuring for retail and corporate banking *** ### Retail customers – Portfolio per customer For retail banking, create a Portfolio for each customer. Each portfolio holds the customer's accounts, such as checking, savings, or credit card. This structure makes balances easy to read per customer. > [!TIP] > > Use [CRM](/en/products/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 **1. Transaction initiated** Maria sends R\$ 500 via Pix from her checking account. **2. Transaction Route validates** 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. **3. Operations execute** 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. **4. Event published** When streaming is enabled, `STREAMING_BROKERS` and the Ledger CloudEvents source are configured, and the transaction reaches an eligible lifecycle state, Midaz emits a transaction lifecycle CloudEvent. The bank's notification service reads the event and sends Maria a push notification. **5. External settlement** 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 | > [!TIP] > > When in doubt, start with a single ledger. You can 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/products/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. > [!NOTE] > > **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 enable route validation on a Ledger, Midaz validates each transaction 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. > [!NOTE] > > You define the validation patterns through Operation Routes and Accounting Routes. ## 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. ## Working with Accounting Routes *** To use Accounting Routes, you complete a one-time setup and then execute transactions. ### 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/products/midaz/ledgers#ledger-settings). This controls whether transactions in that Ledger must comply with your configured routes. ```json PATCH /v2/organizations/{organization_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. `validateRoutes` enforces the `account_type` rule on an Operation Route, independent of this flag. #### 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. With route validation enabled, a transaction can use the route only for the actions that have an entry. 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" }, "accountingEntries": { "direct": { "credit": { "code": "3.1.001", "description": "Fee revenue" } } } } ``` * **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"] }, "accountingEntries": { "direct": { "debit": { "code": "1.1.1.001", "description": "User wallet fee" } } } } ``` **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-out - Current Account", "description": "Operation route for sending Pix payments from 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" } } ``` > [!NOTE] > > 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. An Accounting Route needs at least one source and one destination Operation Route (errors `0153` and `0154`). A bidirectional route counts as both. ```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": [ "0197e6aa-1695-734a-a8c3-8c79e0ad32c2", "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`. Four supplementary keys (`overdraft`, `block`, `unblock`, and `crossLedger`) describe accounting impact but are **not** valid transaction-route actions. The entries determine the `routeCode` and `routeDescription` annotations on each operation. The `accounting.validateRoutes` setting in the [Ledger Settings](/en/products/midaz/ledgers#ledger-settings) controls this behavior. When you enable it, Midaz rejects a transaction whose legs do not match the entries of its Accounting Route. When you disable it, Midaz resolves no rubric, and `routeCode` stays empty. > [!NOTE] > > The **[Accounting Entries](/en/products/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. 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 | > [!CAUTION] > > 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 (error `0163`). 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 `hold`, `commit`, `cancel`, `revert`, or `overdraft`, you must also define `direct` (error `0164`). It is 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`. * **`crossLedger`** classifies the External Account legs that Midaz adds when a transaction spans Ledgers. It requires both rubrics and a `bidirectional` route (error `0162`) with no other entry (error `0256`). An Accounting Route links at most one such route. * Any key outside these nine is rejected with error `0053` (Unexpected Fields). > [!TIP] > > 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, set **`routeId`** to the ID of the Accounting Route you created, and **`operationRouteId`** on each leg. Midaz then validates the transaction against your routing patterns. 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 ``` A matching request to [create a direct transaction](/en/reference/products/midaz/v2/create-transaction-direct): ```json JSON expandable theme={null} { "routeId": "5656daa5-5b2a-4637-955f-e43bafceaf5d", "description": "Cashout fee collection transaction", "asset": "BRL", "amount": "10", "debits": [ { "alias": "@user_wallet_123", "organizationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "ledgerId": "9c858901-8a57-4791-81fe-4a34d4dd8ab5", "amount": "10", "description": "Fee debit from user wallet", "operationRouteId": "0197e6aa-1695-734a-a8c3-8c79e0ad32c2" } ], "credits": [ { "alias": "@external/BRL", "organizationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "ledgerId": "9c858901-8a57-4791-81fe-4a34d4dd8ab5", "amount": "10", "description": "Fee credit to revenue account", "operationRouteId": "0197e675-37cc-71d7-96c2-f58000f33aa0" } ] } ``` When you submit this transaction, Midaz checks that the `@user_wallet_123` account has one of the account types in the `account_type` rule. It also checks that the credited account is `@external/BRL`. ##### 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/products/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/products/midaz/v2/create-operation-route): define a new accounting rule for your operations. * [List Operation Routes](/en/reference/products/midaz/v2/list-operation-routes): view all configured Operation Routes. * [Retrieve an Operation Route](/en/reference/products/midaz/v2/get-operation-route-by-id): get detailed information on a specific Operation Route. * [Update an Operation Route](/en/reference/products/midaz/v2/update-operation-route): modify existing accounting rules. * [Delete an Operation Route](/en/reference/products/midaz/v2/delete-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/products/midaz/v2/create-transaction-route): define new routing logic to connect transactions to accounting operations. * [List Transaction Routes](/en/reference/products/midaz/v2/list-transaction-routes): view all configured Transaction Routes. * [Retrieve a Transaction Route](/en/reference/products/midaz/v2/get-transaction-route-by-id): get details of a specific Transaction Route. * [Update a Transaction Route](/en/reference/products/midaz/v2/update-transaction-route): modify existing routing criteria. * [Delete a Transaction Route](/en/reference/products/midaz/v2/delete-transaction-route): remove routes that are no longer applicable. Routes belong to the Organization. A transaction in any Ledger of the Organization can use a route, whatever Ledger you created it under. To create a route without a Ledger, use [Create an organization Operation Route](/en/reference/products/midaz/v2/create-organization-operation-route) and [Create an organization Transaction Route](/en/reference/products/midaz/v2/create-organization-transaction-route). # Transactions Source: https://docs.lerian.studio/en/products/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`). > [!NOTE] > > If you do not provide a `balanceKey`, the transaction uses the **default balance**. ## Double-entry accounting *** The double-entry system follows one principle. A posted transaction has at least one debit and one credit operation. A pending transaction records only its source side until you commit it. This structure records all financial activity and keeps your accounts balanced. Each transaction uses two kinds of entries: * **Debits** show the value received or the resources consumed. * **Credits** show the value given or the resources provided. Midaz rejects a transaction whose debits and credits do not balance. ### 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. > [!CAUTION] > > 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`) ![Single-source transaction moving BRL 30.00 from one source account to a single destination account](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/single-source.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=8867866e614ebd29f2abc801b03ad894) Figure 1. Example of a single source transaction. **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. ![Multi-source transaction where BRL 30.00 is drawn from two source accounts and sent to a single destination account](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/multi-source.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=a662de8e32affc1346da13432daabcd8) Figure 2. Example of a multi-source transaction. **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`). ![Single-destination transaction moving BRL 30.00 from an external account to one destination account](https://mintcdn.com/lerian-49cb71fc/Cb_meREQqa6luqbJ/images/en/d2/single-destination.svg?fit=max\&auto=format\&n=Cb_meREQqa6luqbJ\&q=85\&s=c027a71eaa987cc22a90171bbb326cc2) Figure 3. Example of a single destination transaction. **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 amount. #### 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`). ![Multi-destination transaction splitting BRL 100.00 from one source account across five destination accounts by percentage and fixed amounts](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/multi-destination.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=fc0a0dff5a1a99b2ffda26988f3fed47) Figure 4 Example of a multi-destination transaction. **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. ![Multi-source and multi-destination transaction drawing BRL 4,000.00 from four accounts and distributing it evenly across four destination accounts](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/multi-source-destination.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=bc6796fc6414907407362317958eb382) Figure 5. Example of a multi-source and multi-destination transaction. **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` | Midaz posted the transaction at once. Only the create response carries this status. Midaz stores the transaction as `APPROVED`. | Yes | Create response of a transaction that is neither pending nor an annotation, reversals included | | `APPROVED` | The transaction completed successfully. Funds have been moved between accounts. | Yes | Stored status of a `CREATED` transaction, or commit of a `PENDING` transaction | | `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/products/midaz/v1/create-transaction-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/products/midaz/v2/cancel-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/products/midaz/v1/create-transaction-annotation) | > [!TIP] > > 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) * **Annotation flow:** → `NOTED` (terminal, no transitions) > [!NOTE] > > 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. On success, the transaction status becomes `APPROVED`. > [!CAUTION] > > 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). * The destination balance does not change until you commit. * You must explicitly `commit` to execute the transfer, or `cancel` to release the funds. * Midaz refuses an external account that you send as a source of a pending transaction (error `0098`). > [!TIP] > > The Two-Phase Transaction feature supports [Flowker](/en/products/flowker/what-is-flowker). You reserve funds at the start of a workflow and run validations later. In *Figure 6*, you can see an example of a two-phase transaction with anti-fraud. ![Two-phase transaction in an anti-fraud workflow, reserving funds first and committing or cancelling them after validation](https://mintcdn.com/lerian-49cb71fc/Wqp8NG2BwBLM-rZd/images/en/d2/two-phase-transaction.svg?fit=max\&auto=format\&n=Wqp8NG2BwBLM-rZd\&q=85\&s=308c052f9967a35d6334888a2c9fb256) Figure 6. Anti-fraud workflow example ### Two-Phase Transaction flow #### 1. Create a Two-Phase Transaction * Use the [Create a Transaction using JSON](/en/reference/products/midaz/v1/create-transaction-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. #### 2. Commit or cancel the pending transaction * **Commit**: finalizes the transaction. Funds move from `on_hold` to the destination balance. * Use the [Commit a Pending Transaction](/en/reference/products/midaz/v2/commit-transaction) endpoint. * Status: `APPROVED`. * **Cancel**: releases the reserved funds back to `available` in the same balance. * Use the [Cancel a Pending Transaction](/en/reference/products/midaz/v2/cancel-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. * `transactionDate` does not change balance history. Balance history uses the time when Midaz records the transaction. * Transactions created through the [Create a Transaction Annotation](/en/reference/products/midaz/v1/create-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" } } ] } } } ``` ## 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. > [!CAUTION] > > 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" } } ] }, "distribute": { "to": [ { "accountAlias": "@account1_BRL", "amount": { "asset": "BRL", "value": "1000" } } ] } } } ``` #### Related endpoint * [Create a Transaction Annotation](/en/reference/products/midaz/v1/create-transaction-annotation): record a transaction without financial impact in the ledger. ## Real-time event publishing *** Midaz publishes transaction lifecycle events through its Kafka-compatible streaming surface. You can track your transactions as they change. After you enable it, Midaz emits `transaction.posted`, `transaction.committed`, `transaction.canceled`, and `transaction.reverted`. A new `PENDING` transaction and a `NOTED` transaction emit no event. A transaction that spans Ledgers also emits group events; see [Event publisher](/en/products/midaz/event-publisher). For more about how to publish and consume transaction events, see the [Event publisher](/en/products/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 is the bridge between Midaz and the external financial world (banks, PSPs, payment rails, and so on). ### Bringing funds into Midaz 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 > [!NOTE] > > 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. ## 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. > [!NOTE] > > 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. The Transaction Routes API is 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. Instead of handling validation separately for each transaction, 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, destination, or bidirectional), and the validation rules for each "leg" of the financial event. > [!WARNING] > > Do not use the `route` field in v1 `from` and `to` entries. Use `routeId` instead. `routeId` accepts a UUID that references an Operation Route created through the Operation Routes API. Midaz accepts `route` but does not use it in validation. ### What you do with Transaction Routes With **Transaction Routes**, you: * Keep a consistent transaction structure across your application. * Validate financial events against predefined patterns. * Configure transaction templates without code changes. * Maintain data integrity through structured validation. ## Initiating a transaction *** > [!WARNING] > > 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 The v2 [`direct`](/en/reference/products/midaz/v2/create-transaction-direct), [`hold`](/en/reference/products/midaz/v2/create-transaction-hold), [`block`](/en/reference/products/midaz/v2/create-transaction-block), and [`unblock`](/en/reference/products/midaz/v2/create-transaction-unblock) endpoints share one body. The required `debits` and `credits` arrays hold the legs of each side. The legs of each side must sum to the request-level `amount`. Each leg requires `alias`, `organizationId`, `ledgerId`, and exactly one value expression: `amount` or `share`. The `remaining` expression is not accepted in v2. Each array accepts 1 to 500 legs. `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 * To create a transaction with JSON, use the [Create a Transaction using JSON](/en/reference/products/midaz/v1/create-transaction-json) endpoint. > [!CAUTION] > > 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. > [!NOTE] > > Reversal creates a **new transaction** that compensates for the original. The original transaction remains in the ledger history for complete traceability. > [!WARNING] > > 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). * Status: `CREATED` in the create response, stored as `APPROVED`. * `parentTransactionId` that references the original transaction. 3. **Processes the reversal** through the standard transaction flow: validation, balance updates, and history recording. ### Example **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`, `CANCELED`, or `NOTED`. #### 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. * A [fee debt](/en/products/midaz/fees/fee-debt) collection cannot be reverted. #### 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/products/midaz/transaction-routing-entities). #### 6. The fee-debt record is not complete * Midaz refuses the revert of a transaction that opened a fee debt while the record of that debt is not complete. * Midaz returns error `0529` (HTTP 409) and moves nothing. Try again later. > [!CAUTION] > > 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/products/midaz/v1/create-transaction-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. > [!NOTE] > > A Block transaction records a **ledger movement** with `BLOCK`-typed operations. This differs from the balance-level controls in [Balances](/en/products/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/products/midaz/v1/create-transaction-block) endpoint to block funds. * Use the [Create an Unblock Transaction](/en/reference/products/midaz/v1/create-transaction-unblock) 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/products/midaz/v1/create-transaction-json): submit a transaction directly using a JSON payload. * [Commit a pending transaction](/en/reference/products/midaz/v2/commit-transaction): finalize a reserved transaction. * [Cancel a pending transaction](/en/reference/products/midaz/v2/cancel-transaction): release reserved funds without executing. * [Revert a Transaction](/en/reference/products/midaz/v2/revert-transaction): create a reversal transaction to undo an approved transaction. * [Create an Inflow Transaction](/en/reference/products/midaz/v1/create-transaction-inflow): register incoming funds from external sources into the Ledger. * [Create an Outflow Transaction](/en/reference/products/midaz/v1/create-transaction-outflow): move funds from internal accounts to the external world. * [Create a Block Transaction](/en/reference/products/midaz/v1/create-transaction-block): flag funds as blocked with `BLOCK`-typed operations. * [Create an Unblock Transaction](/en/reference/products/midaz/v1/create-transaction-unblock): release previously blocked funds with `UNBLOCK`-typed operations. * [List Transactions](/en/reference/products/midaz/v2/get-all-transactions): view all Transactions in your workspace. * [Retrieve a Transaction](/en/reference/products/midaz/v2/get-transaction): get details of a specific Transaction. * [Update a Transaction](/en/reference/products/midaz/v2/update-transaction): edit the metadata of an existing Transaction. * [Create a Transaction Annotation](/en/reference/products/midaz/v1/create-transaction-annotation): record a transaction without financial impact in the ledger. ### Via Lerian Console You can view and create Transactions through Lerian Console. Learn more in the [Managing Transactions](/en/products/midaz/console/managing-transactions) guide. # Transactions Source: https://docs.lerian.studio/en/products/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 *** * [**Balances**](/en/products/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/products/midaz/balance-overdraft): Controlled negative balances. * [**Transactions**](/en/products/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/products/midaz/amount): Which amount field each transaction endpoint version uses, and in which format. * [**Event publisher**](/en/products/midaz/event-publisher): Publish eligible transaction lifecycle CloudEvents through the Kafka-compatible streaming surface. * [**Receiving events from Midaz**](/en/products/midaz/receiving-midaz-events): Consume ledger events from the Kafka-compatible streaming surface or through Streaming Hub. * [**Async transaction processing**](/en/products/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/products/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/products/midaz/operations): The smallest unit of financial activity. It is 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/products/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/products/midaz/midaz-in-banking): Understand how Midaz supports banking operations. * [**Midaz for multi-account customers**](/en/products/midaz/midaz-for-multi-account-customers): Model multiple accounts under a single customer, with segregated balances, rules, and statements. * [**Closing a customer account**](/en/products/midaz/closing-a-customer-account): How to close a customer account in line with BACEN account-closure rules. * [**Global portfolio**](/en/products/midaz/midaz-in-a-global-portfolio): Configure a global account structure similar to those at major banks. * [**Academic records**](/en/products/midaz/midaz-for-academic-record-management): Manage academic records for an educational institution. * [**Supply chain**](/en/products/midaz/midaz-for-supply-chain-management): Manage the supply chain for a manufacturing company. > [!TIP] > > See [Pix use cases](/en/interfaces/pix/pix-use-cases) for scenarios that combine Midaz with Pix. # What is Midaz? Source: https://docs.lerian.studio/en/products/midaz/what-is-midaz Learn what Midaz is: Lerian's core ledger for double-entry accounting, multi-currency operations, and audit trails. **Midaz** is Lerian's core ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.) for financial infrastructure. It covers account management, transaction processing, and multi-currency operations. You configure your financial structure once (organizations, ledgers, accounts, and transaction rules). Midaz handles the rest: double-entry accounting (Every financial movement is recorded as at least two operations: a debit from one account and a credit to another, so the system always balances.), balance tracking, audit trails (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.), 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: * **Move fast**: Modular architecture lets you add products and integrations without re-architecting * **Stay compliant**: Immutable audit trails (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) and double-entry enforcement * **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 (A complete transaction blueprint composed of multiple Operation Routes. It defines all the debits and credits involved in a transaction type, so each transaction of that type complies with double-entry rules. Exposed as transactionRoute in the API.) 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**: Run digital wallets, credit platforms, and embedded finance products * **Banks**: Modernize legacy systems incrementally without full migration risk * **Enterprises**: Simplify payments and collections ## Common use cases *** > **Digital banking** > > Launch checking accounts, savings products, and instant transfers. Structure your accounts with portfolios per customer and enforce business rules through transaction routes. > **Global portfolios** > > Manage multi-currency accounts for international customers. Each currency gets its own asset (A unit of value tracked in the ledger: traditional currencies (BRL, USD), digital currencies (BTC), or custom units like loyalty points. Every account is linked to exactly one asset.) accounts while portfolios (A grouping of accounts belonging to the same account holder, business unit, or purpose. It ties together checking, savings, and investment accounts for a unified view.) tie everything together for a unified customer view. > **Pix and instant payments** > > Offer Pix (Brazil's instant payment system operated by the Central Bank (BACEN), for real-time transfers 24/7 between individuals, businesses, and government entities.) 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. > **Marketplace payouts** > > Handle complex multi-party settlements (The process of finalizing a financial transaction by transferring actual funds between institutions. Settlement accounts act as clearing points where money is held temporarily before reaching its final destination.) where a single payment splits into seller payouts, platform fees, and tax withholdings, all processed atomically as one transaction. > **Non-financial ledgers** > > Use Midaz beyond finance: track academic credits, supply chain materials, loyalty points, or carbon credits. Midaz fits any domain that needs immutable, auditable tracking of units of value. ## How Midaz works *** **1. Structure** Create your Organization, Ledgers, Assets, and Account Types (A classification defining the nature and purpose of an account, such as checking, savings, escrow, or settlement. Account types enable proper transaction routing.) to reflect your business model. **2. Onboard** Set up customer Portfolios (A grouping of accounts belonging to the same account holder, business unit, or purpose. It ties together checking, savings, and investment accounts for a unified view.) and Accounts. Group them into Segments (A classification applied to accounts that share characteristics, like account holder tiers (VIP, Student) or regional divisions. Rules and pricing can differ by segment.) for differentiated rules and pricing. **3. Define rules** Configure Accounting Routes and Operation Routes to define how money moves. Midaz validates transactions against these routes only when `accounting.validateRoutes` is enabled in the Ledger settings. (Accounting Routes are exposed as `transactionRoute` in the API.) **4. Transact** Process transactions through the API. Midaz validates and updates the balances of each transaction in one atomic step, then records its operations. **5. React** Enable event publishing to trigger downstream systems (notifications, analytics, compliance workflows) the moment a transaction completes. **6. Report** 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/products/matcher/what-is-matcher) | Optional: Reconcile (The process of comparing two sets of records (for example, your internal ledger against bank statements) to verify they match and flag discrepancies.) ledger transactions against external data sources | | [Reporter](/en/products/reporter/what-is-reporter) | Optional: Generate regulatory and business reports from ledger data | | [Flowker](/en/products/flowker/what-is-flowker) | Optional: Orchestrate pre-ledger validation workflows (anti-fraud, approvals) | | [Tracer](/en/products/tracer/what-is-tracer) | Optional: Real-time transaction validation and spending controls | > [!NOTE] > > Midaz works independently or as part of the Lerian ecosystem. ## Next steps *** > **[About Midaz](/en/products/midaz/about-midaz)** > > Understand Midaz's domains, entities, and how they relate to each other. > **[Quick start](/en/products/midaz/midaz-setup)** > > Install and configure Midaz to start building your financial infrastructure. > **[Use cases](/en/products/midaz/use-cases)** > > Explore detailed use cases for banking, global portfolios, Pix, and more. > **[Explore the Midaz API](/en/reference/products/midaz/quick-start-guide)** > > Browse endpoints, request/response schemas, and try the API directly. # Products, platform, and plans Source: https://docs.lerian.studio/en/products/products-and-plugins How Lerian's offering is structured: products, platform services, Brazil Rails, Interfaces, and their licensing. Lerian is a complete, modular core banking platform. You choose what you adopt, and when. ## The catalog *** Lerian has six products: Midaz, Tracer, Matcher, Flowker, Lender, and Reporter. Each product covers one financial domain end to end. Each product also deploys and evolves on its own. Console, Access Manager, Streaming Hub, Multi-tenancy, and Observability are platform services. They serve every product and are not products themselves. [Explore the products →](/en/products/about-products) · [Explore the platform →](/en/platform/platform-introduction) ## Licensing *** Lerian licenses every product to institutions. Midaz versions released under the Elastic License 2.0 (ELv2), including Tracer, keep that license. Tracer ships in the Midaz repository and release, and it stays a separate runtime and deployment. Reporter, Matcher, and Flowker work with a ledger, either Midaz or another one, and they do not depend on one. Lender runs its credit journeys on the Midaz ledger. After you license a product, you can also operate it from the Console. ## Plans and what's included *** There are two ways to adopt Lerian: * **The Midaz core.** You can self-host Midaz and build on the core ledger and its embedded CRM under the license terms. The same terms cover Tracer. * **The Enterprise plan.** Enterprise bundles consulting for Midaz, the Midaz plugins such as Pix and TED, and the Lerian Console. The Console is the web interface that manages it all. Lerian does not sell the Console on its own. It comes with Enterprise. > [!NOTE] > > Pricing depends on your setup and scale. [Talk to our team](https://lerian.studio/contact) to find the right plan for your use case. ## Brazil Rails and Interfaces *** A Brazil rail is Lerian-owned messaging software that connects your institution to BACEN or to a market infrastructure, with no intermediary. An interface is the product surface over a rail. Pix Lerian is an application plugin that uses a connectivity adapter and Midaz. It is not the native Lerian SPI rail. Pix JD, Pix BTG, TED JD, and Payments BTG are partner interfaces that ship as separately deployed Midaz plugins. Pix Lerian v1.0.0 is available for integration and testing with the mock provider adapter. Native SPI connectivity is not available in this release. [Explore Brazil Rails →](/en/rails/what-are-brazil-rails) · [Explore Interfaces →](/en/interfaces/introduction) · [Explore the plugins →](/en/products/about-plugins) # Core Banking vision Source: https://docs.lerian.studio/en/start-here/about-lerian/core-banking-vision Understand Lerian's vision for modern core banking, structured around four pillars: Ledger, transactional services, governance, and connectivity. Core banking should be a programmable platform. Four pillars work together to give you 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/interfaces/pix/pix-overview), including scheduled (Pix Agendado) and agreement-based automatic debits (Pix Automático) * Interbank wire transfers: [TED](/en/interfaces/ted/ted-overview), settled through BACEN's STR * Boletos and bill payments: [issuing and paying](/en/interfaces/payments-btg/boleto-and-bill-payment) boletos, DARF taxes, and utility invoices * Card-settlement messaging: [Lerian SLC](/en/rails/slc/what-is-lerian-slc) operates Núclea's ASLC messaging, while [Lerian SILOC](/en/rails/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 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 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 ## What the modular architecture allows *** 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. # Our values Source: https://docs.lerian.studio/en/start-here/about-lerian/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 core ledger. Institutions assemble the products and platform services they need. ## Our Commitment *** We design for scale, so our software grows with our clients' needs. We believe in building technology that adapts to your needs. ## Our Values *** ### Be Your Own Strength Pursue excellence, face challenges with resilience, and embrace growth. True strength comes from the ability to adapt, learn, and overcome obstacles. ### Be Your Own Courage Navigate uncertainty boldly and embrace risk with confidence. 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 underpins 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 means you take ownership, inspire others, and create lasting impact. # Our release cycle Source: https://docs.lerian.studio/en/start-here/about-lerian/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 refining this cadence, so production timing may change. We evolve all our products, improving existing capabilities and, when applicable, developing new products. We validate every change before it reaches production. ## How it works *** * **Planning**: We align scope, priorities, and technical direction. We define requirements and shape the roadmap. * **Development**: The team builds and iterates on new features, enhancements, and platform improvements based on the planned scope. * **Testing and validation**: We test and validate changes for reliability, performance, and consistency across the platform. * **Release**: We consolidate changes into a production release. We publish documentation and release notes with the rollout. # 2025 release schedule Source: https://docs.lerian.studio/en/start-here/about-lerian/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: ## 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 | # Our release schedule Source: https://docs.lerian.studio/en/start-here/about-lerian/release-schedule Track Lerian's release schedules, organized by year. This page links Lerian's release schedules by year. To understand how our release cycle works, see the [Our release cycle](/en/start-here/about-lerian/release-cycle) page. > **[2025 schedule](/en/start-here/about-lerian/release-cycle-2025)** > > Cycles completed in 2025 with dates for each stage. # Version governance Source: https://docs.lerian.studio/en/start-here/about-lerian/versioning-governance Understand Lerian's versioning governance: how changes are communicated, how dependencies stay compatible, and our backward-compatibility promise. This page covers how we communicate changes, how we manage dependencies, and our long-term commitment to backward compatibility. ## Communication of changes *** We communicate every change so teams can prepare. * **Release notes** include new features, improvements, bug fixes, and breaking changes with migration paths. * **Documentation updates** reflect API changes, new configurations, and updated behaviors. * We share **pre-release announcements** in advance for major versions. ## Versioning of dependencies *** We version dependencies with the same principles to ensure stability and compatibility. * **Internal dependencies** follow the same versioning scheme as Midaz. * We manage **external dependencies** to stay compatible throughout a major version lifecycle. ## Backward compatibility commitment *** This versioning policy applies to every new release. We commit to maintaining the defined support windows for all future versions. By preserving backward compatibility, we reduce the risk of breaking changes and let you plan upgrades on your own schedule. > [!TIP] > > For details about timelines and schedules, see the [Release cycle](/en/start-here/about-lerian/release-cycle) page. # Versioning process Source: https://docs.lerian.studio/en/start-here/about-lerian/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 follows 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 *** **Step 1** Identifies commits made since the last version. **Step 2** Determines the new version based on predefined rules. **Step 3** Automatically generates and publishes the new version. ## Version support timeline *** Midaz follows a 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. > [!CAUTION] > > Older versions do not receive further updates or support. ## Upgrade path recommendations *** Follow these recommendations when you upgrade Midaz versions. * **Minor version upgrades** (`1.0.0 → 1.1.0`) require little effort. * **Major version upgrades** (`1.2.0 → 2.0.0`) may involve planning and testing. * Every major version comes with an **upgrade guide**. # Versioning scheme Source: https://docs.lerian.studio/en/start-here/about-lerian/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. ## 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**. Requires explicit upgrade steps. | *1.0.0 → 2.0.0* | | **Y (Minor Version)** | Every development cycle | **Maintains backward compatibility**. 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 *** A breaking change happens 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. * We introduce breaking changes **only in a major version**. * They are **announced in advance**, always with migration guides and examples. * Deprecated features **emit warnings** before removal, so teams have time to adjust. * We aim to **minimize disruption** by grouping breaking changes together and providing alternative solutions whenever possible. ### Examples * **Field replacement**: `routeId` (UUID) superseded the free-text `route` field in transaction payloads. v1 still accepts `route` when `routeId` is absent. v2 accepts only `routeId`. * **Validation rules**: The Ledger Settings API replaced the `ACCOUNT_TYPE_VALIDATION` and `TRANSACTION_ROUTE_VALIDATION` environment variables. The API controls accounting validation per ledger without redeployment. * **Deprecation cycle**: We removed the `scale` field 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/start-here/about-lerian/versions Understand Lerian's versioning strategy: increments, pre-release tags, automation, upgrade paths, and how changes are communicated. Our versioning strategy defines how we increment versions, how we apply pre-release tags, and how automation keeps everything consistent across the system. Each release follows a [clear cycle](/en/start-here/about-lerian/release-cycle). Alongside the technical rules, we provide guidance on upgrade paths, outline our support policy, and communicate changes. # Who we are Source: https://docs.lerian.studio/en/start-here/about-lerian/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 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. We built our platform 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** consolidating payments, collections, and reconciliation across business units ## Our principles *** Three principles guide how we build financial software: #### Independence over vendor lock-in We build systems that you control. We use standard interfaces and open formats, so no single vendor dictates your roadmap. #### Transparency over opacity Financial systems require absolute clarity. We design our infrastructure to be inspectable and auditable, so it has no hidden logic or opaque behaviors that introduce systemic risk. #### Architecture over shortcuts We prioritize scalable design over quick fixes. Our solutions handle real-world complexity, so your infrastructure grows with your business without constant re-platforming. ## Explore further *** > **[Why Lerian](/en/start-here/about-lerian/why-lerian)** > > Understand the problems we solve and how our platform helps financial institutions regain control. > **[Our values](/en/start-here/about-lerian/our-values)** > > Discover the core values and commitments that drive our team and shape our culture. ## Get in touch *** Talk to us about your questions, a partnership, or your architecture. > **[Contact us](https://lerian.studio/en/contact)** > > Reach out to our team to discuss how Lerian can support your infrastructure needs. # Why Lerian Source: https://docs.lerian.studio/en/start-here/about-lerian/why-lerian Understand how Lerian helps financial institutions modernize core banking without vendor lock-in: modular products and flexible deployment. Modern financial institutions face a common set of challenges. Legacy systems cost too much to maintain, and they adapt too slowly. They also lock you into vendors who dictate the roadmap. Lerian gives you back control. > [!NOTE] > > **New to core banking?** If terms like *Ledger*, *debit and credit*, or *chart of accounts* are new to you, start with the fundamentals first. They make everything here easier to follow. > > **[Begin with Core Banking Fundamentals](/en/start-here/fundamentals/core-banking/what-is-core-banking)** ## The problems we solve *** | Challenge | How Lerian helps | | :- | :- | | **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 and catches discrepancies | | **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) and business analytics automatically | | Transaction reconciliation | **Matcher** | Compares internal records against bank statements and payment processors, and 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 | | Credit products and loan management | **Lender** | Defines loan products and manages the loan lifecycle: applications, disbursement, and interest accrual | Console, Access Manager, Streaming Hub, Multi-tenancy, and Observability are platform services. They are not products themselves. | Platform service | What it does | | :- | :- | | **Console** | Operates Lerian products from one web interface, with one login | | **Access Manager** | Controls human and machine access, and issues access tokens | | **Streaming Hub** | Delivers platform events to your webhooks, cloud queues, or pull consumers | | **Multi-tenancy** | Serves independent institution contexts from one deployment | | **Observability** | Collects metrics, traces, and logs from Lerian workloads through OpenTelemetry | ## 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. Choose based on your regulatory requirements and operational maturity. [Compare deployment models in detail →](/en/start-here/evaluate-and-deploy/deployment-models) ## Built for trust and independence *** * **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 * **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** consolidating payments, collections, and reconciliation across business units ## Next steps *** > **[Our Core Banking vision](/en/start-here/about-lerian/core-banking-vision)** > > Understand the four pillars that form our architectural philosophy. > **[Getting started](/en/start-here/getting-started)** > > Choose your path: evaluate the platform or start building. > **[Deployment models](/en/start-here/evaluate-and-deploy/deployment-models)** > > Understand the differences between fully managed and self-hosted deployment. > **[Talk to us](https://lerian.studio/en/contact)** > > Talk to us about your questions or a partnership. # Lerian Cloud (SaaS) and BYOC Source: https://docs.lerian.studio/en/start-here/evaluate-and-deploy/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 > [!TIP] > > Lerian Cloud is ideal if you want the fastest path to production without building or maintaining infrastructure. Lerian operates the platform for you from day one. ## BYOC (Bring Your Own Cloud) *** In the BYOC model, you deploy and operate Lerian in your own infrastructure. That can be 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 (A pre-configured package for deploying software on Kubernetes. Think of it as a ready-to-use installation template that automates setup and configuration.), Terraform templates (An infrastructure automation tool that provisions and manages cloud resources (servers, databases, networks) through configuration files instead of manual setup.), and tooling to make deployment and lifecycle management straightforward. **What Lerian provides:** * Application software (container images, Helm Charts) * [Terraform templates](/en/products/midaz/midaz-terraform-foundation) for infrastructure provisioning * [Lifecycle Management](/en/platform/lifecycle-management) (Enterprise): an agent in your cluster, through which Lerian installs and upgrades products * Security patches and application updates (delivered by Lerian, and applied by you or by Lerian through Lifecycle Management) * Technical support (Enterprise) **What you manage:** * Cloud infrastructure and Kubernetes (A platform for running and managing software applications at scale. It automatically handles deployment, scaling, and recovery across multiple servers.) 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 > [!TIP] > > 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 (A pre-configured package for deploying software on Kubernetes. Think of it as a ready-to-use installation template that automates setup and configuration.) for the products and platform components supported through its Helm repository. Helm is the recommended way to deploy, configure, and manage the platform in your infrastructure. * **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. You do not 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. For step-by-step installation instructions, see the [Midaz chart README](https://github.com/LerianStudio/helm/blob/main/charts/midaz/README.md#install-midaz-helm-chart). ### 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 | > [!NOTE] > > 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) 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. 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. > [!TIP] > > For a deeper look at how tenant isolation works, see [Multi-tenancy](/en/platform/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 | > [!TIP] > > 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 with Helm, or by Lerian through Lifecycle Management | | **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/products/midaz/midaz-models). | | **Community** | **Enterprise** | | :- | :- | :- | | **Lerian Cloud (SaaS)** | Not available | Available | | **BYOC** | Available for Midaz only (self-managed) | Available (with Lifecycle Management and support) | > [!NOTE] > > Lerian Cloud requires Lerian to manage infrastructure, operations, and lifecycle on your behalf. Those capabilities are part of the Enterprise offering. That's why it is not available under the Community model. * **Community + BYOC**: You deploy and manage Midaz using the published Midaz Helm charts and documentation. Community support via GitHub and Discord. * **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 *** > **[Community and Enterprise](/en/products/midaz/midaz-models)** > > Understand the licensing differences and what's included in each model. > **[Getting started](/en/start-here/getting-started)** > > Follow the step-by-step guide to set up your first Ledger and run transactions. > **[Security](/en/products/midaz/security)** > > Learn how the shared responsibility model works for each deployment option. > **[Deployment strategies](/en/products/midaz/deployment)** > > Explore Kubernetes, Helm, and Terraform options for BYOC deployments. # Before you begin Source: https://docs.lerian.studio/en/start-here/evaluate-and-deploy/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 page to understand usage rights. ## Available pages *** > **[Using our Sandbox](/en/start-here/evaluate-and-deploy/using-our-sandbox)** > > Test your integrations in a secure, isolated environment with simulated data. > **[Using the Try It feature](/en/start-here/evaluate-and-deploy/using-try-it)** > > Interact with our APIs directly from the documentation. > **[Lerian's License](/en/start-here/evaluate-and-deploy/lerians-license)** > > Understand how Lerian's software license works, including usage rights and conditions. # Initiating with Enterprise Model Source: https://docs.lerian.studio/en/start-here/evaluate-and-deploy/initiating-with-enterprise-model Set up Lerian Enterprise in the right order to onboard product, engineering, and ops teams across Midaz, Access Manager, and Reporter. > [!NOTE] > > 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**. ## Getting started: Setup order *** Follow this setup order: ![Recommended setup order for the Enterprise Model, sequencing Midaz, Access Manager, and the optional Fees, CRM, and Reporter capabilities](https://mintcdn.com/lerian-49cb71fc/svQbQMf9Iz90q0Cc/images/en/d2/enterprise-model.svg?fit=max\&auto=format\&n=svQbQMf9Iz90q0Cc\&q=85\&s=0a4b51b9c2763c1a87cb07f849de8eb2) Figure 1. Enterprise Model setup order. ### Step 1: Configure Midaz Core Start with the [Midaz recommended workflow](/en/products/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 roles and their permissions. * Assign permissions to roles or users. * Control access to operations by role. > [!TIP] > > Start with the [Access Manager Setup](/en/platform/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 /v2/organizations/{organization_id}/ledgers/{ledger_id}/holders/{holder_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. > [!TIP] > > Start with the [CRM Setup](/en/products/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. > [!TIP] > > Start with the [Fees Engine Setup](/en/products/midaz/fees/fees-engine-overview) page. ### Step 5: Design with Reporter templates Use **Reporter** and its template engine to generate content such as: * Transaction summaries * Regulatory documents * Customer notifications Use it to standardize documents and messages with context data from Midaz and CRM. > [!TIP] > > Start with the [Reporter quick start](/en/products/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. > [!TIP] > > Lerian capabilities are modular. Activate and expand them progressively. # Lerian's License Source: https://docs.lerian.studio/en/start-here/evaluate-and-deploy/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: * Confirm that institutions remain in good standing before they access paid features, such as products, plugins or enterprise features. * Manage which features are available per client. * Let Lerian scale support and sales operations. Only clients with valid licenses, as defined in their contracts, can access specific software components. ## License types *** In Lerian’s ecosystem, licenses can have two different scopes: ### Organization-specific licenses (non-global) Applies to the organizations listed in `ORGANIZATION_IDS`. ### Global licenses Valid for multiple organizations. ## License usage workflow *** The steps below apply to both license types: **1. Submit a license request to Lerian.** * **For organization-specific licenses**: provide the IDs of the organizations that will use the license. * **For global licenses**: inform Lerian that you need a global license. You do not need to provide organization IDs. **2. License generation and activation by Lerian** Lerian’s team follows the internal process to create and activate the license using the provided details. **3. License key delivery** Lerian sends the **License Key** (`LICENSE_KEY`) to the client. **4. Environment configuration** 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. > [!WARNING] > > 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)** ```bash Bash theme={null} # LICENSE LICENSE_KEY=your-license-key ORGANIZATION_IDS=organization-id-1,organization-id-2 ``` * **LICENSE\_KEY**: The key provided by Lerian. * **ORGANIZATION\_IDS**: The IDs of the organizations the license covers, separated by commas. **Global licenses** ```bash Bash theme={null} # LICENSE LICENSE_KEY=your-license-key ORGANIZATION_IDS=global ``` * **LICENSE\_KEY**: The key provided by Lerian. * **ORGANIZATION\_IDS**: Use the value `global` for global licenses. ## License-related errors *** For the error codes a product returns when license validation fails, see [License Manager error list](/en/platform/licensing/license-manager-error-list). > [!TIP] > > For more information about your license or to request an upgrade, please contact the Lerian commercial team. # Using our Sandbox Source: https://docs.lerian.studio/en/start-here/evaluate-and-deploy/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. The sandbox powers the Try It experience. If you haven't seen how the Try It feature works yet, check out the [Using the Try It feature](/en/start-here/evaluate-and-deploy/using-try-it) page. ## Base URLs *** | 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) | | Reporter | `https://reporter.sandbox.lerian.net` | ## 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 > [!TIP] > > Click **Try It** on an endpoint to run a live request and check how the API responds. ## Limitations *** The sandbox is designed for exploration. Some restrictions are intentional: * Data resets frequently * Some endpoints use fixed or sample responses * Some APIs can use simpler authentication rules 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/start-here/evaluate-and-deploy/using-try-it Send live requests directly from the documentation with real API responses in a controlled environment, with 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 controlled environment. If you want to understand how the sandbox works under the hood, check the page [Using our Sandbox](/en/start-here/evaluate-and-deploy/using-our-sandbox). ## What you can do *** With **Try It**, you can: * Send live requests * 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` or `X-Organization-Id`. Each endpoint specifies exactly which headers you must use. > [!CAUTION] > > 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 `id` as the `organization_id` in the Ledger path. ## How to use it *** > [!WARNING] > > The **Try It** feature is available on endpoint pages in the **API Reference**. > > If the Try It button does not appear, reload the page or check that you are in the API Reference section. **Step 1** Open any endpoint in the **API Reference**. **Step 2** Click the **Try It** button next to the endpoint path. **Step 3** Fill in the input fields such as query parameters, body content, or headers. **Step 4** Click **Send Request** to run the call. **Step 5** Review the response body, status code, and headers shown below the request. > [!TIP] > > 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. # The accounting cycle Source: https://docs.lerian.studio/en/start-here/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/start-here/fundamentals/accounting/debits-and-credits), [journal entries](/en/start-here/fundamentals/accounting/journal-entries), the [chart of accounts](/en/start-here/fundamentals/accounting/chart-of-accounts), the [financial statements](/en/start-here/fundamentals/accounting/financial-statements). The **accounting cycle** puts them in a line. It is a routine that repeats. It turns a single real-world event into a report you can trust, 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 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 you record both sides of the movement. * **Ledger**: you sort each entry 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 form the Balance Sheet, Income Statement, and Cash Flow Statement. Then the cycle repeats for the next transaction, the next day, the next period. ```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 you earn the revenue at that moment. Follow it through: 1. **Transaction.** The sale happens and the customer pays. 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. It is the same \$2,000, now part of the big picture. The cycle invents nothing new along the way. Each step reorganizes the same balanced movement into a more useful shape. It goes 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, so the sequence never changes: * 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. The balancing rule from [double-entry](/en/start-here/fundamentals/accounting/double-entry-bookkeeping) rides along at every stage. A mistake therefore shows up early (at the trial balance) instead of hiding inside a finished report. > [!NOTE] > > **See also in Core Banking** > > See how this whole flow lands in a real ledger system in [Designing your ledger plan](/en/start-here/fundamentals/core-banking/designing-your-ledger-plan) and [Where to go next](/en/start-here/fundamentals/core-banking/where-to-go-next). ## Next steps *** > [!NOTE] > > **Next up** > > See how these ideas map onto a real ledger system in [Accounting in Lerian](/en/start-here/fundamentals/accounting/accounting-in-lerian). # Accounting in Lerian Source: https://docs.lerian.studio/en/start-here/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 covers accounting in general. Midaz implements that same accounting. The terms you've already learned have direct counterparts in the product. ## The same ideas, renamed for a ledger *** From here on, the words get more product-specific. The accounting ideas stay the same and only the names change. In the table below, each accounting idea on the left maps to something concrete in Midaz on the right. | Accounting idea | In Lerian / Midaz | | - | - | | [Chart of accounts](/en/start-here/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/start-here/fundamentals/accounting/journal-entries) | A **transaction** made of **operations** | | A single debit or credit line | One **operation** | | 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 ``` ## Chart of accounts → the account hierarchy *** A [chart of accounts](/en/start-here/fundamentals/accounting/chart-of-accounts) organizes the buckets a business sorts its money into. In Midaz, an [account](/en/start-here/fundamentals/core-banking/building-blocks) belongs to one asset code and can have one or more keyed [balance](/en/start-here/fundamentals/core-banking/building-blocks) records. Accounts form a hierarchy, not a flat list. You can nest and group them to mirror the structure of a real business. The five accounting families (assets, liabilities, equity, revenue, expenses) are the kind of grouping that hierarchy expresses. A [Ledger](/en/start-here/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. It is the same map of "where value lives," given room to scale. ## Journal entry → transaction + operations *** A [journal entry](/en/start-here/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/start-here/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. It is the same event and the same two sides, in the product's vocabulary. ```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 *** Midaz builds the iron rule of accounting, [debits must equal credits](/en/start-here/fundamentals/accounting/debits-and-credits), into its processing. Midaz validates resolved source, destination, and transaction totals before processing. If they differ, the movement will not go through. Midaz validates every accepted movement with a clear source and a clear destination. The system enforces this instead of a person. The [core-banking explanation of how the ledger records money](/en/start-here/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. Midaz captures that judgment once as an [accounting route](/en/start-here/fundamentals/core-banking/routes-and-fees). The route is a reusable rule for who can send, who can receive, and which debits and credits a movement should record. With route validation enabled for the Ledger, Midaz checks transactions against that rule. A **fee** means more balanced operations on the same transaction. For example, a wallet transfer's route defines which customer account may be debited, which customer account may be credited, and which fee accounts join in when a fee applies. With route validation enabled, Midaz checks those rules. The [core-banking guide to routes and fees](/en/start-here/fundamentals/core-banking/routes-and-fees) walks through it in full. ## A note on holders and accounts *** In Midaz the [account](/en/start-here/fundamentals/core-banking/holders-accounts-aliases) is the balance container. The real-world owner is a separate **holder** record and friendly nicknames are **aliases**, kept separate so the Ledger's record of value stays clean. If that distinction matters for your work, [who owns what](/en/start-here/fundamentals/core-banking/holders-accounts-aliases) untangles it. ## Next steps *** > [!NOTE] > > **Go deeper** > > To see these entities in full, continue into the [Core banking fundamentals](/en/start-here/fundamentals/core-banking/what-is-core-banking), or revisit any idea from the [Accounting overview](/en/start-here/fundamentals/accounting/what-is-accounting). # Accrual vs. cash accounting Source: https://docs.lerian.studio/en/start-here/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. When does a sale count? The moment you agree to it and send the bill, or the moment the money 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 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 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 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 changes hands**. You write down money in and money out, and you record nothing until money moves. 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. 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 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, and that's revenue *now*, even if payment is weeks away. Receive a bill for electricity you've already used, and that's an expense *now*, even if you pay it later. It captures *promises* as well as cash: * Accrual records money customers owe you (but haven't paid) as revenue and as an asset called **accounts receivable**. * It also records bills you owe (but haven't paid) as expenses and as a liability called **accounts payable**. This gives a truer picture of how the business is *really* doing in a period. That is 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 | This is the same job, **\$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 | None *(cash just arrives)* | The sale and the \$50,000 are the same, but the methods book them 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" *** 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/start-here/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 arrive next month, or may never arrive at all if someone fails to pay. ## Why profit isn't cash *** The same gap explains why **a profitable business can still run out of cash.** Profit is revenue minus expenses. 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, 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 why there are two different reports. The Income Statement shows **profit**. The [Cash Flow Statement](/en/start-here/fundamentals/accounting/financial-statements) shows the **actual cash**. You need both to understand a business. ## Next steps *** > [!NOTE] > > **Next up** > > See how these pieces run together, period after period, in [The accounting cycle](/en/start-here/fundamentals/accounting/accounting-cycle). # Assets, liabilities & equity Source: https://docs.lerian.studio/en/start-here/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 accounting equation 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 **yours** after you pay off the debts. 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. 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 see the same idea in reverse. This is the form accountants 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 asks *how did I fund everything I own?* Nothing changes but the order. The business paid for everything it **owns** 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 *** 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 is the rule that makes accounting trustworthy. If your books *don't* balance, you know immediately that something in the records is wrong. ## Next steps *** > [!NOTE] > > **Next up** > > You've got what a business owns, owes, and keeps. The other half of the picture is what it earns and spends. See [Revenue, expenses & costs](/en/start-here/fundamentals/accounting/revenue-and-expenses), and how profit feeds back into equity. # Chart of accounts Source: https://docs.lerian.studio/en/start-here/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. Think of accounting as 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. Without it, the records would be an unsearchable pile. ## What a chart of accounts is *** A **chart of accounts** lists, in a defined structure, 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, more complete and consistent. You file every transaction into one of these named buckets, so you always know what it was for. ## How it's organized *** The chart groups accounts 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/start-here/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 keep accounts in a fixed order and leave room to add new ones (a `1040` for inventory, say) without disturbing the rest. ## How it makes financial data usable *** 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 goes. * **Scale**: whether you have ten transactions or ten million, they all have a clear home. Without it, [double-entry](/en/start-here/fundamentals/accounting/double-entry-bookkeeping) entries would have nowhere consistent to land, and no one could assemble the [financial statements](/en/start-here/fundamentals/accounting/financial-statements). ## 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 the structure of a real business. [Accounting in Lerian](/en/start-here/fundamentals/accounting/accounting-in-lerian) shows how this idea and the other accounting ideas map onto the product. > [!NOTE] > > **See also in Core Banking** > > This same structure becomes an account hierarchy in a ledger. See [The building blocks](/en/start-here/fundamentals/core-banking/building-blocks) and [Designing your ledger plan](/en/start-here/fundamentals/core-banking/designing-your-ledger-plan). ## Next steps *** > [!NOTE] > > **Next up** > > Once you file every transaction into the right account, those totals roll up into reports. Meet them in [Financial statements](/en/start-here/fundamentals/accounting/financial-statements). # Debits and credits Source: https://docs.lerian.studio/en/start-here/fundamentals/accounting/debits-and-credits What debit and credit 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 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 has 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 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 *** The everyday intuition and the accounting meaning part ways here. In your **wallet**, money in is good, and money out leaves you with 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 is money it **owes you**, not something it owns. 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. The direction persisted on that balance answers it. ## Assets and liabilities: the two sides of the ledger *** Each balance in a Ledger has a **direction**. 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`** decreases its available amount on a debit. On a credit, Midaz repays the [overdraft](/en/products/midaz/balance-overdraft) in use first, and any remainder increases its available amount. An account type can supply a default direction, but Midaz applies the direction persisted on the balance. For every **[asset](/en/products/midaz/assets)** you define (BRL, USD, a loyalty point), the Ledger automatically keeps an external account named after it (`@external/BRL`, `@external/USD`). You can also define your own named external accounts. > [!NOTE] > > The same R\$100 is an **asset** in your wallet and a **liability** on the institution's Ledger. Both are right. They are the two sides of the same movement. [The outside world](/en/start-here/fundamentals/core-banking/external-accounts-and-reconciliation) follows the asset side across the boundary. ## Debits always equal credits *** **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. 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 makes two entries with equal totals: ```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 matching is the basis of **double-entry bookkeeping**, which records every movement twice, once as it leaves and once as it arrives. > [!TIP] > > An **[operation](/en/products/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. > [!NOTE] > > **See also in Core Banking** > > See what debit and credit mean from the ledger's side in [How money is recorded](/en/start-here/fundamentals/core-banking/double-entry-explained). ## Next steps *** > [!NOTE] > > **Next up** > > Debits and credits only make sense as a pair. See how they work together in [Double-entry bookkeeping](/en/start-here/fundamentals/accounting/double-entry-bookkeeping). # Double-entry bookkeeping Source: https://docs.lerian.studio/en/start-here/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 you already understand the idea: **money never vanishes or appears. It always moves from somewhere to somewhere else.** Double-entry is the habit of recording *both* ends of that movement, every 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. Equal value on the other side must match the **value** recorded on one side. The movement is not always physical cash, but the two amounts always agree. So a transaction is **one event with two connected effects**, not a single line floating on its own. Recording both 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/start-here/fundamentals/accounting/debits-and-credits): one account gets a debit, another gets a credit, and the two totals agree. ## Why bother recording it twice? *** Recording both sides feels like extra work. It is also a 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 why banks and ledger systems build 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: **Cash** goes up, and **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/start-here/fundamentals/accounting/journal-entries). ## How it keeps the equation balanced *** Remember **Assets = Liabilities + Equity**? Double-entry is what keeps that equation true after every transaction. Because you record each event with equal and opposite effects, the two sides of the equation can never drift apart. The bookkeeping habit and the balancing rule are the same idea, viewed from two angles. > [!NOTE] > > **See also in Core Banking** > > Core banking uses this same both-sides rule. See [How money is recorded](/en/start-here/fundamentals/core-banking/double-entry-explained). ## Next steps *** > [!NOTE] > > **Next up** > > See how you write a transaction down, step by step, in [Journal entries](/en/start-here/fundamentals/accounting/journal-entries). # Financial statements Source: https://docs.lerian.studio/en/start-here/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 **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/start-here/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. A business can look profitable on paper yet still run out of cash, for example if customers haven't paid their bills yet. Accrual accounting recognizes revenue when a business earns it, and expenses when the business incurs them, not when cash moves. Cash-flow reporting tracks whether the business has the liquidity to pay its bills. ## How they fit together *** The three are 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. Anyone who lends money or makes a decision needs those three facts. > [!NOTE] > > **See also in Core Banking** > > See how a ledger's records are proved against the outside world in [The outside world](/en/start-here/fundamentals/core-banking/external-accounts-and-reconciliation). ## Next steps *** > [!NOTE] > > **Next up** > > See why profit and cash differ, and when each one enters the records, in [Accrual vs. cash accounting](/en/start-here/fundamentals/accounting/accrual-vs-cash). # Journal entries Source: https://docs.lerian.studio/en/start-here/fundamentals/accounting/journal-entries How you 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/start-here/fundamentals/accounting/debits-and-credits) and [double-entry](/en/start-here/fundamentals/accounting/double-entry-bookkeeping) as ideas. You write a transaction down as a **journal entry**, the basic unit of recording. ## What a journal entry is *** A **journal entry** is the record of a single transaction. It names the accounts involved, gives the amount, 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. 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/start-here/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 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. ## 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. Cash was debited this time and credited last time. The same account moves in 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. > [!NOTE] > > **See also in Core Banking** > > In a ledger system a journal entry becomes a transaction made of operations. See [How money moves](/en/start-here/fundamentals/core-banking/how-money-moves) and [The building blocks](/en/start-here/fundamentals/core-banking/building-blocks). ## Next steps *** > [!NOTE] > > **Next up** > > Every entry posts to an account. See where that list of accounts comes from in the [Chart of accounts](/en/start-here/fundamentals/accounting/chart-of-accounts). # Revenue, expenses & costs Source: https://docs.lerian.studio/en/start-here/fundamentals/accounting/revenue-and-expenses What revenue, expenses, and costs 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/start-here/fundamentals/accounting/financial-statements), the report that says whether you made a profit, 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. A business recognizes revenue when it earns it. That moment may differ from the moment cash arrives. Picture a bakery. Every loaf it sells brings in revenue. Add up a day's sales and that total is the day's revenue. Everything else measures against that inflow. Revenue is about what you earned, not necessarily the cash sitting in your account today. [Accrual vs. cash](/en/start-here/fundamentals/accounting/accrual-vs-cash) covers that difference. For now, think of revenue as *money earned from the work*. ## Expenses: the money you spend to earn it *** An **expense** is a cost a business recognizes in a period to keep 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 are the fuel. You can't sell bread without buying flour and paying the baker. The point of accounting is to know 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 distinction is small: * A **cost** is what you *pay* for a resource: the price of acquiring it. 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. 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 | "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/start-here/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 simple 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. That profit 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. This links back to [Assets, liabilities & equity](/en/start-here/fundamentals/accounting/assets-liabilities-equity): the Income Statement is the story of *how* equity changed over a period. ## Next steps *** > [!NOTE] > > **Next up** > > See how you write down every change to the five account families in [Debits and credits](/en/start-here/fundamentals/accounting/debits-and-credits). # What is accounting? Source: https://docs.lerian.studio/en/start-here/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. **Accounting** is the practice of keeping a clear, honest record of a business's money. It covers everything of value the business owns or owes, cash on hand included. 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. A business 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 *** Accounting watches 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 keeps these connected. If money moved, write it down. If something is owned or owed, account for it. 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, prove to a bank that you're worth lending to, or tell if you're 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 | > [!NOTE] > > **See also in Core Banking** > > Banking platforms exist to keep the same trustworthy-record promise. See [Core banking fundamentals](/en/start-here/fundamentals/core-banking/what-is-core-banking). ## Next steps *** > [!NOTE] > > **Next up** > > See why accounting matters the moment software starts holding money in [Why accounting matters](/en/start-here/fundamentals/accounting/why-accounting-matters). # Why accounting matters? Source: https://docs.lerian.studio/en/start-here/fundamentals/accounting/why-accounting-matters The "so what?" for builders: why double-entry is a system requirement, what balancing does and does not guarantee, 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. That record matters for what you build: a ledger, a wallet, a payments flow, a banking platform. The moment software starts holding other people's money, it inherits accounting's rules whether it names them or not. The same ideas that keep a shopkeeper's books honest 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 is a **promise**, not cash sitting in a box. The promise is 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. Transfers, fees, settlement, and statements all sit on top of one question: who owns what, right now? Accounting is the discipline that has answered 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 a 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 *** Balanced records make one part of a transaction continuously testable: the recorded amounts agree. Matching debits and credits detects a recorded amount mismatch. In accounting, *Assets = Liabilities + Equity* is a statement-level equation, not a runtime assertion Midaz applies after each operation. Records can balance while an account is wrong, a transaction lacks authorization, a value has the wrong classification, or another required record does not exist. 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. Balanced records keep the amounts behind a balance consistent. You still need reconciliation and separate controls 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 same accounting ideas remain, now at scale and in real time. That shapes what you build: * **Accounting is the data model**. Accounts, entries, and balances 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 come from accounting at scale**. You handle many transfers at once, keep every copy of the data in agreement, and prove what happened long after the fact. All of it comes down to keeping that balanced record correct across millions of movements. Learn the accounting once and a lot of system design stops feeling arbitrary. > [!NOTE] > > **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/start-here/fundamentals/accounting/accounting-in-lerian), or step into the [Core banking fundamentals](/en/start-here/fundamentals/core-banking/what-is-core-banking). ## Next steps *** > [!NOTE] > > **Next up** > > Start with [Assets, liabilities & equity](/en/start-here/fundamentals/accounting/assets-liabilities-equity), the equation everything rests on. # The building blocks Source: https://docs.lerian.studio/en/start-here/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 is the library itself, the catalog that organizes everything, and the shelves. There is also what sits on each shelf right now, and the *kind* of thing a shelf holds. A core banking system has five pieces that line up the same way. You only 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. Each ledger keeps its own independent, balanced set of books. * **Asset**: the *kind* of value you track, such as a currency like USD, loyalty points, or anything countable. The *category of thing on a shelf*. The system tracks the unit separately from the amount, so it knows 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. Each balance record has its own sending and receiving settings. What sits *on the shelf at this moment*. > [!NOTE] > > An **Organization** can hold many **Ledgers**. A **Ledger** holds many **Accounts**. An **Account** always has 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 *** A **balance** is the **result** of every movement into and out of the account, added up. You do not type it in or adjust it by hand. Money arrives, money leaves, and the balance is where that leaves you. You cannot edit a balance directly, so the books stay trustworthy. Recorded movements with two sides each *move it into the shape it has*. > [!TIP] > > 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. ## Next steps *** > [!NOTE] > > **See it in Lerian** > > See the building blocks in practice: [Core entities](/en/products/midaz/core-entities), [Ledgers](/en/products/midaz/ledgers), [Accounts](/en/products/midaz/accounts), [Balances](/en/products/midaz/balances), and [Assets](/en/products/midaz/assets). # Designing your ledger plan Source: https://docs.lerian.studio/en/start-here/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. Now 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 does". A **ledger plan** (sometimes called a **chart of accounts**) is a *deliberate classification* of the accounts your business needs, and how they relate. It is not a list of accounts you rush to create. 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 four steps below are that method. ## 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. > [!TIP] > > As a rule of thumb, if you'd ever want to ask "how much is in *here* right now?" then 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 *** 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 to collapse these three into one. A *classification* controls what kind an account can be. A *reporting slice* is how you group an account in a report. An *operational group* is which accounts belong to one customer. Each is different, and 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. That sketch is 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. So do the fees you collect. So does settlement, which mirrors the outside world. **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, so add a **label** for region. * *Operational group?* A premium customer might hold several wallets, so **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. ## Next steps *** > [!NOTE] > > **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/products/midaz/accounting-in-midaz), then put your plan to work with the console [Setup path](/en/products/midaz/console/midaz-console-setup-path) and [Concepts map](/en/products/midaz/console/midaz-console-concepts-map). # How money is recorded Source: https://docs.lerian.studio/en/start-here/fundamentals/core-banking/double-entry-explained Why every movement of money has two sides, what debit and credit 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", start here. You don't need an accounting background. You need one idea, and once it clicks, almost everything else in core banking falls into place. **Money never appears out of nowhere, and it never vanishes.** 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 appears or disappears. It only 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. > [!NOTE] > > It's called double-entry because the system records every movement on both sides, once as it leaves and once as it arrives. A single-sided record makes mismatches harder to detect. Recording both sides gives the system an arithmetic constraint to check. ## The two sides have names: debit and credit *** The two sides of a movement have traditional names. In this movement, they describe the recorded source and destination: * 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**. 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. In accounting more broadly, debit and credit are sides of an entry. Whether either increases or decreases a balance depends on the account type and its configured direction. 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. For the transfer shown here, **debit is the source and credit is the destination**. Don't treat that example as a universal rule for every account type. ## Why both sides are always required *** Because the system records every movement 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. 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 rule (*the two sides must balance*) is a consistency control. Midaz validates that the source total, the destination total, and the sent value all match. This check catches a recorded amount mismatch, but it does not by itself prove authorization, classification, or completeness. You can still trace every recorded movement to its source and destination. > [!TIP] > > Each individual part of a movement, a single debit or a single credit, is an **operation**. A movement of money consists of operations that balance out. ## Next steps *** > [!NOTE] > > **See it in Lerian** > > See these ideas in practice: [Transactions](/en/products/midaz/transactions) and the [Glossary](/en/start-here/glossary). # The outside world Source: https://docs.lerian.studio/en/start-here/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. Two ideas explain how a self-contained Ledger connects to the world outside, and how you *prove* your records still match what really happened out there. 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. ## 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 and balances the credit on the receiving account. The direction does not mean the external account has a negative balance. > [!NOTE] > > 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. The timing depends on your business: some reconcile **continuously** as records arrive, others at the **end of each day**. Either way, the moment you connect to the outside world, you take on the job of proving the two sides still agree. > [!NOTE] > > 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 happened, and flags anything that doesn't. ## Next steps *** > [!NOTE] > > **See it in Lerian** > > See these ideas in practice: [Transactions](/en/products/midaz/transactions) and the [Glossary](/en/start-here/glossary). # Account roles: Holders, Accounts, Aliases & Instruments Source: https://docs.lerian.studio/en/start-here/fundamentals/core-banking/holders-accounts-aliases Understand the distinct roles of Holders, Ledger Accounts, account aliases, and CRM Instruments in Midaz. Midaz keeps the balance record, the party context, and the identifiers used to find an account separate. That separation makes each concern explicit. ## Accounts hold balances and movements An **Account** is the Ledger record that holds balances and receives movements. It is the transactional record. ## Holders represent people or companies A **Holder** is the CRM record for a person or company. A Holder supplies the party context that can be associated with Ledger Accounts. ## Aliases identify Accounts An **alias** is a friendly identifier for an Account. It helps people and systems refer to an Account without using its full identifier. ## Instruments associate CRM context with Accounts An **Instrument** is the CRM record that associates a Holder with a Ledger Account. It carries the `ledgerId` and `accountId` for that association and can hold banking details, regulatory fields, metadata, and related parties. An Instrument is not an alias. An alias identifies an Account. An Instrument associates Holder and CRM context with a Ledger Account. ```mermaid theme={null} flowchart LR H["Holder
(person or company)"] --> I["Instrument
(CRM context)"] I --> ACC["Account
(balance and movements)"] AL["Alias
(friendly identifier)"] -.identifies.-> ACC classDef bal fill:#fff8e1,stroke:#f4b400,color:#333; class ACC bal; ``` ## Next steps > [!NOTE] > > **See it in Lerian** > > Explore [Accounts](/en/products/midaz/accounts) and [Instruments](/en/products/midaz/crm/alias-accounts). # How money moves Source: https://docs.lerian.studio/en/start-here/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. Now for the *movement* itself: what one complete movement is, the names of its pieces, and where the money sits before a movement finishes. 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, as one complete handover recorded in full 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 has smaller pieces: * An **operation** is one part of the movement: a single debit or a single credit. The R\$100 transfer is one transaction with two operations. One is a debit of R\$100 that leaves the customer. The other is a credit of R\$100 that arrives at the merchant. The two operations move the same amount in opposite directions, and the system records them together. > [!TIP] > > An operation is one part of a movement. A transaction is the whole movement. ## The shapes a movement can take *** The simplest movement has one source and one destination, but a single transaction can spread across more accounts. 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. > [!NOTE] > > 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. What's possible depends on the ledger you use. > [!NOTE] > > A transaction is **all-or-nothing**. Either every operation succeeds together, or none of them happen. The books never stay half-recorded. The name for this property is **atomicity**. ## Money in transit: holds and two-phase movements *** 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. An account's money has two parts: * **Available balance**: what you can spend right now. * **Held balance**: what's reserved for a movement that hasn't finished. It is still yours, frozen in place until it settles or releases. > [!NOTE] > > 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. ```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, **limits** can cap movement. A limit sets a ceiling on how much can flow. Together, these answer a common question about why an account can't spend. Usually the reason is a hold, a block, or a limit, not missing money. ## Next steps *** > [!NOTE] > > **See it in Lerian** > > See these ideas in practice: [Transactions](/en/products/midaz/transactions), [Operations](/en/products/midaz/operations), and [Balances](/en/products/midaz/balances). # The rules that govern movement Source: https://docs.lerian.studio/en/start-here/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 check that the right kinds of accounts take part. It must also record the right parts the same way every time. And when a fee shows up, where does it come from? 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, with no guessing. That's the idea behind a route. You set the rule once, and it applies 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 may send. * **Who can be the destination?** Which accounts may receive. * **Which parts belong in the record?** Which debits and credits the movement should produce. Define the rule once. When a Ledger has route validation enabled, it checks transactions of that kind against the route. A "customer payment" route spells out that a customer account sends, a merchant account receives, and exactly which operations to write. When a Ledger has route validation enabled, it checks the record of each movement of that kind against the rule. It also checks that the participating accounts are valid for it. Without that setting, a route is still a reusable definition but does not enforce those checks. > [!NOTE] > > A route is like a **form template** for one kind of transaction. It lays out the intended participants and booking. When a Ledger has route validation enabled, it checks movements against the route. A fee is just an extra line on that form. ## Fees are more operations *** A transaction consists of **operations**, each one a single debit or credit. A fee is *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. No value appears and no value disappears. The fee is real money that moves 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; ``` ## 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 you must book it the same way every time: payments, transfers, settlements. And you add a **fee** whenever a movement should also send a slice of value into a revenue or cost account. You define these deliberately up front, as part of the ledger plan that comes next. ## Next steps *** > [!NOTE] > > **See it in Lerian** > > See these ideas in practice: [Accounting Routes entities](/en/products/midaz/transaction-routing-entities) and the [Fees Engine](/en/products/midaz/fees/fees-engine-overview). # What is core banking? Source: https://docs.lerian.studio/en/start-here/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 a trustworthy record of **who owns what** and **what moved**. The other jobs all connect to it by feeding it, moving value through it, reading it, and proving it's right. Because of that record, money can never be quietly lost or invented. ## 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**. This is 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 that 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 of them together, not a Ledger with extras bolted on. The other four build on the Ledger once it records value. 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 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. ## Next steps *** > [!NOTE] > > **See it in Lerian** > > See how these jobs map to real solutions in the [Core banking vision](/en/start-here/about-lerian/core-banking-vision) and [Products and plugins](/en/products/products-and-plugins). # Where to go next Source: https://docs.lerian.studio/en/start-here/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. Next, see 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/products/midaz/what-is-midaz), the Ledger. It's the foundation everything else builds on. * **Moving value**: Midaz records transfers, operations, and holds as [transactions](/en/products/midaz/transactions). When value crosses the boundary with the outside world, [payment interfaces](/en/interfaces/introduction) such as Pix and TED carry it. Plenty of movement never crosses that boundary, and the Ledger records it. The routes and fees you saw come from Midaz's [transaction routing](/en/products/midaz/transaction-routing-entities) and the [Fees Engine](/en/products/midaz/fees/fees-engine-overview). * **Identifying who owns what**: the holders behind your accounts live in [CRM](/en/products/midaz/crm/crm-overview). * **Reading the record back**: you build [account statements](/en/products/midaz/building-account-statements) from Midaz operations, and [Reporter](/en/products/reporter/what-is-reporter) builds richer reports and templates for audits, analytics, and decisions. * **Deciding what may happen**: [Tracer](/en/products/tracer/what-is-tracer) evaluates your rules and spending limits. It returns an allow, deny, or review decision before a transaction executes. 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? *** Pick the path that fits your team: > **[Build with the API](/en/start-here/getting-started)** > > Integrate programmatically. Start here, then go deeper with the Guides and API Reference for the integration details. > **[Set it up in the Console](/en/platform/console/about-lerian-console)** > > Prefer a UI? Do the whole setup by hand in the Lerian Console, no code required. > [!NOTE] > > Need a term? The [Glossary](/en/start-here/glossary) defines every word from this section. Keep it bookmarked, and see how the pieces come together in [Building a complete core banking](/en/products/building-a-complete-core-banking). # Where to start Source: https://docs.lerian.studio/en/start-here/fundamentals/introduction A quick map of the Fundamentals section of Start Here: two plain-language series, Core Banking and Accounting, and which one to read first. The **Fundamentals** section of Start Here 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. Core Banking explains how a complete banking platform applies them. > **[Accounting](/en/start-here/fundamentals/accounting/what-is-accounting)** > > Explains the language underneath it: assets, liabilities, debits, credits, and how the books stay balanced. > **[Core Banking](/en/start-here/fundamentals/core-banking/what-is-core-banking)** > > 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. Core Banking shows you the *machine*. Accounting shows you the *grammar* behind it. ## 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** | > [!TIP] > > 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/start-here/getting-started Begin with Lerian: evaluate the platform, choose a deployment model, and run your first double-entry transaction with Midaz. Use this page to evaluate the platform or to start building. > [!NOTE] > > **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 make everything here easier to follow. > > **[Begin with Core Banking Fundamentals](/en/start-here/fundamentals/core-banking/what-is-core-banking)** ## Evaluating the platform *** Follow these steps to understand the platform's capabilities, architecture, and deployment options. **1. Understand why Lerian exists** Learn what problems the platform solves and who we built it for, in business terms. **[Why Lerian →](/en/start-here/about-lerian/why-lerian)** **2. Explore the architecture** Lerian organizes around four pillars (**Ledger**, **Transactional Services**, **Governance**, and **Connectivity**) that form a composable financial platform. **[Read the Core Banking vision →](/en/start-here/about-lerian/core-banking-vision)** **3. Choose a deployment model** 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/start-here/evaluate-and-deploy/deployment-models)** **4. See it in action** Explore real-world use cases: digital banking, global portfolios, Pix, and more. **[Browse use cases →](/en/products/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. **SaaS (Lerian Cloud)** **1. Receive your credentials** Your Lerian onboarding team provides you with API credentials (client ID and secret) and the base URL for your environment. **2. Authenticate and get your token** 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)** **3. Make your first API call** 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/products/midaz/quick-start-guide)** **4. Create your first organization** Follow the same API flow as any Midaz deployment: create an organization, a ledger, assets, and accounts. **[Follow the recommended workflow →](/en/products/midaz/recommended-workflow)** > [!TIP] > > The API surface is identical across deployment models. Lerian Cloud requires a Bearer token and scopes requests to the authenticated tenant. A permitted non-production BYOC deployment can have authentication disabled. **BYOC (self-hosted)** **1. Choose your deployment model** 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/start-here/evaluate-and-deploy/deployment-models)** **2. Understand the architecture** 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/start-here/about-lerian/core-banking-vision)** **3. Install Midaz** Midaz is Lerian's double-entry core ledger. Install it when you need to model accounts and record transactions. The other products stay modular, and you can adopt them one at a time. **[Install Midaz →](/en/products/midaz/midaz-setup)** **4. Create your first resources through the API** With Midaz running, create the essential building blocks: an organization, a Ledger, assets, and accounts. **[Make your first API call →](/en/reference/products/midaz/quick-start-guide)** **5. Follow the recommended workflow** Follow the end-to-end operational workflow, from resource setup to transaction routing and balance management. **[Follow the recommended workflow →](/en/products/midaz/recommended-workflow)** ## What's next? *** With your Ledger running and your first transactions created, pick the capability you want to add next. > **[Reconcile transactions](/en/products/matcher/getting-started/matcher-quick-start)** > > Run your first automated reconciliation between a bank statement and your Ledger. > **[Validate in real time](/en/products/tracer/getting-started)** > > 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. > **[Generate reports](/en/products/reporter/using-reporter)** > > Create a report template and generate your first financial report from a configured data source. For Midaz data, connect Reporter to Midaz first. > **[Integration diagrams](/en/diagrams/introduction)** > > Learn how the integration diagrams will show the messages of a Brazil rail combining into a journey. # Glossary Source: https://docs.lerian.studio/en/start-here/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 helps 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 account holders' 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 consistent business rules 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 links 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, an audit trail cannot change or be deleted. Regulatory compliance and dispute resolution require this. *** ## B *** ### Balance The current amount of an asset held in an account. Midaz tracks the **available** amount (ready for immediate use) and the **on hold** amount (reserved for pending transactions). ### Blast radius The scope of impact if a failure, misconfiguration, or security incident occurs. In multi-tenancy contexts, blast radius counts the tenants that suffer when something goes wrong in one tenant's scope. See also: **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 without writing application code or requiring deployments. One example: "if transaction amount exceeds 10,000 and account type is personal, flag for review". ### Core banking A complete banking platform that combines a ledger foundation with the capabilities required to operate financial products. Those capabilities are accounts and balances, value movement, account holder 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, an account holder's savings account might have child accounts for "Vacation Fund" and "Emergency Fund." Each child account tracks its own balance. *** ## D *** ### Double-entry accounting An accounting principle where the ledger records every financial movement 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 that represents value entering or leaving the system from outside. Its alias is `@external/` followed by the asset code, for example `@external/BRL`. External accounts are 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. When the JWT does not identify the tenant, or when the tenant configuration does not load, the system rejects the request. The request never falls back to a default or shared tenant. 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 you accidentally send a payment request twice, idempotency ensures the money only moves once. ### Immutable (Immutability) A property meaning "cannot change or be deleted after creation." 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, so applications stay available even when individual servers fail. *** ## 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 takes the form of key-value pairs. It does not affect ledger logic but enriches records for reporting and integration. ### Multi-tenancy An operating model where one deployment serves independent institution contexts (called tenants) with product-specific request scoping and storage isolation. Lerian Cloud operates multi-tenant. 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 consists of one or more operations, which gives precise traceability for compliance and reporting. ### Operation Route A reusable template that defines the rules for one "leg" of a financial transaction. It specifies 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. *** ## 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. ### Portfolio A grouping of accounts belonging to the same account holder, business unit, or purpose. For example, a single account holder 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). 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. Institutions use the supported product APIs and configuration surfaces. See also: **BYOC**. ### Segment A classification applied to accounts that share characteristics, like account holder tiers (VIP, Student, Business) or regional divisions. Segments enable differentiated rules, pricing, and benefits across your account holders. ### Settlement The process of finalizing a financial transaction, transferring the actual funds between institutions. Settlement accounts in Midaz are clearing points. Money waits there before it moves to its final destination (e.g., via Pix or TED to the central bank). ### Streaming Hub Lerian's event-delivery service. It sits at the edge between the platform's internal event stream and the systems you run outside it. It delivers each event you care about to a destination you own: a webhook, a cloud queue, or a cursor you pull. *** ## T *** ### Tenant A logically isolated institution context in a multi-tenant deployment. The supported product service defines its data model, authentication claims, routing, and backing resources. Do not assume one product's tenant model applies to another. See also: **Multi-tenancy**, **Tenant isolation**. ### Tenant isolation The controls that prevent another tenant from using one tenant's supported product service, data, configuration, and operations. 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 Midaz processes them 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, so every transaction of that type complies with double-entry accounting. 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. # Welcome to our guides Source: https://docs.lerian.studio/en/start-here/welcome-to-our-guides Navigate the Lerian documentation, one tab per area of the platform, from the fundamentals to the products, the Brazil rails, the integration diagrams, and the API reference. Lerian's guides sit next to the API reference, one area of the platform per tab. Use the top bar to pick an area, then use the left sidebar to move inside it. ## How to navigate *** **1. Pick a tab in the top bar** Start Here holds the fundamentals, the getting-started path, and the glossary. Lerian Products holds the products and the platform services. Brazil Rails holds the rails and the interfaces. Integration Diagrams shows how the messages of a rail combine into a journey. API Reference holds the endpoints, and News holds the release notes. **2. Pick a section in the left sidebar** Each tab splits into sections. Lerian Products holds one section per product, plus a Platform section. Brazil Rails holds one section per rail, plus an Interfaces section. **3. Read the overview page first** Each section opens with an overview page. Read it first. The pages after it carry the detail, such as how the software works, how to integrate with it, and how to operate it. # About Midaz Console Source: https://docs.lerian.studio/en/products/midaz/about-midaz-console See how the standalone Midaz Console moved into the Lerian Console Midaz Module. Midaz v3.5.0 and later releases do not include the standalone Midaz Console. 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 repository no longer includes the Console. Its features moved into the **Lerian Console** as the [Midaz Module](/en/products/midaz/console/midaz-module). ## What changed *** The Midaz Console moved from a standalone source-available interface to 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 | ## 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. > [!WARNING] > > Midaz v3.5.0 and later releases do not include the standalone Midaz Console. Migrate to the Lerian Console. ## Moving forward *** The [Midaz Module](/en/products/midaz/console/midaz-module) in the Lerian Console replaces the standalone Console. It adds unified authentication, cross-product navigation, and ongoing feature development. To see what the Midaz Module offers, read the documentation: > **[Midaz Module overview](/en/products/midaz/console/midaz-module)** > > Full documentation for the Midaz Module within the Lerian Console. > **[Lerian Console](/en/platform/console/about-lerian-console)** > > Learn about the unified platform for managing all Lerian products. # Auditability Source: https://docs.lerian.studio/en/products/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 specific 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. Midaz stores these events per Organization. You can read them through the [List Protection Audit Events](/en/reference/products/midaz/v2/get-audit-events) 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 `false`. The built-in environment example sets this flag to `false`. Keep the flag set to `false` until you configure your downstream consumer, its retention, and its investigation workflows. The Ledger sends these messages to the RabbitMQ exchange set in `RABBITMQ_AUDIT_EXCHANGE`, with the routing key set in `RABBITMQ_AUDIT_KEY`. Publishing is best-effort: Midaz logs a failed publish and does not block the transaction. 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 after you configure your downstream consumer to retain and process them. * Keep controls, retention, and evidence requirements in the system responsible for the data after Midaz emits it. > [!NOTE] > > For help designing an audit and retention architecture around Midaz, [contact us](https://lerian.studio/contact). # Setting up accounting in the Console Source: https://docs.lerian.studio/en/products/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, and you write no code. It is the Console companion to the developer-focused [Accounting Walkthrough](/en/products/midaz/accounting-walkthrough). You reach the same result through Console forms instead of API calls. It targets product managers, implementation teams, and developers who prefer to model their accounting in the UI. > [!NOTE] > > 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 *** Configure your accounting routes deliberately. A route defines the source and destination Operation Routes for a direct transaction, or a Bidirectional Operation Route that covers both sides. With **Validate Routes** enabled, Midaz validates transactions against the configured route rules. Route validation is disabled by default. Enable it only after you configure the routes your Ledger needs. Cancellation is a source-only lifecycle action: it releases held funds on the source Account and does not involve a destination Account. Each layer builds on the one before it: **1. Plan your chart of accounts** Decide which balances your product needs (customer funds, fees, settlement, treasury, revenue). **2. Create Account Types** Define the categories that classify your accounts. **3. Create Accounts** Open the actual balance containers, each with a **Type** and an Asset. You select the Type for non-external Accounts. When you enable **External account**, the Type becomes `external` automatically. When **Validate Account Type** is enabled, the Type of every non-external Account must match a registered Account Type. **4. Create Accounting Routes** Set the rules for which accounts can join each transaction and how the ledger posts entries. > [!TIP] > > 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. > [!NOTE] > > 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/products/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. > **[Create an Account Type](/en/products/midaz/console/creating-an-account-type)** > > 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 | > [!WARNING] > > The **Key Value** of an Account Type (for example `customer`) is what routes and accounts rely on. Keep it short, lowercase, and stable. You cannot change it later. To use a different key, create a new Account Type and recreate the accounts and routes that depend on it. > [!NOTE] > > The Account Types menu appears only after you enable **Validate Account Type** in your Ledger settings. To turn it on, see [Managing Ledgers](/en/products/midaz/console/managing-ledgers-via-console#ledger-settings). ## Step 3: Create your Accounts *** **Accounts** are the balance containers 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. When you enable **External account**, the Type becomes `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. > **[Create an Account](/en/products/midaz/console/creating-an-account)** > > 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. | > [!WARNING] > > 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. ### Available and On Hold balances When you open an account in the Console, Midaz splits its balance into two **buckets**. You always know what you can spend and what the ledger holds back: | 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. > [!NOTE] > > **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. > **[Manage Accounting Routes](/en/products/midaz/console/managing-accounting-routes)** > > See how the Accounting Routes page works and what each part of the wizard does. > **[Create an Accounting Route](/en/products/midaz/console/creating-an-accounting-route)** > > 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/products/midaz/console/mc-accounting). It explains the choices in plain terms. > **Source, Destination, or Bidirectional?** > > 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. > **How should an account be validated?** > > 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. > **Which accounting scenario?** > > Scenarios decide how the debit and credit entries get recorded: > > * **Direct Transaction**: a one-step movement, posted immediately. > * **Two-Step Transaction**: 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. > [!WARNING] > > 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. ## Example: 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. **1. Create the Account Types** 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/products/midaz/console/creating-an-account-type). **2. Create the Accounts** 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/products/midaz/console/creating-an-account). > [!NOTE] > > `@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/products/midaz/common-mistakes-to-avoid). **3. Create the Accounting Route** 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 Transaction** accounting scenario, with a debit entry on the source and a credit entry on the destination. See [Creating an Accounting Route](/en/products/midaz/console/creating-an-accounting-route). **4. Run a transaction** 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/products/midaz/console/creating-a-transaction). **5. Check the result** 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 belong to the same transaction, so the audit trail stays balanced. > [!TIP] > > For an authorize-then-capture flow, use a **Two-Step Transaction** scenario on the route. You see the amount move into **On Hold** when reserved, and out of it when you confirm or cancel. ## What to do next *** To go deeper, use these technical references. They cover how to automate the setup, understand the entities, and turn ledger activity into reports: > **[Accounting Walkthrough (developer)](/en/products/midaz/accounting-walkthrough)** > > The end-to-end developer version of this guide, including the data model and double-entry detail. > **[Accounting overview](/en/products/midaz/accounting-in-midaz)** > > How the core accounting primitives relate to one another. > **[Transaction Routing entities](/en/products/midaz/transaction-routing-entities)** > > The technical model behind Accounting Routes, operation routes, and entries. > **[Balances](/en/products/midaz/balances)** > > The full balance model behind available and on-hold amounts. # Adding a Portfolio to an Account Source: https://docs.lerian.studio/en/products/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: **Step 1** Click the **Link portfolio** option in the **Portfolio** column of the Account you want to add. **Step 2** The Console displays the form to edit the Account. **Step 3** Switch to the **Portfolio** tab and select the **Portfolio** you want. **Step 4** 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: **Step 1** 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. **Step 2** Click the **Edit** button. In the edit form, select the **Portfolio** you want. **Step 3** Click the **Save** button to confirm the procedure. > **API equivalent** > > [Update an Account endpoint](/en/reference/products/midaz/v2/update-account) # Adding a Segment to an Account Source: https://docs.lerian.studio/en/products/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/products/midaz/segments) page. To add a Segment to an Account, follow these steps: **Step 1** Open the **Accounts** page. Find the Account you want to add to the Segment. In the **Actions** column, click the three dots (). Select **Edit**. **Step 2** The edit sheet opens on the **Account Details** tab. Select the Segment from the **Segment** list. **Step 3** Click the **Save** button to confirm. > **API equivalent** > > [Update an Account endpoint](/en/reference/products/midaz/v2/update-account) # Checking Transaction details Source: https://docs.lerian.studio/en/products/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: **Step 1** On the **Transactions** page, find the transaction to check. In its **Actions** column, click the three dots (). Select **See details**. **Step 2** A page opens with the transaction summary. **Step 3** Then select the **Transaction Data** tab to see more information. > [!TIP] > > In the **Transaction Data** tab, edit the description if needed. # Creating a Ledger Source: https://docs.lerian.studio/en/products/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/products/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 *** **Step 1** From the **Ledgers** page, click the **New Ledger** button in the top-right corner. **Step 2** The **New Ledger** form appears. **Step 3** In the **Ledger Details** tab, enter the Ledger name in the **Ledger Name** field. **Step 4** (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. **Step 5** (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. **Step 6** After you complete the form, click **Save**. **Step 7** 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 new 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` | > [!TIP] > > 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`. > [!WARNING] > > 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. > [!WARNING] > > 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/products/midaz/console/creating-an-asset). 3. If you need to change the validation rules later, use the **Settings** tab described in [Managing Ledgers](/en/products/midaz/console/managing-ledgers-via-console#ledger-settings). > **API equivalent** > > [Create a Ledger endpoint](/en/reference/products/midaz/v2/create-ledger) # Creating a Portfolio Source: https://docs.lerian.studio/en/products/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. ## Create the Portfolio *** To create a Portfolio, follow these steps: **Step 1** From the **Portfolios** page, click the **New Portfolio** button. **Step 2** The **New Portfolio** form opens on the right side of the screen. **Step 3** 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. Minimum 3 characters. | `Acme Retail Portfolio` | | Entity Id | Optional external entity or system identifier. The Console shows this field only when you create the Portfolio. 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` | ## What to do next *** Now link the Portfolio to Accounts from [Adding Portfolio to Account](/en/products/midaz/console/adding-portfolio-to-account). You can also create Accounts that belong to it in [Creating an Account](/en/products/midaz/console/creating-an-account). > **API equivalent** > > [Create a Portfolio endpoint](/en/reference/products/midaz/v2/create-portfolio) # Creating a Segment Source: https://docs.lerian.studio/en/products/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: **Step 1** From the **Segments** page, click the **New Segment** button. **Step 2** The **New Segment** form opens on the right side of the screen. **Step 3** In the **Segment Details** tab, enter the **Segment Name**. **Step 4** (Optional) Select the **Metadata** tab to add metadata. **Step 5** When the details are complete, click the **Save** button. **Step 6** 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` | > [!TIP] > > 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/products/midaz/console/adding-segment-to-account). You can also create Accounts that belong to it in [Creating an Account](/en/products/midaz/console/creating-an-account). > **API equivalent** > > [Create a Segment endpoint](/en/reference/products/midaz/v2/create-segment) # Creating a Transaction Source: https://docs.lerian.studio/en/products/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 *** **Step 1** From the **Transactions** page, click the **New Transaction** button. **Step 2** 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 **Step 3** After selecting the transaction type, the **New Transaction** page opens with a step-by-step form: **1. Transaction Data** Fill in the basic transaction information: * (Optional) Add a **Transaction description** * If **Validate Routes** is on, select the **Transaction Route** * Enter the **Value** (amount) to transfer * Select the **Asset** for the transaction * (Optional) Turn on **Skip fees** to post the transaction without running the Fees Engine. The switch appears only when the Ledger allows skipping fees **Step 2** Click **Continue to the next step** to proceed to the accounts selection step. **3. Source / Destination** 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 > [!WARNING] > > 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/products/midaz/console/editing-account-information). **Step 4** Click **Continue to the next step** to proceed to the operations step. **5. Operations** 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 an optional **Operation description**. If **Validate Routes** is on, select its **Operation Route** * Add metadata in the **Transaction Metadata** section > [!NOTE] > > For complex transactions, the sum of all source amounts must equal the sum of all destination amounts. **Step 4** When all the information is correct, click the **Go to Review** button. **Step 5** 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) **Step 6** Review the information carefully. If everything is correct: * Click **Send Transaction** to submit the transaction * Click **Send and Create another** to submit and immediately start a new transaction **Step 7** On success, the Console shows **Transaction created** with the operations the Ledger recorded. Click **View Transaction** to open the transaction details page. ## Field guide *** **Transaction Data** | Field | What to enter | Example | | - | - | - | | Transaction description | Optional business description of the movement. | `Customer payment to merchant` | | Transaction Route | Shown when **Validate Routes** is on. Select the Transaction Route that the Transaction must match. | `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` | | Skip fees | Shown when the Ledger allows skipping fees. Turn on to post the Transaction without running the Fees Engine. | `Disabled` | **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` | | Operation Route | Shown when **Validate Routes** is on. Select the Operation Route that this transaction leg must match. | `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 > [!TIP] > > The fields in the **Transaction Metadata** section 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 using JSON](/en/reference/products/midaz/v1/create-transaction-json)** > > Create a Transaction programmatically with the JSON endpoint. > **[Retrieve a Transaction](/en/reference/products/midaz/v2/get-transaction)** > > Inspect the Transaction status and operations via API. # Creating an Account Source: https://docs.lerian.studio/en/products/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. > [!WARNING] > > You **must** link an Account to an existing Asset. [Create the Asset](/en/products/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: **Step 1** From the **Accounts** page, click the **New Account** button. **Step 2** The **New Account** form will open on the right side of the screen with three tabs: **Account Details**, **Portfolio**, and **Metadata**. **Step 3** 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. **Step 4** *(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/products/midaz/console/managing-portfolios) * You can link or unlink accounts from portfolios at any time **Step 5** *(Optional)* Switch to the **Metadata** tab to add custom key-value pairs for additional context or integration purposes. **Step 6** Click the **Save** button to create the Account. ## Field guide *** | Field | What to enter | Example | | - | - | - | | Account Name | Required descriptive name for the account, 3 to 255 characters. | `John Doe Checking Account` | | Account Alias | Required unique human-readable identifier. Use letters, numbers, hyphens (`-`), underscores (`_`), `@`, and `:`. 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 *** **Customer Checking Account** **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` | **Treasury Account** **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` | **Department Budget Account** **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` | ## Immutable fields, type validation and permissions *** > **Fields that cannot be changed after creation** > > 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. > **Account Type Validation** > > 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/products/midaz/console/managing-accounts#account-type-validation) for more information. > **Allow Sending and Allow Receiving** > > 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) > [!NOTE] > > 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/products/midaz/console/creating-an-accounting-route). 2. Create a Transaction in [Creating a Transaction](/en/products/midaz/console/creating-a-transaction). > **API equivalent** > > [Create an Account endpoint](/en/reference/products/midaz/v2/create-account) # Creating an Account Type Source: https://docs.lerian.studio/en/products/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/products/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 *** **Step 1** From the **Account Types** page, click the **New Account Type** button. **Step 2** The **New Account Type** form will open on the right side of the screen. **Step 3** 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` | > [!TIP] > > 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/products/midaz/console/creating-an-account). > **API equivalent** > > [Create an Account Type endpoint](/en/reference/products/midaz/v2/create-account-type) # Creating an Accounting Route Source: https://docs.lerian.studio/en/products/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/products/midaz/console/mc-accounting) first. > [!WARNING] > > 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. The sidebar shows **Accounting Routes** only when **Validate Routes** is on for the Ledger. When it is off, open the page from the Console page search (**Search pages...**). ## Step 1: Basic information *** **Step 1** From the **Accounting Routes** page, click **New Route**. **Step 2** The creation wizard opens on the **Transaction Route** step. **Step 3** Fill in the basic information: * **Route 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. **Step 4** 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. > [!NOTE] > > A **Bidirectional** operation route is 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. **Step 1** Click **Add Operation Route** to create your first operation route card. **Step 2** 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. * **Type**: **Source**, **Destination**, or **Bidirectional**. * **Validation Type**: **Account Type** or **@Alias**. The wizard requires at least one Account Type or an alias for each operation route. * **Account Type or Alias**: the selected Account Type values or the specific account alias. **Step 3** Configure **Accounting Scenarios** to define how the ledger records entries. **Direct Transaction** is always on, and you must fill in its entries. Toggle on the other scenarios you need: **Direct Transaction** 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 > [!TIP] > > 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. **Two-Step Transaction** 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 absent. 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. **Reversal** Entries that record the reversal of a completed transaction. Define the debit and credit account codes and descriptions for the reversal entries. > [!NOTE] > > The Reversal scenario is only available for **Bidirectional** operation routes. **Overdraft** 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. > [!NOTE] > > Overdraft is available for **Source** and **Bidirectional** operation routes. **Block** 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. > [!NOTE] > > Block is available for all operation types. Source routes show debit entries only, destination routes show credit entries only, and bidirectional routes show both. **Step 4** *(Optional)* Click **Add Operation Route** again to add more operation routes to this Accounting Route. > [!WARNING] > > 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. **Step 5** Click **Next** to proceed to Step 3. ## Step 3: Review *** **Step 1** 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 **Step 2** To adjust anything, click **Previous** to go back. **Step 3** 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 | Required account-validation rule. Use **Account Type** for account classes or **@Alias** for a specific account. | **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 *** > **Pix Transfer with a Direct scenario** > > **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` | > | 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` | ## 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/products/midaz/console/creating-a-transaction). > **[Create Transaction Route](/en/reference/products/midaz/v2/create-transaction-route)** > > Create the transaction-level route via API. > **[Create Operation Route](/en/reference/products/midaz/v2/create-operation-route)** > > Create operation-level routing rules via API. # Creating an Asset Source: https://docs.lerian.studio/en/products/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. > [!WARNING] > > Choose Asset codes carefully. Accounts and Transactions depend on them. ## Create the Asset *** **Step 1** From the **Assets** page, click the **New Asset** button. **Step 2** The **New Asset** form opens on the right side of the screen. **Step 3** 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. > [!CAUTION] > > You cannot reuse an Asset name or code in the same Ledger. Both the name and the code must be unique. **Step 4** Specify the **Asset Name**. **Step 5** 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. **Step 6** \[Optional] Select the **Metadata** tab to add custom metadata. **Step 7** After you fill in the fields, click the **Save** button. **Step 8** The Asset appears in the list of available Assets for your Ledger. > [!NOTE] > > 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/`. > [!TIP] > > 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/products/midaz/console/creating-an-account). > **API equivalent** > > [Create an Asset endpoint](/en/reference/products/midaz/v2/create-asset) # Creating an Organization Source: https://docs.lerian.studio/en/products/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. > [!NOTE] > > If you set up Midaz for the first time, follow the [Setup path](/en/products/midaz/console/midaz-console-setup-path) before you create downstream entities. ## Create the Organization *** **Step 1** Go to the **Organizations** page. Click the **New Organization** button. **Step 2** The **New Organization** page opens. **Step 3** 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. > [!TIP] > > See the [Field guide](#field-guide) section for details about each field. **Step 4** After you enter all the details, click **Save**. **Step 5** 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 | Select the country where the Organization is registered. | `Brazil` | | State | Select the state, province, or region of the selected country. | `São Paulo` | | City | City where the Organization is registered. | `São Paulo` | | ZIP Code | Postal code, digits only, up to 12 digits. | `04696040` | | Parent Organization | Optional. Select the Organization that this Organization is affiliated with, to create it as a child. | `Acme Holding` | ### 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/products/midaz/console/creating-a-new-ledger). > **API equivalent** > > [Create an Organization endpoint](/en/reference/products/midaz/v2/create-organization) # Creating an Alias from the Holder Source: https://docs.lerian.studio/en/products/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: **Step 1** On the **Holders** page, find the Holder that you want to link to the Alias Account. **Step 2** In the Actions column, click the three dots () and select **Create Alias Account**. **Step 3** The Console opens the Alias Accounts page and shows the New Alias Account form. **Step 4** Fill in the fields as required, then click **Create Alias Account**. > [!NOTE] > > The Console pre-fills the **Holder ID** field. You cannot change it. With Access Manager, the **Create Alias Account** action needs the create permission on instruments. # Creating a Holder Source: https://docs.lerian.studio/en/products/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 *** **Step 1** Go to the **Holders** page. Click the **New Holder** button. **Step 2** 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. **Step 3** Follow the on-screen instructions to enter the required information. Click **Create Holder**. **Step 4** 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` | | 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/products/midaz/console/crm-creating-alias-account). > **API equivalent** > > [Create Holder endpoint](/en/reference/products/midaz/v2/create-holder) # Creating an Alias Account Source: https://docs.lerian.studio/en/products/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 *** **Step 1** Go to the **Alias Accounts** page and click the **New Alias Account** button. **Step 2** A window will open where you can specify the information about the account. **Step 3** Fill in all the necessary information and click **Create Alias Account**. **Step 4** 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/products/midaz/console/managing-crm-alias-accounts) to review the record. > **API equivalent** > > [Create Alias Account endpoint](/en/reference/products/midaz/v2/create-instrument) # Deleting a Holder Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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: **Step 1** On the **Holders** page, find the Holder you want to delete. **Step 2** In the **Actions** column, click the three dots (). Select **Delete**. **Step 3** A confirmation dialog appears. **Step 4** Click **Confirm** to delete the Holder. ## Data deletion options *** > [!WARNING] > > **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. Use 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. > [!NOTE] > > Deletion fails while the Holder still has linked Alias Accounts or Accounts. Delete them before you delete the Holder. For more information about data security and deletion, see [CRM Data Security](/en/products/midaz/crm/crm-data-security). # Deleting an Alias Account Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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: **Step 1** From the **Alias Accounts** page, find the alias account to delete. **Step 2** In the Actions column, click the three dots (). **Step 3** Select **Delete**. **Step 4** A confirmation dialog opens. **Step 5** Click **Confirm** to delete the alias account. # Editing a Holder Source: https://docs.lerian.studio/en/products/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: **Step 1** From the **Holders** page, find the Holder that you want to edit. **Step 2** Click the three dots () from the Actions column, and select **Edit**. **Step 3** The Holders form will open. **Step 4** Edit the information as needed and click **Update Holder**. > [!NOTE] > > The **Document** field stays read-only when you edit a Holder. You cannot change it after you create the Holder. # Editing an Alias Account Source: https://docs.lerian.studio/en/products/midaz/console/crm-editing-alias-account Edit an existing Alias Account in the Midaz Module to update banking details or regulatory data. To edit the information of an Alias Account, follow these steps: **Step 1** From the **Alias Accounts** page, find the alias account that you want to edit. **Step 2** Click the three dots () from the Actions column, and select **Edit**. **Step 3** The Alias Account form will open. **Step 4** Edit the information as needed and click **Update Alias Account**. # Deleting a Ledger Source: https://docs.lerian.studio/en/products/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: > [!CAUTION] > > 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. **Step 1** On the **Ledgers** page, find the Ledger you want to delete. **Step 2** Click the three-dot menu () in the **Actions** column. Select **Delete**. **Step 3** A confirmation dialog will appear. **Step 4** Click **Confirm** to delete the Ledger. > [!WARNING] > > Midaz rejects Ledger deletion when `ENV_NAME=production`. > [!NOTE] > > 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. > **API equivalent** > > [Delete a Ledger endpoint](/en/reference/products/midaz/v2/delete-ledger) # Deleting a Portfolio Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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: **Step 1** On the **Portfolios** page, find the Portfolio to delete. **Step 2** In the **Actions** column, click the three-dot menu () and select **Delete**. **Step 3** A confirmation dialog opens. **Step 4** Click **Confirm** to delete the Portfolio. > **API equivalent** > > [Delete a Portfolio endpoint](/en/reference/products/midaz/v2/delete-portfolio) # Deleting a Segment Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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: **Step 1** On the **Segments** page, find the Segment to delete. In the **Actions** column, click the three dots (). Select **Delete**. **Step 2** A confirmation dialog opens. **Step 3** Click **Confirm** to delete the Segment. **Step 4** After you confirm, the Console removes the Segment from active listings. > **API equivalent** > > [Delete a Segment endpoint](/en/reference/products/midaz/v2/delete-segment) # Deleting an Account Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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: **Step 1** On the **Accounts** page, find the Account to delete. In the **Actions** column, click the three dots (), then select **Delete**. **Step 2** A confirmation dialog appears. **Step 3** Click **Confirm** to delete the Account. **Step 4** Midaz removes the Account from active listings. Your deployment's data retention and compliance policies still apply. > [!NOTE] > > Before deleting an Account, ensure every balance has `available`, `onHold`, and `overdraftUsed` equal to zero. Otherwise, Midaz rejects the deletion. > [!WARNING] > > 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. > **API equivalent** > > [Delete an Account endpoint](/en/reference/products/midaz/v2/delete-account) # Deleting an Asset Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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: **Step 1** On the **Assets** page, find the Asset to delete. In the **Actions** column, click the three dots (), then select **Delete**. **Step 2** A confirmation dialog appears. **Step 3** Click **Confirm** to delete the Asset. **Step 4** Midaz removes the Asset from active listings. Your deployment's data retention and compliance policies still apply. > [!NOTE] > > Deleting an Asset soft-deletes all active External Accounts with the same Asset code in that Organization and Ledger. > **API equivalent** > > [Delete an Asset endpoint](/en/reference/products/midaz/v2/delete-asset) # Deleting an Organization Source: https://docs.lerian.studio/en/products/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: > [!CAUTION] > > When you delete an Organization, you cannot use it anymore. 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. **Step 1** On the **Organizations** page, find the Organization you want to delete. **Step 2** In the **Actions** column, click the three-dot menu ( ). Then select **Delete**. **Step 3** A confirmation dialog will appear. **Step 4** Click **Confirm** to delete the Organization. > [!WARNING] > > Midaz rejects Organization deletion when `ENV_NAME=production`. > **API equivalent** > > [Delete an Organization endpoint](/en/reference/products/midaz/v2/delete-organization) # Editing a Ledger Source: https://docs.lerian.studio/en/products/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: **Step 1** On the **Ledgers** page, locate the Ledger you want to view or update. Click the three dots () in the **Actions** column and select **Edit**. **Step 2** The edit panel opens with three 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. Click **Save** to apply the name and metadata changes. **Step 3** 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. Each switch saves when you change it, and the Console shows **Ledger settings updated successfully**. The **Settings** tab has no **Save** button. > **[Ledger Settings](/en/products/midaz/console/managing-ledgers-via-console#ledger-settings)** > > Learn how to enable Validate Account Type and Validate Routes for this Ledger. > **API equivalent** > > [Update a Ledger endpoint](/en/reference/products/midaz/v2/update-ledger) > > [Update Ledger Settings endpoint](/en/reference/products/midaz/v2/update-ledger-settings) # Editing Account information Source: https://docs.lerian.studio/en/products/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: **Step 1** On the **Accounts** page, find the Account that you want to edit. In the **Actions** column, click the three dots () and select **Edit**. **Step 2** The edit sheet opens with three tabs: **Account Details**, **Portfolio**, and **Metadata**. **Step 3** Edit the information as needed and click **Save**. > [!WARNING] > > You cannot edit External Accounts. Midaz creates one automatically for each Asset. > **API equivalent** > > [Update an Account endpoint](/en/reference/products/midaz/v2/update-account) # Editing an Account Type Source: https://docs.lerian.studio/en/products/midaz/console/editing-account-type Edit the name, description, and metadata of an existing Account Type from the Console. To edit the information of an Account Type, follow these steps: **Step 1** 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**. **Step 2** The **Edit** form will open on the right side of the screen. **Step 3** Edit the information as needed and click **Save**. > [!NOTE] > > You cannot change the **Key Value** when editing an Account Type. Only the name, description, and metadata are editable. > **API equivalent** > > [Update an Account Type endpoint](/en/reference/products/midaz/v2/update-account-type) # Editing an Accounting Route Source: https://docs.lerian.studio/en/products/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. **Step 1** Go to the **Accounting Routes** page. Find the Accounting Route to edit. Click the three dots () on the route's card. Select **Details**. **Step 2** 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. **Step 3** Click **Edit** at the bottom of the detail sheet. **Step 4** The edit form opens with the existing configuration already filled in. It uses the same multi-step layout as the creation wizard. **Step 5** 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 > [!NOTE] > > 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/products/midaz/v2/update-transaction-route) API. > [!WARNING] > > 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. **Step 6** Click **Update Transaction Route** to apply your changes. ## Viewing route details *** To view an Accounting Route without editing it: **Step 1** Go to the **Accounting Routes** page. Click the three dots () on the route's card. Select **Details**. **Step 2** 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. > [!TIP] > > 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: **Step 1** Open the Accounting Route in **Edit** mode (via **Details** → **Edit**). **Step 2** Find the operation route card to remove. Click the delete button. **Step 3** Click **Confirm** in the dialog. The Console removes the card from the form. **Step 4** Click **Update Transaction Route**. The removal applies only at this step. After the update, the Console tries to delete each removed operation route. When the ledger keeps one, the Console shows a warning with the reason. For example, another Accounting Route still uses it. > [!WARNING] > > 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. Midaz rejects a change that breaks this rule. > **[Update Transaction Route](/en/reference/products/midaz/v2/update-transaction-route)** > > Update the transaction-level route via API. > **[Update Operation Route](/en/reference/products/midaz/v2/update-operation-route)** > > Update operation-level routing rules via API. # Editing an Organization Source: https://docs.lerian.studio/en/products/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: **Step 1** Select the Organization switcher at the top of the left sidebar. **Step 2** Select **Edit** from the displayed window. **Step 3** You can update the following supported Organization fields: * **Legal and Trade names** * **Address information** * **Parent Organization** * **Metadata** * **Avatar**: an image of 2 MB or less * **Accent color** **Step 4** After making your changes, click **Save** to persist the supported changes. ## Editing via the Organization page *** **Step 1** Go to the **Organizations** page and locate the Organization you want to update. **Step 2** Click the three-dot menu ( ) in the **Actions** column, then select **Edit**. **Step 3** The Organization edit page opens, where you can edit the supported Organization fields. **Step 4** After making your changes, click **Save** to apply them. > **API equivalent** > > [Update an Organization endpoint](/en/reference/products/midaz/v2/update-organization) # Editing an Asset Source: https://docs.lerian.studio/en/products/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: **Step 1** From the **Assets** page, find the Asset that you want to edit, click the three dots () from the **Actions** column, and select **Edit**. **Step 2** The edit form opens on the right side of the screen. **Step 3** Edit the information as needed and click **Save**. * You cannot change the type or the **Code** of the Asset after creation. You can edit the name and metadata. > **API equivalent** > > [Update an Asset endpoint](/en/reference/products/midaz/v2/update-asset) # Editing Portfolio information Source: https://docs.lerian.studio/en/products/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: **Step 1** From the **Portfolios** page, find the Portfolio that you want to edit, click the three dots () from the **Actions** column, and select **Details**. **Step 2** The **Portfolio Details** form will open on the right side of the screen. **Step 3** Edit the information as needed and click **Save**. > **API equivalent** > > [Update a Portfolio endpoint](/en/reference/products/midaz/v2/update-portfolio) # Editing Segment information Source: https://docs.lerian.studio/en/products/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: **Step 1** From the **Segments** page, find the Segment that you want to edit, click the three dots () from the **Actions** column, and select **Details**. **Step 2** The **Segment Details** form will open on the right side of the screen. **Step 3** Edit the information as needed and click **Save**. > **API equivalent** > > [Update a Segment endpoint](/en/reference/products/midaz/v2/update-segment) # Managing Account Types Source: https://docs.lerian.studio/en/products/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. For more details, refer to the **Account Types** section on the [Accounts](/en/products/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. **1. Open the Ledger Settings** On the **Ledgers** page, click the three dots () next to the Ledger. Select **Edit**. In the edit panel, click the **Settings** tab. **2. Enable Validate Account Type** Toggle on the **Validate Account Type** switch. The Console saves the change when you toggle the switch. > [!WARNING] > > The Type field is required whether or not validation is on. 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. > [!NOTE] > > The **Account Types** menu item appears only when you enable **Validate Account Type** 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. > [!WARNING] > > **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: > **[Create an Account Type](/en/products/midaz/console/creating-an-account-type)** > **[Edit an Account Type](/en/products/midaz/console/editing-account-type)** # Managing Accounting Routes Source: https://docs.lerian.studio/en/products/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 is the title and description. * The operation routes set which accounts participate and in which direction. * The accounting scenarios control how the ledger records entries for each transaction type. > **[Accounting rules](/en/products/midaz/console/mc-accounting)** > > 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/products/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 > [!NOTE] > > A valid Accounting Route requires at least one source and one destination operation route, or at least one bidirectional route. > [!TIP] > > A bidirectional route works as both source and destination. ### 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, the Console 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. > [!TIP] > > 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 *** > [!NOTE] > > The **Accounting Routes** menu appears only when you enable the **Validate Routes** setting for the current Ledger. To enable it, select **Edit** for the Ledger, go to the **Settings** tab, and toggle the **Validate Routes** switch. > [!WARNING] > > **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: > **[Create an Accounting Route](/en/products/midaz/console/creating-an-accounting-route)** > > Define a new transaction route with operation routes and accounting scenarios. > **[Edit an Accounting Route](/en/products/midaz/console/editing-an-accounting-route)** > > Modify an existing accounting route and its operation routes. ## Common use cases *** > **Pix transfers** > > 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. > **Two-step payment authorization** > > Create an Accounting Route with separate source and destination routes. Enable Two-Step Transaction scenarios to record the hold (reservation), commit (confirmation), and cancel (cancellation) entries. On the source route, the ledger validates the hold, commit, and cancel entries as an atomic group. > **Treasury operations with reversal** > > 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 *** **1. Use descriptive titles** Name your Accounting Routes clearly to indicate the type of transaction they handle (e.g., `Pix Transfer Route` instead of `Route 1`). **2. Choose the right operation type** Use **Bidirectional** when the same validation rules apply to both sides. Use separate **Source** and **Destination** routes when each side has different account requirements. **3. Configure accounting scenarios early** Define your chart of accounts entries during route creation. This ensures that transactions processed through the route produce correct accounting records from the start. **4. Review before confirming** 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/products/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 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 * **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** - Represent external assets or liabilities. Midaz creates one when you create an Asset, and you can create others with the **External account** option For technical details, refer to the [Accounts](/en/products/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** - Group accounts by cost center, department, or region. ## 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 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 > [!NOTE] > > The padlock icon marks an **external account**, which you cannot edit or delete. ### Available actions From the **Accounts** page, you can: > **[Create an Account](/en/products/midaz/console/creating-an-account)** > > Add internal accounts for customers, departments, or business operations > **[Edit Account information](/en/products/midaz/console/editing-account-information)** > > Modify account details, metadata, and organizational settings > **[Delete an Account](/en/products/midaz/console/deleting-an-account)** > > Delete accounts you no longer need (internal accounts only) ## Account organization strategies *** > **Customer Account structure** > > 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` > **Treasury and float management** > > 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`) > **Multi-currency operations** > > 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`) > **Department cost centers** > > 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`) ## 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, select **Edit** for the Ledger, 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 } } ``` For more information about managing account types, see [Managing Account Types](/en/products/midaz/console/managing-account-types). ## Best practices *** **1. Use descriptive names and aliases** Name accounts clearly and use aliases for easy reference (e.g., `@customer_john_checking` instead of UUID references). **2. Use account types** Enable account type validation to enforce your chart-of-accounts structure and prevent misclassification. **3. Organize with portfolios** Group related accounts into portfolios for better reporting and management, especially for customer relationships. **4. Plan your asset codes** Use consistent asset code conventions (ISO 4217 for currencies) to simplify multi-currency operations. **5. Document with metadata** Use metadata fields to store additional context like customer IDs, department codes, or integration references. # Managing Assets Source: https://docs.lerian.studio/en/products/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: > **[Create an Asset](/en/products/midaz/console/creating-an-asset)** > **[Edit an Asset](/en/products/midaz/console/editing-asset-information)** > **[Delete an Asset](/en/products/midaz/console/deleting-an-asset)** # Managing Balances Source: https://docs.lerian.studio/en/products/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. You find balances in the **Balances** section of the account details page. ## What you can do * **List balances** for an account to see the available and on-hold amounts per balance key. * **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 (**Send** and **Receive** in **Balance Details**). > [!NOTE] > > You manage overdraft limits through the update-balance API, not the Console. > [!NOTE] > > The Console UI does not support balance history or balance deletion. # Managing Alias Accounts Source: https://docs.lerian.studio/en/products/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. They add business context such as 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 * **Related parties** - People linked to the account, each with a role and a start date, under Regulatory * **Custom metadata** - Additional fields for integration and business-specific needs Alias Accounts let you keep customer-facing account information separate from the transactional ledger. This supports multi-bank scenarios and external system integration. For technical details about the underlying CRM capabilities, refer to the [CRM Overview](/en/products/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. With Access Manager, this page needs the read permission on instruments. To create, edit, or delete an alias account, you need the create, update, or delete permission on instruments. The **Alias Accounts** page displays a table listing the Alias Accounts of the current Organization, 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 > [!NOTE] > > 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: > **[Create an Alias Account](/en/products/midaz/console/crm-creating-alias-account)** > > Link a holder to a Midaz ledger account with bank details > **[Edit an Alias Account](/en/products/midaz/console/crm-editing-alias-account)** > > Update account information and financial details > **[Delete an Alias Account](/en/products/midaz/console/crm-deleting-alias-account)** > > Remove the link between a holder and ledger account ## Common use cases *** > **Linking customer accounts** > > 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) > **Multi-bank scenarios** > > 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 > **External system integration** > > 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 ## Best practices *** **1. Create holders before alias accounts** Always create the Holder record first, then create Alias Accounts to link them to ledger accounts. **2. Use consistent account numbering** Follow your institution's account numbering standards when creating alias accounts to avoid confusion and integration issues. **3. Keep financial institution details accurate** Ensure Bank IDs and other banking details are correct for proper payment routing and regulatory compliance. # Managing Holders Source: https://docs.lerian.studio/en/products/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 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/products/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 the Holders of the current Organization. ### Available actions From the **Holders** page, you can: > **[Create a Holder](/en/products/midaz/console/crm-creating-a-holder)** > > Add new individuals or companies as Holders > **[Edit a Holder](/en/products/midaz/console/crm-editing-a-holder)** > > Update holder information, contact details, and status > **[Delete a Holder](/en/products/midaz/console/crm-deleting-a-holder)** > > 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`. > [!NOTE] > > 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 *** > **Setting up individual customers** > > 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.) > **Managing business customers** > > 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 > **Integrating with external systems** > > 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 *** **1. Choose the correct holder type at creation** 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. **2. Keep document numbers accurate** 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. **3. Maintain updated contact information** Update email addresses and phone numbers in the console regularly. **4. Use External ID for integration** When you create a holder, fill in the External ID field. It maps the holder to your existing customer database. **5. Use metadata for custom needs** Use the Metadata step in the holder form to store extra information. You do not change the core structure. **6. Keep the status field meaningful** 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/products/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*). ![Home page header section displaying the name of the currently selected Ledger](https://mintcdn.com/lerian-49cb71fc/yneNeAhnXdrrmE3_/images/en/console/home-header-ledger.png?fit=max\&auto=format\&n=yneNeAhnXdrrmE3_\&q=85\&s=2b259ca02ffe000365bc5159be069b74) Figure 1. Section displaying the name of the current Ledger. ## 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 the Ledgers in your Organization and marks the current one with **(current)**. ### Available actions From the **Ledgers** page, you can: > **[Create a new Ledger](/en/products/midaz/console/creating-a-new-ledger)** > **[Edit a Ledger](/en/products/midaz/console/editing-a-ledger)** > **[Delete a Ledger](/en/products/midaz/console/deleting-a-ledger)** ## Ledger settings *** Each Ledger has a **Settings** tab where you can enable validation rules for Accounts and Transactions. **1. Open the Ledger edit panel** On the **Ledgers** page, find the Ledger you want to configure. Click the three dots () in the **Actions** column. Select **Edit**. **2. Open Settings** In the edit panel, click the **Settings** tab. **3. Update validation rules** Enable or disable **Validate Account Type** and **Validate Routes** for this Ledger. The Console saves each change when you toggle the switch. ### 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. | > [!WARNING] > > 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/products/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. To open the page, select **Metadata Indexes** in the **Settings** section of the left sidebar menu. To create an index, choose an **Entity Name** and enter a **Metadata Key**. **Unique** is off by default. **Sparse** is on by default. Keep the index focused on the keys you query. This keeps search fast and predictable. # Managing Operation Routes Source: https://docs.lerian.studio/en/products/midaz/console/managing-operation-routes Manage Operation Routes in the Console: list, create, edit, and delete the rules that select the accounts on each side of a transaction. Use the **Midaz Module** to manage the **Operation Routes** of the current Ledger. An operation route is a rule for one side of a transaction. It sets which accounts can take part in that side. You can also create operation routes inside an Accounting Route. For the full routing model, refer to [Managing Accounting Routes](/en/products/midaz/console/managing-accounting-routes) and [Accounting Routes entities](/en/products/midaz/transaction-routing-entities). ## Accessing the Operation Routes page *** > [!NOTE] > > The **Accounting** section of the sidebar appears only when you enable the **Validate Routes** setting for the current Ledger. To enable it, select **Edit** for the Ledger, go to the **Settings** tab, and toggle the **Validate Routes** switch. To open the page, select **Operation Routes** in the **Accounting** section of the left sidebar menu. > [!WARNING] > > **Operation 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 process the operation routes registered here. Entries follow these rules only after you enable **Validate Routes** in the Ledger settings. The page lists the operation routes of the current Ledger in a table: | Column | Content | | - | - | | **Title** | The name of the operation route. | | **Description** | The description of the operation route. | | **Operation Type** | The side of the transaction that the route validates. | | **Rule Type** | The validation rule. Hover over the value to see the alias or the Account Types that the rule accepts. | | **Metadata** | The metadata of the operation route. | | **Actions** | The menu with the **Details** and **Delete** options. | If the Ledger has no operation routes, the page shows the message **You haven't created any Operation Routes yet.** and a **New Operation Route** button. ## Creating an operation route *** **1. Open the form** Select **New Operation Route**. A side panel opens with two tabs: **Operation Route Details** and **Metadata**. **2. Fill in the details** On the **Operation Route Details** tab, fill in the fields in the table below. **3. Add metadata (optional)** On the **Metadata** tab, add key-value pairs. **4. Save the route** Select **Save**. A message confirms that the Console created the operation route. | Field | Required | Description | | - | - | - | | **Operation Route Name** | Yes | The title of the operation route. | | **Description** | No | A description of the operation route. | | **Operation Type** | Yes | **Source** or **Destination**. You cannot change it after you create the route. | | **Rule Type** | Yes | **Alias** or **Account Type**. | | **Account Alias** | Yes, for the **Alias** rule | Search for the account by its alias and select it. | | **Account Types** | Yes, for the **Account Type** rule | Select one or more Account Types of the Ledger. | | **Code** | No | A code for the operation route. | > [!NOTE] > > This form has no **Bidirectional** option and no accounting scenarios. To set them, use the wizard in [Creating an Accounting Route](/en/products/midaz/console/creating-an-accounting-route). ## Editing an operation route *** **1. Open the route** In the row of the operation route, click the three dots () and select **Details**. The edit panel opens with the current values. **2. Change the fields** Change the fields on the **Operation Route Details** and **Metadata** tabs. You cannot change the **Operation Type** field. **3. Save the changes** Select **Save**. A message confirms that the Console updated the operation route. ## Deleting an operation route *** **1. Select Delete** In the row of the operation route, click the three dots () and select **Delete**. **2. Confirm the deletion** In the confirmation dialog, select **Confirm**. A message confirms that the Console deleted the operation route. ## Permissions *** With Access Manager, this page needs the read permission on operation routes. To create, edit, or delete an operation route, you need the create, update, or delete permission on operation routes. # Viewing Operations Source: https://docs.lerian.studio/en/products/midaz/console/managing-operations View the Operations that make up a Transaction in the Lerian Console: inspect each balance movement per account and its 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`, `RELEASE`, `OVERDRAFT`, `BLOCK`, or `UNBLOCK`. ## What you can do * **View operation details** to see the type, direction, asset, amount, status, balance key, and accounting route. The details also show the balance before and after the operation, along with any metadata. > [!NOTE] > > You cannot edit the operation details sheet. 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 Statement** section on the account details page. # Managing Organizations Source: https://docs.lerian.studio/en/products/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. ### Available actions From the **Organizations** page, you can: > **[Create an Organization](/en/products/midaz/console/creating-an-organization)** > **[Edit an Existing Organization](/en/products/midaz/console/editing-an-organization)** > **[Delete an Organization](/en/products/midaz/console/deleting-an-organization)** ## Multiple Organizations *** 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*). ![Alternative way to open the Organizations page, using the organization icon in the left navigation bar](https://mintcdn.com/lerian-49cb71fc/yneNeAhnXdrrmE3_/images/en/console/organizations_menu_alt.png?fit=max\&auto=format\&n=yneNeAhnXdrrmE3_\&q=85\&s=a4af7abcf3575db2fe6517eb995ff14a) Figure 1. Alternative options to open the Organizations page for multiple Organizations. ### Changing Organizations To work on a different Organization, use the same pop-up window. It lists up to three other Organizations that you can select directly. To see all of them, select the cog icon (). The **Organizations** page opens. Find the Organization you want. Click the three dots () in the **Actions** column. Then select **Switch to this organization**. # Managing Portfolios Source: https://docs.lerian.studio/en/products/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 single 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 technical details about portfolios in Midaz, refer to the [Portfolios documentation](/en/products/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. > [!NOTE] > > 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: > **[Create a Portfolio](/en/products/midaz/console/creating-a-portfolio)** > > Add a new portfolio to group your accounts > **[Edit a Portfolio](/en/products/midaz/console/editing-portfolio-information)** > > Update portfolio name and metadata > **[Delete a Portfolio](/en/products/midaz/console/deleting-a-portfolio)** > > Remove a portfolio from your ledger ## Common use cases *** > **Customer account grouping** > > 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 > **Product-based organization** > > 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 > **Institutional portfolios** > > 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 > **Operational grouping** > > 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 *** **1. Plan your portfolio structure** Design your portfolio structure before you create accounts. Decide how to group the accounts and which Entity IDs to use for external systems. **2. Use Entity ID for integration** 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. **3. Choose meaningful names** Use portfolio names that clearly indicate the purpose or contents of the portfolio. **4. Use metadata for context** 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. **5. Keep portfolios focused** 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/products/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 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 * **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. Each ledger can have its own set of segments. For technical details about segments in Midaz, refer to the [Segments documentation](/en/products/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. > [!NOTE] > > 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: > **[Create a Segment](/en/products/midaz/console/creating-a-segment)** > > Add a new segment to organize your accounts > **[Edit a Segment](/en/products/midaz/console/editing-segment-information)** > > Update segment name and metadata > **[Delete a Segment](/en/products/midaz/console/deleting-a-segment)** > > Remove a segment from your ledger ## Common use cases *** > **Departmental organization** > > 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 > **Product line separation** > > 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 > **Regional organization** > > Group accounts by geographic region: > > * `North Region` - Accounts for northern operations > * `South Region` - Accounts for southern operations > * `International` - Cross-border and international accounts > **Compliance and reporting** > > 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 *** **1. Plan your segment structure** Plan your segments before you create accounts. **2. Use clear, descriptive names** Choose segment names that clearly indicate their purpose. **3. Add metadata for context** Use the Metadata tab to store additional information like cost center codes, department IDs, or integration references. **4. Keep segments consistent across ledgers** If you have multiple ledgers, consider using consistent segment naming conventions for easier cross-ledger reporting. **5. Review segments regularly** 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. Fee packages and Tracer limits can use a segment as a scope. Route validation in [Accounting Routes](/en/products/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/products/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. > [!NOTE] > > See the [Transactions](/en/products/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. > [!NOTE] > > 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: > **[Create a Transaction](/en/products/midaz/console/creating-a-transaction)** > > Record a new asset movement between accounts > **[Check Transaction details](/en/products/midaz/console/checking-transaction-details)** > > See complete transaction information and operations ## Transaction status *** Transactions in Midaz have the following statuses: * **CREATED** - Only the create response carries this status. Midaz stores the transaction as `APPROVED` * **PENDING** - A two-step transaction holding funds, awaiting commit or cancel * **APPROVED** - Midaz posted the transaction * **CANCELED** - Midaz canceled the transaction * **NOTED** - An annotation-only transaction with no balance changes. Its operations remain with all balance fields zeroed > [!NOTE] > > 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 *** > **Simple transfer between two accounts** > > 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 > **Payment with fees** > > 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 > **Consolidated payment** > > Multiple sources funding a single destination: > > * **Sources**: > * Primary account (debit \$500) > * Secondary account (debit \$300) > * **Destination**: Vendor account (credit \$800) > * Multiple sources, single destination > **Complex multi-party transaction** > > 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 *** **1. Write clear transaction descriptions** Include a clear description of the purpose of each transaction. **2. Verify account balances before transacting** Make sure the source account has enough balance before you create a transaction. This avoids rejections. **3. Use metadata for integration context** Store external reference IDs, order numbers, or other system identifiers in the transaction metadata for traceability. **4. Group related operations logically** When possible, combine related transfers into one transaction instead of several separate transactions. **5. Review transactions before confirming** Double-check the source accounts, destination accounts, and amounts before you submit. After you create a transaction, you can edit only its description and metadata. ## Accounting Routes integration *** Midaz can validate Transactions against [Accounting Routes](/en/products/midaz/console/managing-accounting-routes) before it processes them. Accounting Routes define which accounts can transact with each other. # Accounting rules Source: https://docs.lerian.studio/en/products/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. The sidebar shows the **Accounting** section only when the current Ledger has **Validate Routes** on. 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 you enable route validation, 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 you enable the Ledger setting `accounting.validateRoutes` (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 *** ![How accounts, account types, and accounting routes fit together so a transaction request is validated and posted as debit-and-credit entries](https://mintcdn.com/lerian-49cb71fc/SFzzdxyH5SN7w_fC/images/en/d2/accounting-route-flow.svg?fit=max\&auto=format\&n=SFzzdxyH5SN7w_fC\&q=85\&s=a504463da029fe875e18198b63546708) | 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 when the rule applies only to the sending side** > > Use **Source** for accounts where value originates. > > Example: a customer account can send funds in a payment flow. > **Use Destination when the rule applies only to the receiving side** > > Use **Destination** for accounts where value lands. > > Example: a merchant account can receive funds in a payment flow. > **Use Bidirectional when the same rule applies to both sides** > > Use **Bidirectional** when the same account class can send and receive. > > Example: checking accounts can transfer value to other checking accounts. > [!NOTE] > > 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. | Each Operation Route in the Console needs an account rule: registered Account Types or an `@Alias`. Midaz enforces the rule when you enable route validation. 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. | > [!WARNING] > > 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 *** > **[Setting up accounting in the Console](/en/products/midaz/console/accounting-setup-in-console)** > > 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. > **[Manage Accounting Routes](/en/products/midaz/console/managing-accounting-routes)** > > Configure accounting routes with operation rules and accounting scenarios in one wizard. # Account management Source: https://docs.lerian.studio/en/products/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. 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. The **alias** is a friendly identifier on the account itself. It lets you refer to the account 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 *** > **[Manage Accounts](/en/products/midaz/console/managing-accounts)** > > Create and configure the accounts that hold and move assets within your ledgers. > **[Manage Holders](/en/products/midaz/console/managing-crm-holders)** > > Register the individuals or entities that own accounts in your system. > **[Manage Alias Accounts](/en/products/midaz/console/managing-crm-alias-accounts)** > > Link holders to ledger accounts with banking details. > **[Manage Segments](/en/products/midaz/console/managing-segments)** > > Organize accounts into logical groupings for reporting and control. > **[Manage Portfolios](/en/products/midaz/console/managing-portfolios)** > > Group accounts into portfolios for consolidated management. # Foundation elements Source: https://docs.lerian.studio/en/products/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. 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**: A registered classification for accounts (for example, `customer`, `treasury`, `fee`, or `revenue`). Registering Account Types is optional, but every account has a type. ## Available pages *** > **[Manage Organizations](/en/products/midaz/console/managing-organizations)** > > Create and configure the top-level entities that group your ledgers. > **[Manage Ledgers](/en/products/midaz/console/managing-ledgers-via-console)** > > Set up ledgers to organize and track your financial operations. > **[Manage Assets](/en/products/midaz/console/managing-assets)** > > Register the currencies and instruments used across your ledgers. > **[Manage Account Types](/en/products/midaz/console/managing-account-types)** > > Define the categories that classify your accounts within each ledger. # Transaction basics Source: https://docs.lerian.studio/en/products/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. 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** - Midaz stores each transaction as `PENDING`, `APPROVED`, `CANCELED`, or `NOTED`. Only the create response carries `CREATED` * **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, or view it at any time. To commit or cancel a pending transaction, use the API. For technical details about transactions in Midaz, refer to the [Transactions documentation](/en/products/midaz/transactions). ## Available pages *** > **[Manage Transactions](/en/products/midaz/console/managing-transactions)** > > Create Transactions and inspect their details after processing. # Concepts map Source: https://docs.lerian.studio/en/products/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. You configure Account Types, CRM records, Portfolios, Segments, and Accounting Routes when the workflow needs them. ## Core hierarchy *** ![Core Midaz Module hierarchy, from Organization and Ledger down through Assets, Accounts, Routes, and Transactions](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/midaz-core-hierarchy.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=5f6cfdcad7c59f335dbac4f8f622b953) ### 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` | A registered classification such as `checking`, `treasury`, or `expense`. Registering one is optional, but every account has a type | 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 balance movement recorded in the ledger | It is the output of the whole setup. | ## How a transaction depends on prior setup *** ![How creating a transaction depends on the prior setup of organization, ledger, assets, accounts, and accounting routes](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/midaz-transaction-dependency.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=9e9a12efb097f1569985402ec9b9b31c) The required prerequisites depend on the Transaction and Ledger settings. A transaction requires Assets and Accounts. It requires an Accounting Route only when route validation is on. ## Practical reading of the UI *** Use this interpretation when you guide users: • `Foundation` defines where the system lives: Organization, Ledger, Asset, Account Types. • `Accounting` defines optional participation and posting rules: Accounting Routes and Operation 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 *** > **Account vs Holder vs Alias** > > 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 vs Segment vs Portfolio** > > Account Type is a registered classification, and registering one is optional. Segment and Portfolio are ledger-scoped persisted groupings of Accounts. They are not merely labels or direct Holder relationships. > **Accounting Route vs Transaction** > > The Route is an optional reusable rule. The Transaction is the individual movement. Midaz checks it against configured route rules only when route validation is on. > **Why the ledger matters so early** > > Almost every object below it is ledger-scoped. Users who treat the ledger as a cosmetic container usually misconfigure accounts and routes later. > [!TIP] > > Pair this page with [Setup path](/en/products/midaz/console/midaz-console-setup-path) when you onboard new users. This page explains the model. Setup path explains the order. # Onboarding Source: https://docs.lerian.studio/en/products/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. 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 You enter the key details of your Organization. **Required fields:** * **Legal Name** – Your company's official registered name. * **Document** – A registration number such as a CNPJ or EIN. The step also has a **Trade Name** (DBA) field. > [!WARNING] > > 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. **Fields:** * **Address** – Street address (line 1) * **Complement** – Additional address information (optional) * **Country** – Select from the dropdown * **State** – Select from the dropdown, based on country (optional) * **City** – Your city name * **ZIP Code** – Postal code, numbers only > [!TIP] > > 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`. > [!TIP] > > 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 in the Ledger, then use them when you create Accounts. **Required field:** * **Ledger Name** – A unique name for your Ledger. You can also turn on **Validate Account Type** and **Validate Routes** in this step. > [!NOTE] > > 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 configure the rest of the Midaz Module: > **[Manage Organizations](/en/products/midaz/console/managing-organizations)** > > Edit your Organization settings and create additional Organizations. > **[Manage Ledgers](/en/products/midaz/console/managing-ledgers-via-console)** > > Configure your Ledger and create new ones. > **[Create Assets](/en/products/midaz/console/creating-an-asset)** > > Define the currencies and tokens for your Ledger. > **[Create Accounts](/en/products/midaz/console/creating-an-account)** > > Set up accounts to start managing balances. # Setup path Source: https://docs.lerian.studio/en/products/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 *** **1. Create your Organization** Start with the legal and operational owner of the environment. Use [Midaz Module onboarding](/en/products/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/products/midaz/console/creating-an-organization). **2. Create your first Ledger** The Ledger is the workspace that scopes balances, Accounts, and Transactions. Go to [Creating a new Ledger](/en/products/midaz/console/creating-a-new-ledger). **3. Review Ledger settings** 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/products/midaz/console/creating-a-new-ledger#ledger-settings), or update them later from [Managing Ledgers](/en/products/midaz/console/managing-ledgers-via-console#ledger-settings). **4. Create Assets** Assets define a unit of value for the Ledger, such as `BRL`, `USD`, or a custom token. Accounts reference an Asset when you create them. Go to [Creating an Asset](/en/products/midaz/console/creating-an-asset). **5. Create Account Types** Define Account Types when your configured Account or Route rules need those classifications. Go to [Creating an Account Type](/en/products/midaz/console/creating-an-account-type). **6. Create Accounts** Accounts hold balances and are the source or destination of transactions. Go to [Creating an Account](/en/products/midaz/console/creating-an-account). **7. Add Portfolios, Segments, Holders, and Aliases when needed** These are persisted support structures. Configure them when your workflow needs ownership context, grouping, fee resolution, or scoped rules. Use: * [Creating a Portfolio](/en/products/midaz/console/creating-a-portfolio) * [Creating a Segment](/en/products/midaz/console/creating-a-segment) * [Creating a Holder](/en/products/midaz/console/crm-creating-a-holder) * [Creating an Alias Account](/en/products/midaz/console/crm-creating-alias-account) **8. Create Accounting Routes** Accounting Routes define optional participant and posting rules. Midaz applies them when route validation is on. Go to [Creating an Accounting Route](/en/products/midaz/console/creating-an-accounting-route). **9. Create Transactions** Create Transactions after the Ledger has an Asset and the required Accounts. You need a matching Accounting Route only when route validation is on. Go to [Creating a Transaction](/en/products/midaz/console/creating-a-transaction). **10. Configure Fees Engine after core setup is stable** Fees Engine is part of Midaz, but it is optional. Enable it under **Settings > Plugins**, then configure its fee packages after the base ledger model is already clear. Go to [Midaz module settings](/en/platform/console/plugin-settings). ## Fast path by goal *** > **[I need my first working ledger](/en/products/midaz/console/midaz-console-onboarding)** > > Use onboarding, then create at least one Asset and one Account. > **[I need to model account structure](/en/products/midaz/console/creating-an-account-type)** > > Start with Ledger settings, Account Types, Accounts, and Segments. > **[I need to control how money moves](/en/products/midaz/console/creating-an-accounting-route)** > > Create the accounts first, then define Accounting Routes and test with a transaction. > **[I need to estimate fees](/en/products/midaz/fees/console/managing-fee-calculations)** > > Enable Fees Engine under **Settings > Plugins**, then configure it after the core ledger structure is already in place. ## Common sequencing mistakes *** > **Creating an Account before creating an Asset** > > You must link every Account to an Asset. If the Asset does not exist yet, account creation stalls immediately. > **Enabling Account Type validation before defining Account Types** > > The Type field then lists only registered Account Types, so the list is empty. Create the Account Types first or disable validation until they are ready. > **Designing Accounting Routes before knowing the real accounts** > > Users often try to model routes too early. Route rules are easier to define after the treasury, customer, fee, and settlement accounts already exist. > **Trying to create Transactions before aliases or accounts are ready** > > 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. > [!NOTE] > > 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/products/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, including any `@` prefix. > **[Setup path](/en/products/midaz/console/midaz-console-setup-path)** > > Review the recommended setup order before you change individual screens. ## Common issues *** > **I cannot see the Account Types menu** > > The **Account Types** menu appears when you enable **Validate Account Type** in the current Ledger settings. > > To fix it: > > 1. Open **Foundation > Ledgers**. > > 2. Select **Edit** for the target Ledger. > > 3. Go to the **Settings** tab. > > 4. Enable **Validate Account Type**. > > 5. Return to the sidebar and open **Foundation > Account Types**. > > > [!WARNING] > > > > After you enable this setting, new Accounts must use a valid Account Type. Create the required types before you ask operators to create accounts. > **I cannot create an Account because the Asset field is empty** > > 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/products/midaz/console/creating-an-asset), then return to [Creating an Account](/en/products/midaz/console/creating-an-account). > **The Account Type dropdown is empty** > > 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/products/midaz/console/creating-an-account-type). If your workflow does not need type validation, disable **Validate Account Type** in Ledger settings. > **I cannot find an Account when creating a Transaction** > > Transaction creation searches accounts by alias. Confirm that: > > * The account exists in the selected Ledger. > * The alias matches the registered alias, including any `@` 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/products/midaz/console/managing-accounts). > **My Transaction fails route validation** > > 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/products/midaz/console/managing-accounting-routes) and then review [Creating an Accounting Route](/en/products/midaz/console/creating-an-accounting-route). > **My Transaction amounts do not balance** > > 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. > **I cannot delete an entity** > > 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. > **Balances do not look right after a Transaction** > > 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 status: `APPROVED`, `PENDING`, `CANCELED`, or `NOTED` (an annotation with no balance change). A reversal is a separate transaction. The original stays `APPROVED`. > > 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: > **[List Accounts](/en/reference/products/midaz/v2/list-accounts)** > > Confirm whether the account exists in the selected Ledger. > **[Retrieve an Account](/en/reference/products/midaz/v2/get-account-by-id)** > > Inspect account details such as type, asset, and metadata. > **[List Transactions](/en/reference/products/midaz/v2/get-all-transactions)** > > Review transaction history for a Ledger. > **[Retrieve a Transaction](/en/reference/products/midaz/v2/get-transaction)** > > Inspect transaction status and operation details. # About Midaz module Source: https://docs.lerian.studio/en/products/midaz/console/midaz-module Use the Midaz Module in Lerian Console to 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. > [!NOTE] > > 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 *** Start with these pages if you are new to Midaz. They show what to create and in which order. They also show how the main components work together. > **[Setup path](/en/products/midaz/console/midaz-console-setup-path)** > > Follow the recommended setup order for Organizations, Ledgers, Assets, Accounts, Routes, and Transactions. > **[Midaz Console concepts map](/en/products/midaz/console/midaz-console-concepts-map)** > > Learn how Midaz components connect and how they support financial operations. > **[Midaz Console troubleshooting](/en/products/midaz/console/midaz-console-troubleshooting)** > > 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 * **Configure Fees Engine**: Fees Engine is integrated into Midaz, but it is optional. Enable it from the [Settings > Plugins](/en/platform/console/plugin-settings) page before you configure it ## Recommended setup order *** For a new environment, we recommend configuring Midaz in the following order: **1. Create an Organization** Define the top-level structure that groups your ledgers and operations. **2. Create a Ledger** Create the ledger that will store balances, accounts, and transactions. **3. Configure Ledger settings** Adjust operational and accounting settings for the ledger. **4. Create Assets** Define the currencies or asset types that will circulate in the ledger. **5. Configure Account Types** Define how accounts behave within the financial structure. **6. Create Accounts** Create the accounts that will hold balances and participate in transactions. **7. Configure Portfolios and Segments** Organize accounts based on business or operational needs. **8. Create Accounting Routes** Define how transactions move assets between accounts. **9. Create Transactions** Start recording and monitoring financial operations. **10. Configure Fees Engine** Fees Engine is integrated into Midaz, but it is optional. Enable it under **Settings > Plugins**, then configure its fee packages after the core ledger structure is in place. For a more detailed explanation of each step, see [Setup path](/en/products/midaz/console/midaz-console-setup-path). ## Accessing Midaz *** To open Midaz, select the **Midaz Module** from the Lerian Console product launcher (*Figure 1*). ![The Midaz Module card in the Lerian Console product launcher, used to open Midaz](https://mintcdn.com/lerian-49cb71fc/yneNeAhnXdrrmE3_/images/en/console/midaz_button.png?fit=max\&auto=format\&n=yneNeAhnXdrrmE3_\&q=85\&s=237682e7534e9c4fe11cefe3379e2437) Figure 1. The card used to access the Midaz Module. ## Ledger activity *** The Midaz home page shows a **Ledger activity** section for the current Ledger. Select the period at the top of the section: **Last 7 days**, **Last 30 days**, or **Last 90 days**. The default is the last 30 days. The section has three cards and a chart: * **Transactions in period**: the number of transactions in the period, split by status. * **Settled volume**: the settled amount of each asset, with its number of transactions. When an asset has reversals, a line shows their amount. * **Current position**: the available amount of each asset, the number of accounts that hold it, and the amount on hold. The period does not change this card. * **Transactions per Day**: a chart of the number of transactions on each day of the period. With Access Manager, this section needs the read permission on the Midaz dashboard resource. ## 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**. > **[Foundation](/en/products/midaz/console/mc-foundation)** > > Configure Organizations, Ledgers, Assets, and Account Types that support your financial structure. > **[Accounting](/en/products/midaz/console/mc-accounting)** > > Configure Accounting Routes that control how assets move through the system. > **[Accounts](/en/products/midaz/console/mc-accounts)** > > Manage Accounts, Holders, Aliases, Segments, and Portfolios. > **[Transactions](/en/products/midaz/console/mc-transactions)** > > Create and monitor transactions that transfer assets between accounts. > **[Plugins](/en/platform/console/mc-console-plugins)** > > Manage optional platform plugins available from Lerian Console. ## Migrating from Midaz Console *** If you're currently using the standalone **Midaz Console**, here's what changes when moving to Lerian Console. > [!WARNING] > > Midaz v3.5.0 and later releases do not include the standalone Midaz Console. > > 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 # Instruments Source: https://docs.lerian.studio/en/products/midaz/crm/alias-accounts Associate Holders with Ledger Accounts and store banking, regulatory, metadata, and related-party information. An **Instrument** is the CRM record that associates a [Holder](/en/products/midaz/crm/holders) with a Ledger Account in Midaz. It stores the `ledgerId` and `accountId` for that association, together with banking details, regulatory fields, metadata, and related parties. Instruments keep CRM context separate from the transactional Ledger. The Ledger remains the balance and movement record. The Instrument holds the context associated with a Holder and its Ledger Account. ## Create an Instrument Create an Instrument in the scope of a Holder. The creation input requires `ledgerId` and `accountId` for the associated Ledger Account. The ledger and the account must exist in the organization. The `bankingDetails` object can contain `branch`, `account`, `type`, `openingDate`, `closingDate`, `iban`, `countryCode`, and `bankId`. Its individual fields are optional. Midaz rejects banking details whose `bankId`, `branch`, and `account` another Instrument in the organization already holds. An Instrument response includes its `id`, `holderId`, `ledgerId`, `accountId`, `document`, `type`, timestamps, and any configured banking, regulatory, metadata, or related-party data. To open the Ledger Account and its Instrument in one call, use `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/holders/{id}/accounts`. The Holder in the path owns the new Account. The body takes the Account fields and, optionally, `bankingDetails`, `regulatoryFields`, and `relatedParties`. When the body has none of these three, Midaz creates only the Account. A `201` response carries `account` and `instrument`. When Midaz creates the Account but cannot write the Instrument, `instrument` is `null` and `instrumentError` carries `status` and `reason`. The Account stays, and you can create its Instrument separately. ## Regulatory fields and related parties Use `regulatoryFields.participantDocument` to store the participant document when it applies. `regulatoryFields.accountType` accepts `DEPOSIT`, `SAVINGS`, `INVESTMENT`, `OTHER_FINANCIAL_INVESTMENTS`, `NON_RESIDENT`, or `PAYMENT`. An Instrument can include related parties. Each related party requires `document`, `name`, `role`, and `startDate`. The `role` is `PRIMARY_HOLDER`, `LEGAL_REPRESENTATIVE`, or `RESPONSIBLE_PARTY`. The `id` and `endDate` fields are optional, and `endDate` must not be before `startDate`. ## Manage Instruments The Midaz API lets you: * List Instruments for an organization. * Create an Instrument for a Holder. * Retrieve, update, or delete an Instrument in the scope of its Holder. * Delete a related party from an Instrument. Use the [Midaz API reference](/en/reference/products/midaz/v2/list-instruments) for the current request and response schema. ## Next steps > **[Holders](/en/products/midaz/crm/holders)** > > Learn about the Holder associated with an Instrument. > **[Accounts](/en/products/midaz/accounts)** > > Learn how Midaz Ledger Accounts hold balances and movements. # Best practices Source: https://docs.lerian.studio/en/products/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/products/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. > [!WARNING] > > Make sure your identifiers are correct before you create or link records. CRM checks that the `ledgerId` and `accountId` of an Instrument exist in the organization. ## 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. * **Pass the keys to the deployment from your secret vault.** To configure them on a Midaz deployment, see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). For the full list of protected fields and encryption strategies, see [CRM data security](/en/products/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/products/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. Instead: * Route CRM traffic through your **backend services** or an internal API layer. * Use [Access Manager](/en/platform/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** (`hard_delete=true`): CRM removes the record from its database. For most cases, soft delete is the safer choice. It preserves audit trails. Reserve hard delete for cases where regulations demand complete removal (for example, GDPR right-to-erasure requests). > [!NOTE] > > 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 is a **neutral, persistent data layer**. It does not 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. * 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/products/midaz/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 your deployment configuration before any major upgrade. For upgrade procedures, see the [Helm upgrade guide](https://github.com/LerianStudio/helm/tree/main/charts/midaz). ## 8. Monitor database health and performance *** CRM uses MongoDB for data storage. In production: * **Monitor connection pool usage.** The Helm chart sets `MONGO_CRM_MAX_POOL_SIZE` to 1000 by default. 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/products/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/products/midaz/crm/crm-core-concepts-overview Meet the two CRM entities that connect customer identity to Midaz Ledger accounts: Holders that carry identity and Instruments that link them to 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/products/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. * [**Instruments**](/en/products/midaz/crm/alias-accounts): The link between a Holder and a specific ledger account. Instruments let one Holder associate accounts in different ledgers. Each one keeps banking details and metadata in one place. # CRM data security Source: https://docs.lerian.studio/en/products/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. ### Personal data under GDPR and LGPD 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 *** 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. The split of security work between Lerian and you depends on your deployment model. See the [shared responsibility model](/en/products/midaz/security). Still, Lerian delivers technology that follows strong, market-aligned security practices. These practices protect sensitive CRM data as follows: ### In transit The Midaz ledger serves plain HTTP. Terminate TLS in front of it, at your ingress or service mesh, to protect the data 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 In BYOC, disk and volume encryption is your responsibility. We strongly recommend it. It adds another 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** creates a search token for some encrypted fields, such as the Holder `document`, so the CRM can find a record by that value. The field itself stays encrypted. > [!WARNING] > > The CRM writes the protected fields to the database only in encrypted form. ### Encryption and hashing strategies The CRM combines several cryptographic techniques. 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 the encryption key. 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. ### Key management The CRM uses encryption keys and hashing keys, in one of two modes that `KMS_VENDOR` selects. When `KMS_VENDOR` is unset or `none`, the CRM reads its keys from `LCRYPTO_ENCRYPT_SECRET_KEY` and `LCRYPTO_HASH_SECRET_KEY`. When `KMS_VENDOR` is `hashicorp-vault`, the CRM generates keys for each organization and wraps them with a HashiCorp Vault Transit key that you operate. Never hardcode a key or expose it in source code or version control. With `hashicorp-vault`, the CRM uses the Vault Transit engine mounted at `transit-st`. Each Organization has its own Transit key, named `org-`. Midaz signs in to Vault with AppRole: set `KMS_VAULT_AUTH_METHOD` to `approle`, and set `KMS_VAULT_ROLE_ID` and `KMS_VAULT_SECRET_ID`. Give that AppRole permission to encrypt and decrypt with the Organization keys on `transit-st`. Midaz provisions the CRM keys of an Organization when the Organization first needs them. To provision them in advance, call `POST /v2/organizations/{organization_id}/encryption/provision`. To check them, call `GET /v2/organizations/{organization_id}/encryption/status`. > [!CAUTION] > > Protect your keys with a dedicated secret manager or secure storage. ### 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. > [!CAUTION] > > Never store sensitive information in the `metadata` object. #### Holder * `name` * `document` * `contact.primaryEmail` * `contact.secondaryEmail` * `contact.mobilePhone` * `contact.otherPhone` * `naturalPerson.motherName` * `naturalPerson.fatherName` * `naturalPerson.monthlyGrossIncome.value` * `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 protected fields stay unreadable in their 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. > [!WARNING] > > 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/products/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/products/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/products/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. > [!NOTE] > > 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/products/midaz/v2/create-holder). > **Individual holder example** > > ```bash theme={null} > curl -X POST http://localhost:3002/v2/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" > } > }' > ``` > **Company holder example** > > To register a company instead of an individual, set the type to `LEGAL_PERSON`: > > ```bash theme={null} > curl -X POST http://localhost:3002/v2/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" > } > } > }' > ``` > [!TIP] > > Save the `id` 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/products/midaz/v2/create-instrument). > **Example request** > > ```bash theme={null} > curl -X POST http://localhost:3002/v2/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 /v2/organizations/{organization_id}/holders/{holder_id}` | [Retrieve a holder](/en/reference/products/midaz/v2/get-holder-by-id) | | List all holders | `GET /v2/organizations/{organization_id}/holders?limit=10&page=1` | [List holders](/en/reference/products/midaz/v2/list-holders) | | List instruments for a holder | `GET /v2/organizations/{organization_id}/instruments?holder_id={holder_id}` | [List instruments](/en/reference/products/midaz/v2/list-instruments) | | Retrieve a specific instrument | `GET /v2/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}` | [Retrieve an instrument](/en/reference/products/midaz/v2/get-instrument-by-id) | | Update a holder | `PATCH /v2/organizations/{organization_id}/holders/{holder_id}` | [Update a holder](/en/reference/products/midaz/v2/update-holder) | > **Update holder example** > > ```bash theme={null} > curl -X PATCH http://localhost:3002/v2/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" > } > }' > ``` ## Step 4: Clean up *** To remove resources, delete instruments first, then holders. Midaz refuses to delete a holder that still has instruments or accounts that carry its ID. | Operation | Endpoint | API reference | | - | - | - | | Delete an instrument | `DELETE /v2/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id}` | [Delete an instrument](/en/reference/products/midaz/v2/delete-instrument) | | Delete a holder | `DELETE /v2/organizations/{organization_id}/holders/{holder_id}` | [Delete a holder](/en/reference/products/midaz/v2/delete-holder) | > **Example requests** > > Delete an instrument: > > ```bash theme={null} > curl -X DELETE http://localhost:3002/v2/organizations/{organization_id}/holders/{holder_id}/instruments/{instrument_id} > ``` > > Delete a holder: > > ```bash theme={null} > curl -X DELETE http://localhost:3002/v2/organizations/{organization_id}/holders/{holder_id} > ``` ## Next steps *** > **[CRM API reference](/en/reference/products/midaz/v2/create-holder)** > > Explore advanced filtering, metadata queries, and all available endpoints. > **[Using CRM with Midaz Console](/en/products/midaz/crm/using-crm-with-midaz-console)** > > Manage holders through a graphical interface. # CRM migration history Source: https://docs.lerian.studio/en/products/midaz/crm/crm-migration-guide Historical context for moving standalone CRM data to the Midaz v4 Ledger. > [!WARNING] > > This page documents a legacy topology. Do not use it as a deployment guide for Midaz v4. Earlier installations could run CRM separately. Midaz v4 embeds CRM in the Ledger: use Holder and Instrument endpoints only through `/v2/organizations/{organization_id}/...`. The Ledger is the current deployment unit. CRM is not a plugin, standalone service, or separate v4 release. ## What a v4 installation does not use Do not use the standalone CRM service, port `4003`, the `X-Organization-Id` header, or the legacy CRM v1 alias endpoints in a v4 installation. Fees in Midaz v4 runs in the Ledger and is available only on `/v2`. If you retire an older standalone CRM deployment, retain its MongoDB backup and release values. Then reconcile Holder and Instrument records through the Ledger `/v2` API before you decommission the old workload. Preserve that release record. See [CRM getting started](/en/products/midaz/crm/crm-getting-started) and the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). # What is CRM? Source: https://docs.lerian.studio/en/products/midaz/crm/crm-overview The optional CRM layer adds customer identity, contact details, and business context to Midaz Ledger accounts, outside the ledger. CRM is not part of the ledger's transactional domain. It adds business attributes to ledger accounts. 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. ## Current API *** CRM is served by the Ledger only on `/v2`, under `/v2/organizations/{organization_id}/...`. `/v1` does not expose CRM. Use the `midaz` authorization namespace with the `holders` and `instruments` resources. > [!TIP] > > **Test CRM locally** > > You can run CRM locally, without Kubernetes, with the Midaz stack from [Getting started](/en/products/midaz/midaz-getting-started). ## 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. > [!TIP] > > 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/products/midaz/crm/holders)** – The main entity in CRM. It represents the persona for a Midaz ledger account. It stores identity attributes and individual traits. * **[Instrument](/en/products/midaz/crm/alias-accounts)** – The business context for a [Ledger Account](/en/products/midaz/accounts) in **Midaz Ledger**. It holds financial details such as bank account information. *Figure 1* shows how the CRM entities connect to Midaz. ![How CRM entities, the Holder and the Instrument, connect to accounts in the Midaz ledger](https://mintcdn.com/lerian-49cb71fc/svQbQMf9Iz90q0Cc/images/en/d2/entities-new.svg?fit=max\&auto=format\&n=svQbQMf9Iz90q0Cc\&q=85\&s=87a301718533b124568d374140b17cea) Figure 1. How CRM entities connect to Midaz ## Design principles *** CRM focuses on: * **Separation of concerns**: It keeps user profile data out of transaction logic. * **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. > [!CAUTION] > > CRM does not validate 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 or grouping logic used in Midaz accounts. * It gives consistent access to holder metadata without loading the transaction layer. ## Security & compliance *** In BYOC, you run Midaz and its components in your own infrastructure. 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 serves plain HTTP, so terminate TLS at your ingress or service mesh. * **Data at rest**: Midaz encrypts the protected CRM fields before it stores them. > [!CAUTION] > > You own encryption key management. Use a secret-management service to store 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/products/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, CRM removes the record from its database. Your policies define the behavior. ## Access control *** To restrict who can access your CRM data, enable our [Access Manager](/en/platform/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. > [!TIP] > > Access Manager is available under the Enterprise model. > To evaluate it, [contact our team](https://lerian.studio/contact). > [!TIP] > > See [Getting started with CRM](/en/products/midaz/crm/crm-getting-started) for a step-by-step guide. ## Next steps *** > **[Core concepts](/en/products/midaz/crm/crm-core-concepts-overview)** > > Understand the key entities managed by CRM: Holders and Instruments. > **[Explore the CRM API](/en/reference/products/midaz/v2/create-holder)** > > Browse endpoints for holders, instruments, and related parties. > **[Using CRM](/en/products/midaz/crm/crm-using-overview)** > > Learn how to register holders and link them to ledger accounts. # Using CRM Source: https://docs.lerian.studio/en/products/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/products/midaz/crm/crm-getting-started): Manage Holders and Instruments through the REST API. * [**Using CRM with Midaz Console**](/en/products/midaz/crm/using-crm-with-midaz-console): Manage Holders and Instruments through the Midaz Console graphical interface. # Holders Source: https://docs.lerian.studio/en/products/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/products/midaz/crm/crm-overview) manages Holders. Holders do not belong to the ledger's transactional domain. They enrich ledger accounts with business data. Each Holder is one of two types: **Natural Person** (individual) or **Legal Person** (company/organization). > [!TIP] > > To create and manage holders step by step, see [Getting started with CRM](/en/products/midaz/crm/crm-getting-started). ## Holder types *** 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 > [!TIP] > > 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` | No | Primary address line (street, number). Max 256 characters. | | **line2** | `string` | No | Secondary address line (apartment, suite). Max 256 characters. | | **zipCode** | `string` | No | Postal or ZIP code. Max 20 characters. | | **city** | `string` | No | City name. Max 100 characters. | | **state** | `string` | No | State or province code. Max 100 characters. | | **country** | `string` | No | 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 | 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. | | **monthlyGrossIncome** | `object` | No | Monthly gross income, with `value`, `currency`, and `referenceDate`. All three are required inside the object. | | **status** | `string` | No | Lifecycle status (e.g., `Active`, `Inactive`, `Suspended`, `Closed`). | ### Legal Person fields | 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). | | **annualGrossRevenue** | `object` | No | Annual gross revenue, with `value`, `currency`, and `referenceDate`. All three are required inside the object. | | **totalAssets** | `object` | No | Total assets, with `value`, `currency`, and `referenceDate`. All three are required inside the object. | | **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. > [!WARNING] > > 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/products/midaz/crm/crm-data-security). ## Managing Holders *** ### Via API Use the CRM API to manage Holders programmatically: * [Create a Holder](/en/reference/products/midaz/v2/create-holder): Register a new Holder in the system. * [List Holders](/en/reference/products/midaz/v2/list-holders): View all Holders with pagination and filters. * [Retrieve a Holder](/en/reference/products/midaz/v2/get-holder-by-id): Get the details of a specific Holder. * [Update a Holder](/en/reference/products/midaz/v2/update-holder): Edit an existing Holder's data. * [Delete a Holder](/en/reference/products/midaz/v2/delete-holder): Soft-delete or permanently remove a Holder. Midaz refuses while the Holder still has Instruments or accounts that carry its ID. > [!NOTE] > > 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), 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 lets you create, view, edit, and delete Holders without code. [**Learn more in the Managing Holders guide.**](/en/products/midaz/console/managing-crm-holders) ## Next steps *** > **[Instruments](/en/products/midaz/crm/alias-accounts)** > > Learn how Instruments link Holders to Midaz ledger accounts. > **[Using CRM](/en/products/midaz/crm/crm-using-overview)** > > Follow the step-by-step guide to register Holders and create Instruments. # CRM in Lerian Console Source: https://docs.lerian.studio/en/products/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. 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. ## 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** - Link holders to ledger accounts on the **Alias Accounts** page ### Available actions From the Midaz Module, you can: > **[Manage Holders](/en/products/midaz/console/managing-crm-holders)** > > Create and manage individuals or companies associated with your accounts > **[Manage Alias Accounts](/en/products/midaz/console/managing-crm-alias-accounts)** > > Link holders to Midaz ledger accounts with business context # Database migrations Source: https://docs.lerian.studio/en/products/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 do not migrate at startup. They boot against a schema that the runner has already migrated. > [!NOTE] > > This model is what runs under `make up` in local development and under the Helm chart in Kubernetes. If you use `make up`, you need no manual step. The compose stack gates the app on the migration runner. ## Why a dedicated runner *** The dedicated runner has these properties: * Migrations run once, as a separate step. * The app image does not bundle migration SQL and does not change the schema. * The runner image runs under a hardened security context (see [Security posture](#security-posture)). ## Runner images *** Each release ships two images: | 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), at a pinned version. * 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. Postgres tracks migration progress in the `schema_migrations` table, 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 prints phase markers (`applying onboarding migrations`, `applying transaction migrations`, `applying tracer migrations`, `migrations complete`). ## Local development *** ### `make up` From the repository root, `make up`: 1. Starts infra and waits until Postgres, MongoDB, Valkey, and RabbitMQ are ready. 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 Each component exposes host-side Make targets that run migrations directly against a local database, for example when you develop a new migration. The targets install the pinned `golang-migrate` CLI when `migrate` is absent, 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 the last migration on both databases | Tracer (`components/tracer`): | Target | Effect | | - | - | | `make migrate` | Applies the tracer schema | | `make migrate-down` | Rolls back the last tracer migration | ### Example: run the Ledger runner manually You rarely need to invoke the runner image by hand, because 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=midaz-postgres-primary \ -e DB_ONBOARDING_PORT=5701 \ -e DB_ONBOARDING_USER=midaz \ -e DB_ONBOARDING_PASSWORD=lerian \ -e DB_ONBOARDING_NAME=onboarding \ -e DB_ONBOARDING_SSLMODE=disable \ -e DB_TRANSACTION_HOST=midaz-postgres-primary \ -e DB_TRANSACTION_PORT=5701 \ -e DB_TRANSACTION_USER=midaz \ -e DB_TRANSACTION_PASSWORD=lerian \ -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 that matter if you author your own manifests. > [!NOTE] > > The runner migrates the databases pointed at by its environment, once. ### Security posture The runner works under 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. You control TLS for migrations through `sslmode` alone, 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 use a pinned `golang-migrate` version. The host `make` targets install the same version when `migrate` is absent. ## Upgrading from earlier versions *** 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` gates the apps on the one-shot migrate services. ## Related pages *** * [Installing Midaz](/en/products/midaz/midaz-setup): full local setup with `make up`. * [Updating Midaz](/en/products/midaz/updating-midaz): upgrade flows for local and Helm. * [Deployment strategies](/en/products/midaz/deployment): BYOC deployment overview. * [Tracer environment variables](/en/products/tracer/tracer-environment-variables): full Tracer configuration reference. # Deployment strategies Source: https://docs.lerian.studio/en/products/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/start-here/getting-started) to begin configuring your platform. > [!NOTE] > > Not sure which model fits? See [Deployment models](/en/start-here/evaluate-and-deploy/deployment-models) for a full comparison of SaaS and BYOC. Set up your environment, meet the prerequisites, and follow our best practices. > [!TIP] > > Before you deploy, check out the [Midaz Architecture Overview](/en/products/midaz/architecture). ### Content * [Midaz Terraform Foundation](/en/products/midaz/midaz-terraform-foundation): Ready-made Terraform examples for provisioning the cloud infrastructure Midaz runs on (AWS, GCP, or Azure). To install, configure, and upgrade Midaz with its chart, see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). ## Deployment strategies *** You manage the infrastructure in both Midaz editions, Community and Enterprise. We strongly recommend using Kubernetes with our Helm Charts. This approach simplifies deployment and scaling. You can choose the deployment strategy that fits your needs. ### Deploying with Kubernetes We recommend our official Helm Charts. They 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. For the step-by-step installation, see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). ### 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, with a different orchestrator or directly on virtual machines or bare metal. For instructions, check the [Installing Midaz](/en/products/midaz/midaz-setup) page. ## Platform requirements *** The default Helm configuration meets these minimums, except for RabbitMQ. The bundled RabbitMQ server runs below the 4.x target. If you bring your own infrastructure components, make sure you meet the minimum supported versions: | Service | Minimum Supported Version | Notes | | :- | :- | :- | | Kubernetes | 1.23+ | 1.30+ recommended | | Helm CLI | 3.8+ | Required for OCI registry | | PostgreSQL | 17 | Primary database | | MongoDB | 8.x | Document storage | | RabbitMQ | 4.x | Message broker | | Valkey | 8.x | Live balances (Redis fork) | Midaz needs these minimum versions for a stable, performant platform. ## 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), which holds the live balances. ### Observability * Integrate tools that monitor logs, traces, and metrics. * Midaz supports observability through OpenTelemetry. ## 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/products/midaz/security) page. > [!TIP] > > For guidance on securing your environment before deployment, check the [Security Recommendations](/en/products/midaz/security-recommendations) documentation. # Fees Engine billing package examples Source: https://docs.lerian.studio/en/products/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. > [!TIP] > > 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/products/midaz/fees/fees-engine-overview#billing-packages) and the [API reference](/en/reference/products/midaz/v2/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 /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "Boleto Issuance — Tiered", "description": "Monthly volume billing for boleto issuance with progressive tiers", "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 /v2/organizations/{organization_id}/ledgers/{ledger_id}/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** for 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. A PF segment groups all personal accounts. ### Package configuration ```json theme={null} POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "PF Account Maintenance", "description": "Monthly maintenance fee for active personal accounts", "type": "maintenance", "enable": true, "feeAmount": "9.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pf", "accountTarget": { "segmentId": "0198e5f4-7b2a-7c3d-9e1f-2a3b4c5d6e7f" } } ``` ### 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** for 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 *** ### The business need A fintech charges R\$ 0.10 per Pix sent (flat rate, no tiers). ### Package configuration ```json theme={null} POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "Pix Send — Standard", "description": "Flat-rate billing per Pix sent", "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" } ``` ### How the calculation works **Example result** for 5,000 Pix transactions: | Detail | Value | | :- | :- | | Total Pix sent | 5,000 | | Unit price | R\$ 0.10 | | **Total** | **R\$ 500.00** | ## 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 /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "PJ Maintenance — PME", "description": "Monthly maintenance for PME business accounts", "type": "maintenance", "enable": true, "feeAmount": "29.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pj", "accountTarget": { "portfolioId": "0198e5f4-8c1d-7a2b-9f3e-4d5c6b7a8e9f" } } ``` **Corporate portfolio (R\$ 89.90/month):** ```json theme={null} POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "PJ Maintenance — Corporate", "description": "Monthly maintenance for Corporate business accounts", "type": "maintenance", "enable": true, "feeAmount": "89.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pj", "accountTarget": { "portfolioId": "0198e5f4-9d2e-7b3c-8a4f-5e6d7c8b9a0e" } } ``` ### How the calculation works A single call to `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing/calculate` with `"type": "maintenance"` processes both packages. The response includes one result per package and a consolidated summary. **Example result** for 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, so revenue stays consolidated. The orchestrator sends each transaction payload to Midaz independently. ## Next steps *** > **[Create a billing package](/en/reference/products/midaz/v2/create-billing-package)** > > API reference for creating volume and maintenance billing packages. > **[Calculate billing](/en/reference/products/midaz/v2/calculate-billing)** > > Trigger billing calculations for a period and retrieve transaction payloads. > **[Billing calculations](/en/products/midaz/fees/fee-engine-calculation#billing-calculations)** > > Detailed mechanics of tiered pricing, free quotas, and maintenance billing. > **[Best practices](/en/products/midaz/fees/fees-engine-best-practices)** > > Operational guidance for billing packages in production. # Connect the Fees Engine to Midaz Source: https://docs.lerian.studio/en/products/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. Each fee request must provide `send.asset`. Midaz rejects an empty value instead of using a fallback currency. See [Midaz environment variables](/en/products/midaz/midaz-environment-variables) for ledger configuration. ## See it in Lerian *** * The full integration journey across the pieces: [Building a complete core banking](/en/products/building-a-complete-core-banking) * What the Fees Engine does: [What is Fees Engine?](/en/products/midaz/fees/fees-engine-overview) * Deploy the stack: [Helm charts](https://github.com/LerianStudio/helm) # Creating a Fee Package Source: https://docs.lerian.studio/en/products/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 *** **Step 1** Go to the **Fee Packages** page and click the **New Package** button. **Step 2** The **New Fee Package** form will open with multiple sections to configure. > [!TIP] > > Check the field guide for more information about the fields in the form. **Step 1** Fill in the **Basic Information** section **Step 2** Set **Minimum Amount** and **Maximum Amount** in the same section. > [!NOTE] > > Transactions outside this range will not have fees applied from this package. **Step 3** Click the **Next** button. **Step 4** Select one of the options in the **Add Fee** section: * **Flat Fee** - Fixed amount regardless of transaction value. * **Percentage Fee** - Percentage of the transaction amount. * **Max between types** - Higher value between flat and percentage. **Step 5** Add Fee information. **Step 6** (Optional) You can add another fee rule if needed. To do so, select a new fee type and fill the information. **Step 7** Click the **Next** button. **Step 8** (Optional) Configure **Account Waivers** section: * Add account aliases that should be exempt from fees in this package * Click **Add** to add each waived account **Step 9** Click the **Next** button. **Step 10** Review the information and confirm that everything is correct. **Step 11** 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 | > [!NOTE] > > 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` | ### Account Waivers | Field | Description | Example | | - | - | - | | Account Alias | The alias of the account that will be waived. | `@customer_john_checking` | ## Priority, ranges and waived accounts *** ### Fee priority ordering When a package contains multiple fees, Midaz applies them in priority order (lowest number first). With **After Fees Amount** as the reference, earlier fees affect the calculation base for later fees. **Example**: * Fee A (Priority 1): 1% on original amount, not deductible * Fee B (Priority 2): 0.5% on after fees amount On a \$100 transaction: * Fee A = \$1.00 (1% of \$100) * Fee B = \$0.505 (0.5% of \$101) ### Amount range considerations * Midaz rejects a package whose range overlaps another package with the same transaction route and segment * 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 * When every source account is waived, the package charges no fee. Otherwise, the fee is split among the accounts that are not waived: sources for a fee added on top, destinations for a deductible fee. ## What to do next *** After creating the Fee Package, run a test calculation in [Running a Fee Estimation](/en/products/midaz/fees/console/fe-fee-simulation) before using it with live Transactions. > **API equivalent** > > [Create Package endpoint](/en/reference/products/midaz/v2/create-package) # Deleting a Fee Package Source: https://docs.lerian.studio/en/products/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. > [!CAUTION] > > 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 **Step 1** On the **Fee Packages** page, click the three dots () in the **Actions** column for the package you want to delete. **Step 2** Select **Delete**. **Step 3** Review the confirmation dialog. **Step 4** Click **Delete Package** to finalize the deletion. # Editing a Fee Package Source: https://docs.lerian.studio/en/products/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 **Step 1** On the **Fee Packages** page, find the package you want to edit. **Step 2** Click the three dots () in the **Actions** column and select **Edit**. **Step 3** The creation wizard opens with the current values. **Step 4** Make your changes and click **Save**. **Step 5** A confirmation toast appears and the updated package returns to the list. # Running a Fee Estimation Source: https://docs.lerian.studio/en/products/midaz/fees/console/fe-fee-simulation Run a fee estimation with the Console Fee Calculator to preview charges on sample amounts, accounts, and assets 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/products/midaz/fees/console/fe-creating-a-package) saved. * You need the source and destination Account aliases you want to test against. ## Running an estimation *** **Step 1** In the Midaz Module sidebar, expand **Fees Engine** and click **Calculator**. **Step 2** Fill in the estimated transaction. See the [Field guide](#field-guide) below for each input. **Step 3** Click **Calculate Fees** to run the estimation. **Step 4** Review the results displayed below the form. ## Field guide *** | Field | What to enter | Example | | - | - | - | | Transaction Amount | Total value to estimate fees against. | `100.00` | | Asset | One of the assets of the current ledger. Select it in the list. | `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` | > [!NOTE] > > 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: ### Totals * **Payer paid** - The amount the source sends, including fees added on top * **Total fees** - The sum of all fees for the transaction * **Recipient received** - **Payer paid** minus **Total fees** ### Fee breakdown For each fee rule applied, you'll see: | Field | Description | | - | - | | **Fee Label** | Name identifying the fee | | **Source Pays** or **Destination Pays** | Who pays the fee. The source pays a fee added on top. The destination pays a deductible fee. | | **Calculation Type** | **Flat**, **Percentage**, or **Max Between** | | **Calculated Amount** | The actual fee value | ## Estimation examples *** > **Simple flat fee estimation** > > **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 fees: \$2.00 > * Recipient received: \$98.00 if the fee is deductible. If it is not deductible, Payer paid is \$102.00 and Recipient received is \$100.00. > **Percentage fee estimation** > > **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 fees: \$7.50 (1.5% of \$500) > * Calculation: 500 × 0.015 = 7.50 > **Combined fee estimation** > > **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) > **Multiple fees estimation** > > **Scenario**: Package with two fees applied in priority order > > **Fee A** (Priority 1): 1% on original amount, not deductible > **Fee B** (Priority 2): 0.5% on after fees amount > > **Input**: \$1000 transaction > > **Expected Result**: > > * Fee A: \$10.00 (1% of \$1000) > * Fee B: \$5.05 (0.5% of \$1010) > * Total fees: \$15.05 ## 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/products/midaz/fees/console/fe-editing-a-package), or create a real Transaction in [Creating a Transaction](/en/products/midaz/console/creating-a-transaction). > **API equivalent** > > [Estimate Fees endpoint](/en/reference/products/midaz/v2/estimate-fee-calculation) # Fees Engine in Lerian Console Source: https://docs.lerian.studio/en/products/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 four 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. * [**Billing Packages**](/en/products/midaz/fees/console/managing-billing-packages): create and edit volume and maintenance billing packages. * **Billing Runs**: preview the billing for a period. 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, either **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 in [Plugin settings](/en/platform/console/plugin-settings). **1. Open Plugin settings** From the Midaz Module sidebar, go to **Settings** > **Plugins**. **2. Enable Fees Engine** Toggle **Fees Engine** on. The **Fees Engine** menu item appears in the sidebar with **Fee Packages**, **Calculator**, **Billing Packages**, and **Billing Runs** sub-items. > [!NOTE] > > If you disable the Fees Engine later, the Console hides its menu. Midaz keeps your packages and still applies them to matching Transactions. ## Where to go next *** > **[Manage Fee Packages](/en/products/midaz/fees/console/managing-fee-packages)** > > Create, edit, and delete packages. Define rules, scope by ledger or segment, and set waivers. > **[Fee Calculator](/en/products/midaz/fees/console/managing-fee-calculations)** > > Estimate what a package would charge for a sample Transaction without running a real one. > **[Manage Billing Packages](/en/products/midaz/fees/console/managing-billing-packages)** > > Create volume and maintenance packages, and turn them on or off. > **[Billing Runs](/en/products/midaz/fees/console/billing-runs)** > > Preview the fees for a period before you apply them. # Managing Billing Packages Source: https://docs.lerian.studio/en/products/midaz/fees/console/managing-billing-packages Create, edit, activate, and delete volume and maintenance Billing Packages from the Midaz Console to set the periodic charges that Billing Runs calculate. A **Billing Package** sets a periodic charge in the [Fees Engine](/en/products/midaz/fees/console/fees-engine-overview). A package has one of two types: * **Volume**: charges per number of events in the period. It supports price tiers, a free quota, and discounts. * **Maintenance**: charges a fixed amount from each target account every period. > [!NOTE] > > Enable the Fees Engine in [Midaz module settings](/en/platform/console/plugin-settings) before you use this page. ## Accessing the Billing Packages page *** In the Midaz Module sidebar, expand **Fees Engine**. Select **Billing Packages**. The page lists the packages of the Ledger that you select in the organization switcher. The list shows the **Name**, **Type**, **Asset**, and **Status** of each package. Use the **Type** filter to show Volume or Maintenance packages. From the list, you can: * Turn the **Status** switch on or off to activate or deactivate a package. * Select **Details** in the row menu to see the package configuration. * Select **Edit** in the row menu to change the package. * Select **Delete** in the row menu to delete the package. The Console asks you to confirm. ## Creating a Billing Package *** **1. Choose the type** Click **New Package**. In **Choose the billing type**, select **Volume Package (Tiered)** or **Maintenance Package (Flat Fee)**. **2. Fill in the basic information** In **Type & Information**, enter the **Package Name** and an optional **Description**. Select the **Asset Code**. The package uses the Ledger that you select in the organization switcher. **3. Configure the package** In **Configuration**, set the fields for the package type. The next two sections describe these fields. **4. Review and save** In **Review**, check the summary. To change a section, use its edit button to go back to that step. Then select **Save**. A new package is active when you create it. The next billing run includes it. You can change the type until you save the package. If you change it after you fill in **Configuration**, the Console discards the **Configuration** data. ## Volume package fields *** | Section | What you set | | - | - | | **Event Filter** | The **Transaction Route** that defines which transactions the package counts, and the **Transaction Status** to count: **Approved** or **Pending**. | | **Pricing Model** | **Tiered**: events fill the tiers in order, and each tier has its own unit price. **Flat Tier**: one unit price applies to every event. | | **Price Tiers** | For **Tiered**, each row has **From (min)**, **To (max)**, and **Unit Price**. Each tier starts at the quantity after the maximum of the previous tier. The last tier has no maximum. For **Flat Tier**, you set one unit price. | | **Free Quota** | For **Tiered**. The package does not charge the first N events in the period, where N is the quota. | | **Progressive Discounts (optional)** | For **Tiered**. Each discount sets a percentage that applies from a starting quantity. Enter the discounts in ascending order. | | **Debit and Credit Accounts** | The **Debit Account (alias)** and the **Credit Account (alias)**. Both are required. | | **Calculation Preview** | The charge for sample event counts, from the tiers, the free quota, and the discounts. | ## Maintenance package fields *** * **Fee Amount**: the amount that the package charges from each target account every period. * **Target Accounts**: the accounts to charge. * **By Segment**: the accounts in the selected segment. * **By Portfolio**: the accounts in the selected portfolio. * **Account List**: account aliases that you type, or that you upload in a UTF-8 CSV file with an `accountAlias` column. * **Credit Account (alias)**: the account that receives the charged amounts. An account list can have at most 100 aliases. When you create the package, Midaz checks that each alias exists in the Ledger. If an alias does not exist, Midaz refuses the package. ## Editing a Billing Package *** Select **Edit** in the row menu. You can change the **Package Name** and the **Description**. The type, the asset, and the **Configuration** fields do not change after creation. To stop a package from charging, turn its **Status** switch off. Billing runs use active packages only. ## Where to go next *** > **[Billing Runs](/en/products/midaz/fees/console/billing-runs)** > > Preview the fees for a period before you apply them. > **[Fees Engine overview](/en/products/midaz/fees/console/fees-engine-overview)** > > Understand how the Fees Engine, packages, and rules fit together. # Fee Calculator Source: https://docs.lerian.studio/en/products/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/products/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 and validate logic with business teams. ## Accessing the Fee Calculator *** **Step 1** In the Midaz Module sidebar, expand **Fees Engine**. **Step 2** Click **Calculator** to open the Fee Calculator page. > [!WARNING] > > You need at least one [Fee Package](/en/products/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/products/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/products/midaz/fees/console/fees-engine-overview). Each package defines a set of fee rules and the conditions under which they apply to Transactions. > [!NOTE] > > 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/products/midaz/console/managing-accounting-routes) and [Segments](/en/products/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. > [!NOTE] > > 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: > **[Create a Fee Package](/en/products/midaz/fees/console/fe-creating-a-package)** > > Define new fee calculation rules for transactions > **[Edit a Fee Package](/en/products/midaz/fees/console/fe-editing-a-package)** > > Update fee configurations and rules > **[Delete a Fee Package](/en/products/midaz/fees/console/fe-deleting-a-package)** > > Delete a fee package from your configuration > [!NOTE] > > Select a package from the list to open its **details view**. There you can review the package's configuration (scope, amount range, and fee rules) before you edit it. To preview the fees a period generates across packages, use [Billing Runs](/en/products/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 Package Name** (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: * The fee name field (placeholder **Display Name**) - Name that identifies this fee * The fee type card - How Fees Engine calculates the fee: * **Flat Fee** - Fixed amount regardless of transaction value * **Percentage Fee** - Percentage of the transaction amount * **Max between types** - 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) * **Deductible from transaction?** - Whether Fees Engine deducts this fee from the transaction * **Credit Account ID** - Account that receives the fee amount ## Common use cases *** > **Simple flat fee per transaction** > > Apply a fixed fee to all transactions within an amount range: > > * Fee type: **Flat Fee** > * **Calculation**: `flat` with value `2.50` > * **Example**: $2.50 fee on every transaction between $10 and \$1000 > **Percentage-based fee** > > Charge a percentage of the transaction amount: > > * Fee type: **Percentage Fee** > * **Calculation**: `percentage` with value `1.5` > * **Example**: 1.5% fee on transfers, where $100 transfer = $1.50 fee > **Tiered fee structure** > > Create multiple packages with different amount ranges: > > * Package 1: $0-$100 → 2% fee > * Package 2: $100.01-$1000 → 1.5% fee > * Package 3: \$1000.01+ → 1% fee > * Lower fees for higher-value transactions > **Combined flat + percentage (maximum)** > > Apply whichever is higher between flat and percentage: > > * Fee type: **Max between types** > * **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 > **Fee waiver for premium accounts** > > 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 *** **1. Plan your fee structure** Design your fee packages before implementation. Consider transaction types, amount ranges, and customer segments that require different fee treatments. **2. Use descriptive labels** Choose clear fee group labels and descriptions that explain when and why fees apply. This helps with auditing and customer communication. **3. Set appropriate amount ranges** Define minimum and maximum amounts carefully to avoid gaps or overlaps in your fee structure. **4. Test fee calculations** Use the [Fee Estimation](/en/products/midaz/fees/console/fe-fee-simulation) feature to preview fee calculations before you enable packages in production. **5. Manage priority carefully** When you use multiple fees in a package, set priorities so the fees apply in the correct order. **6. Document waived accounts** Record why you waive fees for certain accounts. This supports audit and compliance. ## Fee calculation flow *** When Fees Engine processes a transaction, it: 1. Finds the enabled packages that match the transaction route, segment, and amount range 2. Keeps the most specific match, so one package applies at most 3. Skips the waived accounts 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 > [!NOTE] > > Enable a fee package so it applies to transactions. To pause a package, disable it. A disabled package stays in your configuration. # Fee debt Source: https://docs.lerian.studio/en/products/midaz/fees/fee-debt Keep an unpaid fee as a debt that the payer's next credits can settle, oldest first, instead of refusing the transaction. By default, Midaz refuses a transaction when the payer cannot fund its fees. A **deferrable** fee changes this. The payer pays the part of the fee that its available amount covers. The rest becomes a **fee debt**, and the transaction goes through. Later credits to the payer can settle the debt. Fee debt is off by default. It applies only to fees that you mark as deferrable. ## Make a fee deferrable *** Set `deferrable: true` on a fee when you [create](/en/reference/products/midaz/v2/create-package) or [update](/en/reference/products/midaz/v2/update-package) its fee package. * Only a fee with `isDeductibleFrom: false` can be deferrable. Midaz refuses a deductible fee that is also deferrable with error `0530`. * A deferrable fee opens a debt only in a `/v2` direct transaction. In a `/v2` hold, Midaz refuses an unfunded fee with error `0018`. * When a balance already has 256 open fee debts, Midaz refuses a new unfunded deferrable fee on it with error `0018`. **Example.** A payer holds 105 and sends 100 with a deferrable fee of 10. The transaction moves 105: the payment of 100 and 5 of the fee. The other 5 becomes a fee debt. The transaction metadata key `feeDebtOpenings` records the debt. ## How a debt is settled *** A credit to the payer's balance through the `/v2` API can settle its open fee debts, oldest first. This includes direct transactions, commits, and reversals. A credit that is smaller than a debt settles part of it. A payer's balance that cannot send funds settles nothing, and settlement stops at the first debt whose fee account cannot receive funds. Each settlement moves the value from the payer's balance to the fee account of the debt. Midaz records these movements as operations of type `FEE_SETTLEMENT`, and the transaction metadata key `feeDebtSettlements` records the settled debts. To settle debts without a credit, use [Collect open fee debts](/en/reference/products/midaz/v2/collect-a-balance-s-open-fee-debts). Send the `accountAlias` of the payer, and optionally a `balanceKey` and a `maxAmount`. * Midaz pays the open debts of that balance from its available amount, oldest first, and charges no fee on the collection. * The response gives the `collected` amount and the `transactionId` of the collection. * When nothing is collected, the response gives `collected: "0"` and Midaz creates no transaction. * You cannot revert a collection. Midaz refuses it with error `0089`. ## Reversals and fee debt *** When you [revert](/en/reference/products/midaz/v2/revert-transaction) the transaction that opened a debt, the payer gets the whole fee back: * The reversal returns the part of the fee that the payer paid in the original transaction. * Midaz cancels the part of the debt that is still open. * Midaz refunds the part that later credits settled. The fee account pays the refund, and Midaz records it as operations of type `FEE_REFUND`. On `/v2`, the refund can then settle the payer's other open debts, oldest first. A revert of a credit that settled a debt can reopen that debt for the settled amount. When the fee-debt record of a transaction is not complete yet, Midaz refuses the revert with error `0529` (HTTP 409) and moves nothing. Try again later. ## Read fee debts *** * [List fee debts](/en/reference/products/midaz/v2/list-the-fee-debts-of-a-ledger-oldest-first) gives the debts of a ledger, oldest first. Filter by `account_alias`, `balance_key`, and `status` (`open` or `settled`). With `account_alias`, the response also gives `openTotal`, the total that the balance owes. * [Retrieve a fee debt](/en/reference/products/midaz/v2/get-a-fee-debt) gives one debt with its `remaining` amount and its history of changes. Listing and reading fee debts needs the `midaz/fee-debts` permission with `get`. Collecting fee debt needs `post`. Each change has one of these kinds: | Kind | What happened | | - | - | | `opened` | The payer could not fund the fee, and the debt opened. | | `settled` | A credit or a collection paid part or all of the debt. | | `canceled` | A revert of the original transaction canceled the open part. | | `reopened` | A revert took back a settlement, and that amount is owed again. | | `refunded` | A revert of the original transaction returned a settled part to the payer. | ## Accounts and balances with fee debt *** Midaz refuses to delete a balance, or to delete or close its account, when: * The balance owes fee debt (error `0527`). Credit the balance or collect the debt first. * Other balances owe fee debt to the balance (error `0528`), for example a fee account. Credit the balances that owe the debt first. ## Before you enable fee debt *** * Run Midaz v4.2.0 or later on every ledger instance before you set `deferrable` on a fee. An earlier version cannot complete a transaction that carries fee debt. After you set it, do not roll the ledger back to an earlier version. * The ledger keeps open fee debts in Valkey. Valkey must be persistent and keep these keys. See [Configuring dependencies](https://github.com/LerianStudio/helm/blob/main/charts/midaz/README.md#valkey). # Fees Engine calculations Source: https://docs.lerian.studio/en/products/midaz/fees/fee-engine-calculation Learn how the Fees Engine calculates fees, handles decimal precision, splits amounts across accounts, and applies exemptions over daily, weekly, 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. **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 counts qualifying transactions for volume packages within that window. Maintenance packages charge the accounts that are active when you run the calculation, and the period only labels the result. > [!TIP] > > 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`: `false` adds the fee on top of what the source sends. `true` deducts it from what the destination receives. The **Result** columns below show that amount. * `referenceAmount`: either `originalAmount` or `afterFeesAmount`. * `priority`: defines the order of application. Priority 1 must always use `originalAmount`. * `deferrable`: `true` keeps the part of the fee that the payer cannot fund as a [fee debt](/en/products/midaz/fees/fee-debt). ### 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 | Result | | :- | :- | :- | | `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 | Result | | :- | :- | :- | | `false` | `referenceAmount + (referenceAmount * value)` | R\$ 506.35 | | `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 waived account pays no share of the fee. When every source account is waived, no fee applies. > [!TIP] > > **Hierarchy:** Value range check > then account exemption ## Mixed example: fee exemptions and proportional fee splitting *** This package 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 | > [!CAUTION] > > 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). The engine keeps amounts 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 API rejects overlapping tiers. With two or more tiers, it also rejects gaps and requires the last tier to be unbounded. A package holds at most 50 tiers. 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 the engine prices **all** 1,750 units 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. The API requires at least one tier on every volume package. **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. The engine applies its percentage to the gross amount. The engine evaluates discounts 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 account that is active when you run the calculation. 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`). The engine keeps only accounts whose status code is `active` and excludes every other status. 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 The metadata of a volume transaction payload includes 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. The metadata of a maintenance transaction payload includes 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/products/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. These recommendations complement the [Fees Engine overview](/en/products/midaz/fees/fees-engine-overview) and the [calculation mechanics](/en/products/midaz/fees/fee-engine-calculation) guide. ## 1. Design fee packages with clear naming and segmentation *** 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`. > [!TIP] > > 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/products/midaz/v2/estimate-fee-calculation) 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. > [!NOTE] > > A v2 transaction selects the matching package automatically. The estimate endpoint requires a specific `packageId`, so you choose 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. 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. * The engine automatically adjusts fee splits with repeating decimals (e.g., R\$ 10 divided by 3 accounts) to keep ledger totals exact. ## 6. 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. * **Periodically review** deleted packages if your database grows significantly. Archiving strategies can help manage storage without losing audit capability. ## 7. Monitor fee processing in production *** Fees run inside the Midaz ledger process. Use the ledger's shared OpenTelemetry configuration and health endpoints to observe fee processing. See [Environment variables](/en/products/midaz/midaz-environment-variables) for the supported configuration. In production: * **Monitor the ledger health endpoints** and its traces and logs. * **Set up alerts** for sustained high latency or errors in fee calculations. * **Monitor the configured MongoDB deployment**: connection-pool usage, disk space, and replication health. * **Review ledger pod resource usage** against your traffic patterns and autoscaling behavior. ## 8. Keep versions compatible *** Fees runs inside the Midaz ledger process, so it upgrades with Midaz and has the same version. Before you upgrade Midaz: * Check the [version compatibility table](/en/products/midaz/midaz-version-compatibility) for the components you run with Midaz. * 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 the chart's per-version upgrade notes, see the [Midaz chart upgrade notes](https://github.com/LerianStudio/helm/tree/main/charts/midaz/docs). ## 9. 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/products/midaz/security-recommendations), which cover: * Network segmentation and Zero Trust Architecture * Secret management and rotation (including database credentials) * TLS 1.2+ enforcement for all communications * RBAC configuration via [Access Manager](/en/platform/access-manager) * Patch management and vulnerability scanning ## 10. 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. ## 11. 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. ## 12. 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. With two or more tiers, the API rejects gaps and requires an open upper bound on the last tier. 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). > [!TIP] > > Use test calculations with known transaction counts to validate your tier and discount configuration before enabling the package in production. ## 13. Size maintenance account targets appropriately *** Maintenance packages support three target types: | Target type | Use case | | - | - | | `segmentId` | Standard tiers (PF, PJ, premium) | | `portfolioId` | Business portfolios (PME, Corporate, Enterprise) | | `aliases` | Specific named accounts, at most 100 | For more than 100 accounts, use a segment or a portfolio in Midaz. ## 14. 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, so a retry is safe. A retry reflects the ledger at the time it runs. * **Check error responses** for the package that failed. The message names its ID and label. A common cause is a volume package whose tiers do not cover the billable count. * **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/products/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. It offers: * **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, weekly, 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. * **Fee debt** for deferrable fees: a fee that the payer cannot fund becomes a debt that later credits settle. > [!TIP] > > 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. Below are 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 *** The unified Midaz service exposes Fees and Billing only on the ledger-scoped v2 surface. The URL is the sole ledger input: a `ledgerId` query parameter is not accepted, and a request body that sends `ledgerId` is rejected as an unknown field. Retrieving, updating, or deleting a package owned by another ledger returns 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: * **Ledger scope** – The v2 URL identifies the ledger that records the transaction and its fees. Do not send `ledgerId` in the request body. * **transactionRoute** – The ID of a transaction route. The package matches a transaction that sends the same `routeId`. * **segmentId** – A segment ID. The package matches when every source account, except `@external` accounts, belongs to this segment. * **metadataSelector** – Metadata pairs that the transaction must all carry for the package to match. A pair matches only a string metadata value that is equal to it. You set it when you create the package. * **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 what the destination receives, instead of adding it on top. * **referenceAmount** – The base amount for calculations. * **deferrable** – Whether the part of the fee that the payer cannot fund becomes a [fee debt](/en/products/midaz/fees/fee-debt). The default is `false`. ### Validation rules 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`. * Fees with `deferrable: true` must use `isDeductibleFrom: false`. * Each fee's `creditAccount` must be an account alias that exists in the ledger. Midaz checks it when you create or update the package. ### Fee application and estimation Midaz applies fees in-process when it creates a transaction. There is no separate public calculate-fees endpoint. The ledger evaluates the matching package in the transaction flow and applies its rules when one matches. #### [Estimate transaction fees](/en/reference/products/midaz/v2/estimate-fee-calculation) * Estimates fees for a specific package by its `packageId`. * Returns calculated fees only if the amount falls inside the package's amount range. It does not check `transactionRoute` or `segmentId`. * Useful for testing, debugging, or a preview of fees, without writing to the ledger. > [!NOTE] > > Fee application happens during transaction creation. Use `estimate` when you want to test a specific package without writing to the ledger. ### 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:0198e5f4-7b2a-7c3d-9e1f-2a3b4c5d6e7f"`. > [!WARNING] > > 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. > [!TIP] > > 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, weekly, 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 fee per account that is active when you run the calculation. > [!NOTE] > > 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, using `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**: The accounts the charge debits and credits. Both must exist in the ledger. ### Maintenance packages A maintenance package applies a fixed fee per account that is active when you run the calculation, 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). > [!WARNING] > > 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 /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages`: Create a billing package. * `GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages`: List all billing packages. * `GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages/{id}`: Retrieve a specific billing package. * `PATCH /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages/{id}`: Update a billing package (`label`, `description`, `enable`). * `DELETE /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages/{id}`: Soft-delete a billing package. * `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/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:`) | No (`accountTarget` selects the accounts to charge) | | **Volume discounts** | No | Yes (`discountTiers`) | | **Free quotas** | No | Yes (`freeQuota` per period) | | **Recurring charges** | No | Yes (maintenance type) | | **Output** | Transaction with fees applied | Calculation payloads for the caller to execute | > [!NOTE] > > Fee packages and billing packages operate independently. ## 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` on its package, which selects the transactions the package applies to. 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`. ## Soft delete for safe recordkeeping *** When you delete a resource: * Fees Engine marks it with a `deletedAt` timestamp. * Standard queries exclude it, but the database still stores it for audit and history. ## 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. Use cases include: * Exchange engines * Lending platforms * Bill payment systems * Smart contracts * Pix (Brazil's instant payment platform) ## Security recommendations *** Before you deploy any component, review our [Security Recommendations](/en/products/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) ## Next steps *** > **[Explore the Fees Engine API](/en/reference/products/midaz/v2/create-package)** > > Browse endpoints for fee packages, calculations, and estimations. > **[Using Fees Engine](/en/products/midaz/fees/using-fee-engine)** > > Learn how to create fee packages and apply them to transactions. # Migrate Bank Transfer fees to Midaz Source: https://docs.lerian.studio/en/products/midaz/fees/fees-migration-guide Move fee packages from the standalone plugin-fees to the Midaz Fees Engine, and switch Bank Transfer 3.1.0 or later to Midaz fees without a gap in charging. Bank Transfer 3.0.x charges fees through the standalone Fees plugin, `plugin-fees`. Bank Transfer 3.1.0 and later can charge them through the [Fees Engine](/en/products/midaz/fees/fees-engine-overview) that runs inside Midaz 4.1.0 and later. This page moves your fee packages from plugin-fees to Midaz. It then switches Bank Transfer over in an order that keeps every transfer charged. Follow this page when all of these are true: * You run Bank Transfer 3.0.x. * Your fee packages live in plugin-fees. * You upgrade to Bank Transfer 3.1.0 or later and to Midaz 4.1.0 or later. Bank Transfer uses only the fee packages of plugin-fees. If you also keep billing packages in plugin-fees, recreate them as Midaz [billing packages](/en/products/midaz/fees/using-fee-engine) before you retire plugin-fees. This page does not map them. > [!NOTE] > > The Lerian Console manages Midaz fee packages only. It does not show the packages that live in plugin-fees. See [Console](#console). ## How Bank Transfer chooses who charges the fee *** From 3.1.0, the Bank Transfer variable `MIDAZ_FEE_MODE` decides who calculates and charges the fee of each transfer. | Value | Who charges the fee | Midaz API that Bank Transfer posts on | | - | - | - | | `legacy` | plugin-fees, as in Bank Transfer 3.0.x | `/v1` | | `native` | The Midaz Fees Engine, from the fee packages of the ledger | `/v2` | | `auto` (default) | Bank Transfer reads the Midaz version. It uses `native` on Midaz 4.1.0 or later, and `legacy` on an earlier version. | `/v2` or `/v1` | * Bank Transfer does not start with any other value. * Under `auto`, Bank Transfer reads the Midaz version again every `MIDAZ_FEE_MODE_REFRESH` (default `5m`). When it cannot read the version, it keeps the mode it has, or uses `legacy` if it has none. * Bank Transfer fixes the mode of a P2P transfer or TED OUT at `/initiate`, and of a TED IN when it receives it. Every later step of that transfer uses the same mode: retries, confirmation, cancellation and reconciliation. A change of mode applies to new transfers only. * In `legacy` mode, `BTF_FEE_ENABLED` turns the plugin-fees integration on. With `false`, the default, Bank Transfer does not call plugin-fees, and every transfer runs with fee 0. * Set `native` only on Midaz 4.1.0 or later. Midaz 4.0.x charges fees without marking the fee entries, so Bank Transfer cannot read the fee that it charged. > [!WARNING] > > Do not rely on `auto` during the migration. Under `auto`, Bank Transfer 3.1.0 moves to `native` by itself as soon as it reads Midaz 4.1.0 or later. New transfers then use the Midaz packages. If your packages are still only in plugin-fees, Midaz finds no package, and those transfers carry no fee. > > In Bank Transfer 3.1.0, `BTF_FEE_ENABLED` does not stop Midaz from charging fees in `native` mode. To stop a fee, disable its Midaz package with `enable: false`, or delete the package. Midaz then also charges no fee on a native transfer that it has not posted yet. ## Before you start *** You need: * Midaz 4.1.0 or later, or a plan to upgrade to it. See [Updating Midaz](/en/products/midaz/updating-midaz). * Bank Transfer 3.1.0 or later, or a plan to upgrade to it. * Access to the plugin-fees API, to read and estimate your packages. * Access to the Midaz fee package API, or to the Console [Fee Packages](/en/products/midaz/fees/console/managing-fee-packages) page. * For each ledger and transfer type, the Midaz transaction route that Bank Transfer posts with. The tenant policy `routing.ledger_bindings` names it. See [Ledger routing](/en/interfaces/ted-jd/ted-configuration#ledger-routing). * Three Midaz permissions for the Midaz credentials of Bank Transfer: the resource `packages` with action `get`, the resource `estimates` with action `post`, and the resource `organizations` with action `get`. In single-tenant mode, these are the credentials of `MIDAZ_CLIENT_ID`. In multi-tenant mode, Bank Transfer uses the Midaz credentials of each tenant, so grant all three permissions to the application of each tenant. `legacy` mode does not use these permissions. Both modes also use the Midaz `settings` `post` permission described in [Bank Transfer environment variables](/en/interfaces/ted-jd/ted-environment-variables). In `native` mode, without `packages`, every P2P and TED OUT initiation answers `503 BTF-2000`. Without `estimates`, every initiation that matches a package answers `503 BTF-2000`. `organizations` is needed because this page sets `MIDAZ_FEE_MODE=native` explicitly: Bank Transfer then makes one read of the Midaz organization list, to check the `/v2` API, before it first uses a set of credentials. Under `auto`, it does not make this read. Without `organizations`, Bank Transfer in single-tenant mode does not start, and in multi-tenant mode every P2P and TED OUT initiation of that tenant answers `503 BTF-2000`. The examples below use these shell variables. Every UUID is a placeholder. Use the values from your own environment. Bank Transfer reaches the Midaz ledger at `MIDAZ_TRANSACTION_URL`, or at `MIDAZ_BASE_URL` when that is unset. It strips a trailing `/v1` or `/v2` from that address. Strip it in `MIDAZ_LEDGER_URL` too, because the paths below carry the version. ```bash theme={null} # Addresses, as Bank Transfer has them. FEES_BASE_URL="https://plugin-fees.example.com" # FEES_BASE_URL MIDAZ_LEDGER_URL="https://midaz.example.com" # Two different bearers. They are not interchangeable. FEES_BEARER_TOKEN="..." # for plugin-fees MIDAZ_BEARER_TOKEN="..." # for the ledger MIDAZ_ORGANIZATION_ID="3fa85f64-5717-4562-b3fc-2c963f66afa6" MIDAZ_LEDGER_ID="9c858901-8a57-4791-81fe-4a34d4dd8ab5" ``` ## Map a plugin-fees package to a Midaz package *** The fee rules keep their field names and their JSON shape. Three things change: where the organization goes, where the ledger goes, and what `transactionRoute` holds. | plugin-fees | Midaz | What to do | | - | - | - | | `X-Organization-Id` header | `{organization_id}` in the URL | Send the organization in the path. | | `ledgerId` in the body | `{ledger_id}` in the URL | Send the ledger in the path. | | `transactionRoute` | `transactionRoute` | Replace the value. See [Transaction route](#transaction-route). | | `feeGroupLabel`, `description` | Same fields | Copy. | | `segmentId` | `segmentId` | Copy it or leave it out, as [Package scope](#package-scope) says. Leave it out of every TED IN package. | | `minimumAmount`, `maximumAmount` | Same fields | Copy. | | `waivedAccounts` | `waivedAccounts` | Copy. Both accept account aliases and the `segment:` form. | | `enable` | `enable` | Migrate only packages with `true`. Create each one with `false`. You enable it when you switch to `native`. | | `feeLabel`, `calculationModel`, `referenceAmount`, `priority`, `isDeductibleFrom` of each fee | Same fields | Copy. | | `creditAccount` of each fee | `creditAccount` | Copy. The account must exist in the ledger. Midaz checks it when you create the package. | | `routeFrom`, `routeTo` of each fee | Same fields | Midaz writes the value as the operation route of the fee entry. Use the ID of a Midaz operation route, or leave the field out. In `legacy` mode, Bank Transfer dropped a value that was not a route ID. | | `id`, `createdAt`, `updatedAt`, `deletedAt` | New values | Leave them out. Midaz assigns a new ID. Keep a record of the plugin-fees package that each Midaz package replaces. | | No equivalent | `metadataSelector` (Midaz 4.2.0 or later) | Leave it out. | Midaz rejects a field that it does not know. Send none of `ledgerId`, `id`, `createdAt`, `updatedAt` and `deletedAt`. A TED OUT charges its fee on top of the amount. In both modes, Bank Transfer 3.1.0 or later refuses a TED OUT with `422 BTF-3002` when the package that matches it has a fee with `isDeductibleFrom: true`. ### Transaction route plugin-fees compares `transactionRoute` with a name that Bank Transfer sends: `ted_out`, `ted_in` or `p2p`. Midaz compares it with the `routeId` of the transaction, which is the ID of a Midaz transaction route. Midaz accepts a UUID in this field. For each package, use the transaction route that `routing.ledger_bindings` names for the ledger and the transfer type of the package. > [!WARNING] > > A ledger whose binding uses `mode: omit` has no safe package: the only one that can match its transfers has no route, so it charges every `/v2` transaction of the ledger. Give that binding the Midaz transaction route of each transfer type on the Console [Accounting Routes](/en/interfaces/ted-jd/console/bt-accounting-routes) page, and check **Cutover readiness** there. This changes the accounting of every Bank Transfer posting on that ledger. Do it after step 2, once Midaz runs 4.1.0 or later, and before you switch. ### Package scope plugin-fees and Midaz select the package of a transfer in different ways, so an exact copy can charge a different fee. Apply these rules to the enabled packages of each ledger: * **The ledger has one enabled package.** plugin-fees applies it to every transfer in its amount range, P2P, TED OUT and TED IN alike. It ignores the `transactionRoute` and the `segmentId` of that package. In Midaz, create one copy for each transfer type that it charged, each with the transaction route of that type, and without `segmentId`. * **A route has one package.** When the ledger has more than one enabled package, plugin-fees applies the only package of a route to every transfer on that route, inside its amount range. It ignores the `segmentId` of that package. Create the Midaz package without `segmentId`. * **A route has several packages.** plugin-fees then picks by the segment of the sender. A sender with a segment gets only a package with that segment. A sender with no segment gets only a package with no segment. Keep each `segmentId`. Midaz also applies a package with no segment to a sender who has a segment, when no package of that segment covers the amount. If the route has a package with no segment, decide which behavior you want. * When every fee of that package is charged on top of the amount (`isDeductibleFrom: false`), you can keep the plugin-fees behavior. Add `segment:` to the `waivedAccounts` of that package for every segment of the ledger, and for every segment that you create later. Midaz then charges none of its fees to a sender in those segments. * On a fee taken out of the amount, the waiver also exempts a recipient in those segments, so do not use it there. For such a package, the engines cannot match. Decide whether a sender whose segment has no package of its own for that amount pays it in Midaz, or whether you leave it out, so that a sender with no segment pays none of its fees. * **TED IN.** For a TED IN, Bank Transfer sent plugin-fees the segment of the recipient account. Midaz reads the segment from the source accounts, and it skips the external account, which is the only source of a TED IN. So a Midaz package with a `segmentId` never matches a TED IN. Create every TED IN package without `segmentId`. * If the ledger has more than one TED IN package in plugin-fees, plugin-fees charged a recipient with a segment only a package with that segment, and a recipient with no segment only a package with no segment. Midaz cannot see the segment of the recipient, and it applies the packages with no segment to every recipient. Decide the TED IN price that applies to every recipient. * **A package with no route.** When the ledger has more than one enabled package, plugin-fees never applied a package with no route to a Bank Transfer transfer, because Bank Transfer always sends a route. Leave it out. * **Two packages that match.** Midaz selects the package that matches the most constraints. When two packages match equally, Midaz refuses the transaction with error `0198`. At initiation, Bank Transfer answers `422 BTF-2005`. ## Migrate step by step *** **1. Pin the legacy mode** Set `MIDAZ_FEE_MODE=legacy` in the Bank Transfer environment. Bank Transfer 3.0.x ignores the variable, so you can set it ahead of the upgrade. If you deploy with the Helm chart, follow its [upgrade note](https://github.com/LerianStudio/helm/blob/main/charts/plugin-br-bank-transfer/docs/UPGRADE-2.3.md) to set the value and check it before the rollout. Keep `BTF_FEE_ENABLED=true` and the `FEES_*` variables as they are. **2. Upgrade Bank Transfer, then Midaz** Before you upgrade, make sure that no plugin-fees package that a TED OUT can match has a fee with `isDeductibleFrom: true`. [Package scope](#package-scope) says which packages a TED OUT can match. From 3.1.0, Bank Transfer refuses such a TED OUT with `422 BTF-3002` in `legacy` mode too. Upgrade Bank Transfer to 3.1.0 or later. Wait until every Bank Transfer pod runs 3.1.0 or later. An earlier version cannot settle a transfer that Bank Transfer created in `native` mode. Check the log of every pod for `midaz: fee mode resolved` with `feeMode=legacy` and `source=config`. In single-tenant mode, a pod logs it at boot. In multi-tenant mode, it logs it for each tenant at the first transfer of that tenant. `source=config` proves that the pin works. Check it while Midaz still runs a version earlier than 4.1.0. There, `auto` also resolves to `legacy`, so a transfer that pays its plugin-fees fee does not prove the pin. Upgrade Midaz to 4.1.0 or later. Make a small transfer. Check that plugin-fees still charges its fee. **3. Recreate each enabled package in Midaz** List the enabled packages of each ledger in plugin-fees. Each answer holds one page, and its `total` counts the items of that page only. Read the next page until a page returns fewer items than `limit`. The list leaves out deleted packages. ```bash theme={null} curl -s "$FEES_BASE_URL/v1/packages?ledgerId=$MIDAZ_LEDGER_ID&enable=true&limit=100&page=1" \ -H "Authorization: Bearer $FEES_BEARER_TOKEN" \ -H "X-Organization-Id: $MIDAZ_ORGANIZATION_ID" ``` Create each package in Midaz with the mapping and the scope rules above. Set `enable` to `false`. Keep a list of the packages that you create: you enable exactly these when you switch. This example recreates a flat TED OUT fee. Its `transactionRoute` is the TED OUT transaction route of the ledger binding. ```bash theme={null} curl -s -X POST "$MIDAZ_LEDGER_URL/v2/organizations/$MIDAZ_ORGANIZATION_ID/ledgers/$MIDAZ_LEDGER_ID/packages" \ -H "Authorization: Bearer $MIDAZ_BEARER_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "feeGroupLabel": "TED OUT fee", "description": "Fee on outgoing TEDs", "transactionRoute": "0199a5c1-3e2f-7b4d-9a8e-5f6d7c8b9a0e", "minimumAmount": "0.01", "maximumAmount": "1000000.00", "fees": { "tedOutFee": { "feeLabel": "TED OUT fee", "calculationModel": { "applicationRule": "flatFee", "calculations": [{ "type": "flat", "value": "5.00" }] }, "referenceAmount": "originalAmount", "priority": 1, "isDeductibleFrom": false, "creditAccount": "fees-revenue" } }, "enable": false }' # -> {"id": "..."} this is the Midaz package ID ``` You can also create the packages on the Console [Fee Packages](/en/products/midaz/fees/console/managing-fee-packages) page. **4. Compare the fees of both engines** For each package, estimate the same transaction in plugin-fees and in Midaz. Compare the fee amounts. They must be equal. Use amounts at both ends of the range of the package, and one amount in the middle. For a package that carries the segment waivers from [Package scope](#package-scope), use a sender outside those segments. Midaz exempts a sender in them by design, and plugin-fees does not. ```bash theme={null} TRANSACTION='{ "send": { "asset": "BRL", "value": "1500.00", "source": { "from": [{ "accountAlias": "@sender", "amount": { "asset": "BRL", "value": "1500.00" } }] }, "distribute": { "to": [{ "accountAlias": "@external/BRL", "amount": { "asset": "BRL", "value": "1500.00" } }] } } }' # plugin-fees: the plugin-fees package ID, and the ledger in the body curl -s -X POST "$FEES_BASE_URL/v1/estimates" \ -H "Authorization: Bearer $FEES_BEARER_TOKEN" \ -H "X-Organization-Id: $MIDAZ_ORGANIZATION_ID" \ -H 'Content-Type: application/json' \ -d '{ "packageId": "0198f1a2-6c3d-7e4f-8a9b-1c2d3e4f5a6b", "ledgerId": "'"$MIDAZ_LEDGER_ID"'", "transaction": '"$TRANSACTION"' }' # Midaz: the Midaz package ID, and the ledger in the URL curl -s -X POST "$MIDAZ_LEDGER_URL/v2/organizations/$MIDAZ_ORGANIZATION_ID/ledgers/$MIDAZ_LEDGER_ID/estimates" \ -H "Authorization: Bearer $MIDAZ_BEARER_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "packageId": "0199b7d2-4f5e-7a6b-8c9d-2e3f4a5b6c7d", "transaction": '"$TRANSACTION"' }' ``` In the Midaz answer, each fee entry has `"feeLeg": "true"` in its `metadata`. You can also use the Console [Fee Calculator](/en/products/midaz/fees/console/managing-fee-calculations). An estimate calculates the one package that you name. It does not check `transactionRoute` or `segmentId`, so it does not show which package a transfer gets. The next step tests that. **5. Rehearse the selection in homologation** Run steps 1 to 4 in homologation first, with the same packages. Then test which package each transfer gets, before you switch production: 1. Under `legacy`, call `POST /v1/transfers/initiate` for each case: P2P and TED OUT, from a sender in each segment of the ledger and from a sender with no segment, with amounts at both ends of the range of each package. Record `feeAmount` and `packageAppliedId` from each answer. Do not process these initiations. An initiation posts nothing to Midaz. It writes a `payment_initiations` row that expires at `expiresAt`, keeps a duplicate fingerprint for 300 seconds by default, and sends a `payment_initiation.created` event to your webhook and event consumers. A TED OUT initiation outside the operating window answers `422 BTF-0010`. 2. Switch homologation to `native`, as in the next step. 3. Repeat the same initiations. Compare each `feeAmount`. `packageAppliedId` now names the Midaz package. Check that it is the package that replaces the plugin-fees one. An identical initiation inside that duplicate window answers `409 BTF-0012`, so wait for it to pass. 4. TED IN has no initiation. Under each mode, receive one small TED IN for a recipient in each segment and for a recipient with no segment. Compare the fees. Expect a difference only where [Package scope](#package-scope) says the engines cannot match, and only the one you chose there. Fix every other difference in the Midaz packages, and repeat the cases. **6. Switch Bank Transfer to native** Before the switch, run the P2P and TED OUT initiation cases of step 5 in production, under `legacy`. Use test accounts of your own, one in each segment and one with no segment. Record each `feeAmount`. Step 5 says what an initiation writes. 1. Give the Midaz credentials of Bank Transfer the `packages` (`get`), `estimates` (`post`) and `organizations` (`get`) permissions. 2. Enable each Midaz package that you created in step 3. Send `"enable": true` with [Update a Package](/en/reference/products/midaz/v2/update-package). No initiation tests a TED IN, so first compare the `transactionRoute` of each TED IN package with the **Transaction route** of the `TED_IN` binding of its ledger, on the Console [Accounting Routes](/en/interfaces/ted-jd/console/bt-accounting-routes) page. 3. Set `MIDAZ_FEE_MODE=native`. 4. Restart every Bank Transfer pod. Right after the restart, run the same cases again, with the same accounts. Compare each `feeAmount`, and check each `packageAppliedId`. Expect a difference only where [Package scope](#package-scope) says the engines cannot match, and only the one you chose there. An identical initiation inside the duplicate window answers `409 BTF-0012`, so wait for the window to pass between the two runs. An initiation posts nothing to Midaz, so these cases show a wrong route or segment ID without moving money. If another difference shows, [roll back](#roll-back). Correct the fees or the amount range of a package with Update a Package. Update a Package cannot change `transactionRoute` or `segmentId`, so for a wrong route or segment, create a corrected package with `enable: false`, and delete the wrong one only after the native transfers that used it have finished. This query lists the transfers created in `native` mode since the restart began. Check the fee of each one by hand. ```sql theme={null} SELECT id, type, status, created_at FROM transfers WHERE ledger_fee_mode = 'native' AND created_at >= '2026-10-09 14:00:00-03'; -- just before the restart began ``` New transfers now use Midaz fees. A P2P transfer or TED OUT initiated before the restart, and a TED IN received before it, keep the `legacy` mode until they finish. **7. Check real transfers** Make a small P2P transfer or TED OUT. Check the fee that Bank Transfer shows at initiation. Check the fee that it records on the transfer. Both must match the fee that plugin-fees charged before, except for a difference that you chose in [Package scope](#package-scope). Check the first TED IN that arrives after the switch the same way. In Midaz, the metadata of a transaction that a package charged or exempted has `packageAppliedID`. It is the ID of that Midaz package. **8. Retire plugin-fees** Keep plugin-fees running while this query returns rows. In multi-tenant mode, run it on the Bank Transfer database of each tenant. ```sql theme={null} SELECT id, status FROM transfers WHERE type = 'TED_IN' AND ledger_fee_mode = 'legacy' AND status NOT IN ('COMPLETED', 'REJECTED', 'FAILED', 'CANCELLED'); ``` These are TED INs received before the switch. When Bank Transfer resumes one, it asks plugin-fees for its fee. If plugin-fees is stopped, Bank Transfer credits the recipient with no fee. When `fees.fail_closed_default` is `true`, it returns the TED to the sender instead, unless Bank Transfer cannot read that policy. Then back up the plugin-fees MongoDB database, and stop plugin-fees. After that, you cannot [roll back](#roll-back). Keep `MIDAZ_FEE_MODE=native`. Under `auto`, a pod that cannot read the Midaz version starts in `legacy` mode, and `legacy` needs plugin-fees. ## Roll back *** You can return to plugin-fees while plugin-fees still runs with its packages unchanged: 1. Set `MIDAZ_FEE_MODE=legacy`. 2. Restart every Bank Transfer pod. New transfers then use plugin-fees again. This needs `BTF_FEE_ENABLED=true` and the `FEES_*` variables still in place. Transfers created in `native` mode keep that mode. Bank Transfer finishes them on the Midaz `/v2` API, and Midaz charges their fees when it posts them. Keep the Midaz packages enabled until every native transfer has finished. Keep Bank Transfer on 3.1.0 or later while any transfer created in `native` mode is still open. An earlier version settles that transfer on `/v1`, so its fee is not charged or not recorded. ## Console *** The Fees Engine pages of the Console, [Fee Packages](/en/products/midaz/fees/console/managing-fee-packages) and [Fee Calculator](/en/products/midaz/fees/console/managing-fee-calculations), work on Midaz fee packages only. They do not read plugin-fees. ## See also *** * [What is Fees Engine?](/en/products/midaz/fees/fees-engine-overview) * [Using Fees Engine](/en/products/midaz/fees/using-fee-engine) * [Estimate transaction fees](/en/reference/products/midaz/v2/estimate-fee-calculation) * [Bank Transfer configuration](/en/interfaces/ted-jd/ted-configuration) * [Bank Transfer environment variables](/en/interfaces/ted-jd/ted-environment-variables) # Using Fees Engine Source: https://docs.lerian.studio/en/products/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). > [!TIP] > > Fee packages apply charges at transaction time. Billing packages calculate charges over a period (daily, weekly, 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 v2 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/products/midaz/v2/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** - The ID of a transaction route, when you need route-level matching. * **segmentId** - The ID of the segment the source accounts belong to, when you need segment-level matching. * **metadataSelector** - Metadata pairs that the transaction must carry, when you need metadata-level matching. * **Ledger scope** - The request URL identifies which ledger records the transaction; do not send `ledgerId` in the package body. * **Minimum** and **maximum** amount - Thresholds for fee application. * **routeFrom** / **routeTo** - Define how each fee moves across accounting flows. These fields let 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/products/midaz/v2/get-all-packages) - List all packages created. * [Retrieve a Package](/en/reference/products/midaz/v2/get-package-by-id) - Retrieve information of a specific package. * [Update a Package](/en/reference/products/midaz/v2/update-package) - Update the information of a specific package. * [Delete a Package](/en/reference/products/midaz/v2/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/products/midaz/v2/estimate-fee-calculation) 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 through the v2 transaction API, for example with the [Create a Transaction using the v2 direct model](/en/reference/products/midaz/v2/create-transaction-direct) endpoint. Only v2 transactions apply fees, including the direct and hold items of a v2 batch. To match a route-scoped package, send the transaction route ID in `routeId`. Midaz reads the segment from the source accounts. ### Step 4 - Fees Engine evaluates the packages Fees Engine runs in-process when Midaz creates the transaction. It evaluates whether a package applies, based on: * **transactionRoute**, when configured * **segmentId**, when configured * **metadataSelector**, when configured * **Ledger scope from the request URL** * **Minimum** and **maximum** amount * **waivedAccounts**, when configured for fee exemptions > [!NOTE] > > Fees Engine selects at most one package per transaction: the most specific match. Each `metadataSelector` pair makes a package more specific. When two packages match equally, Midaz refuses the transaction with error `0198`. ### Step 5 - Check for exemptions The system checks: * If the transaction amount is outside the allowed range. * If every source account is waived. If either condition is true, Fees Engine applies no fees and the transaction proceeds normally. The External Account (`@external/`) that Midaz adds to a cross-ledger transaction never pays a fee. ### 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/products/midaz/v2/estimate-fee-calculation) 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. > [!TIP] > > 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 | | :- | :- | :- | | 0009 | 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. | | 0007 | 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. | | 0180 | Invalid fee priority | The priority field in fees is invalid. Field can not be repeated. | | 0182 | minimumAmount greater than maximumAmount | minimumAmount value is greater than maximumAmount. | | 0186 | Failed to calculate fee | Failed to calculate the fee for the transaction. Please check the fee configuration and try again. | | 0188 | originalAmount is required when priority is one | For Priority equals to one, referenceAmount must be 'originalAmount' for fee ``. | | 0189 | Failed to apply rule: flatFee or percentual | applicationRule flatFee or percentual must have exactly 1 calculation for Fee ``. | | 0198 | Package filtering error | More than one fee package matches this transaction at the same specificity, so none of them can be applied. Re-scope one of these packages: ``. | | 0199 | Package amount range overlap | The maximumAmount and minimumAmount of the new package overlap with the amount range of an existing package. | | 0530 | Deductible fee cannot be deferrable | Fee `` is deducted from the payment (isDeductibleFrom true), so it cannot be deferrable. Set deferrable to false or isDeductibleFrom to false. | ## 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**, a charge per Pix sent with tiered pricing: ```json theme={null} POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "Pix Send Monthly Billing", "description": "Monthly volume billing for Pix transactions", "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**, a monthly fee per active PF account: ```json theme={null} POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages Body: { "label": "PF Account Maintenance", "description": "Monthly maintenance fee for active PF accounts", "type": "maintenance", "enable": true, "feeAmount": "9.90", "assetCode": "BRL", "maintenanceCreditAccount": "fees-maintenance-pf", "accountTarget": { "segmentId": "0198e5f4-7b2a-7c3d-9e1f-2a3b4c5d6e7f" } } ``` ### Step 2: Trigger billing calculation Call `POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing/calculate` with the billing period. The URL identifies the ledger. The engine evaluates every enabled billing package in the ledger, limited to the `type` you send. ```json theme={null} POST /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing/calculate Body: { "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 Each result includes: * The billing package that generated it. * The calculated amounts. ### Step 4: Execute charges Submit only nonempty `transactionPayload` results. An empty object means there is no charge to execute. > [!NOTE] > > 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 from nonempty transaction payloads for reconciliation. * The billing calculation is stateless. A re-run for the same period reflects the ledger at the time of the re-run. ### Managing billing packages Use these endpoints to manage existing billing packages: * `GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages`: List all billing packages. * `GET /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages/{id}`: Retrieve a specific billing package. * `PATCH /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages/{id}`: Update a billing package (`label`, `description`, `enable` only). * `DELETE /v2/organizations/{organization_id}/ledgers/{ledger_id}/billing-packages/{id}`: Soft-delete a billing package. > [!WARNING] > > 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/products/midaz/fees/using-fee-engine-with-midaz-console Define, test, and apply fees visually with the Fees Engine in the Midaz Module, with 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). The visual interface lets you 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 * **Configure billing**: Create volume and maintenance billing packages, and preview the billing for a period ## 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: > **[Fees Engine in Midaz Module](/en/products/midaz/fees/console/fees-engine-overview)** > > Complete guide to managing fees through the Lerian Console interface. # Prerequisites Source: https://docs.lerian.studio/en/products/midaz/installing-and-deploying-overview Prerequisites hub for installing and deploying Midaz: security recommendations, setup, updates, deployment strategies, Terraform, and Helm 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 *** * [**Security recommendations**](/en/products/midaz/security-recommendations): Understand shared responsibilities, best practices, and the configurations you need to deploy Midaz and its plugins securely. * [**Installing Midaz**](/en/products/midaz/midaz-setup): Instructions to install and configure Midaz. * [**Updating Midaz**](/en/products/midaz/updating-midaz): Instructions to update your instance when you need to. * [**Deployment strategies**](/en/products/midaz/deployment): Learn about Midaz deployment strategies and platform requirements. * [**Midaz Terraform Foundation**](/en/products/midaz/midaz-terraform-foundation): Set up the Midaz cloud foundation with Terraform best practices. * [**Midaz chart README**](https://github.com/LerianStudio/helm/blob/main/charts/midaz/README.md): install and configure Midaz with its chart. Per-version upgrade notes are in the chart's [upgrade notes](https://github.com/LerianStudio/helm/tree/main/charts/midaz/docs). * [**Version compatibility**](/en/products/midaz/midaz-version-compatibility): Check which plugin versions work with each Midaz release. > [!CAUTION] > > 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) plugin. When `ENV_NAME=production`, the ledger requires authentication and does not start without it. For details, [contact us](https://lerian.studio/contact). # Lerian Support Portal Source: https://docs.lerian.studio/en/products/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. > [!NOTE] > > If you use the community version of Midaz, see the [Community support](/en/products/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 Management * 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 > [!TIP] > > 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. | 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. | > [!TIP] > > 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 | > [!CAUTION] > > For P1 or P2 incidents, provide as much detail as possible in the description field. ## How to open an incident *** To open an incident, follow these steps: **Step 1** Go to the [Lerian Support Portal](https://lerian.atlassian.net/servicedesk). **Step 2** Log in with your email and password. **Step 3** Select **Incidents**. **Step 4** Choose the incident type: **Non Transactional Incident** or **Transactional Incident**. **Step 5** Fill in all required fields, especially the description and product selection. **Step 6** Click **Submit**. ## Still need help? *** If you are still unsure where your issue fits, email us at [**suporte@lerian.studio**](mailto:suporte@lerian.studio). We route your message to the right team. # Environment variables Source: https://docs.lerian.studio/en/products/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 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, 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. > [!NOTE] > > You deploy the **ledger** service. It serves the onboarding and transaction routes, and **CRM** and **Fees** run in the same process. The ledger reads the CRM and Fees variables below. Tracer ships as its own optional service. The Helm chart also 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` | 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 | | `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` | Default date-range window in months for transaction and operation lists when neither start nor end date is supplied. The bundled example configuration ships `3`. | `1` | No | | `TRANSACTION_BATCH_MAX_SIZE` | Maximum number of transactions in one atomic batch request. Accepts `1` to `50`. Any other value stops the server on startup. | `10` | No | | `OPENAPI_DOCS_ENABLED` | Serve the OpenAPI 3.1 spec at `/openapi.json` and `/openapi.yaml`, and the interactive docs at `/docs` | `false` | 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 it. Supply it 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 | | `DB_TRANSACTION_ROUTE_TX_READS_TO_PRIMARY` | Send the reads of the transaction flow to the primary, not the replica. A false value, such as `false` or `0`, turns it off. | `true` | 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 | `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` | Base64-encoded GCP service account credentials for IAM auth | — | When `REDIS_USE_GCP_IAM=true` | ## Message broker (RabbitMQ) The internal asynchronous balance-operation pipeline uses RabbitMQ when `RABBITMQ_TRANSACTION_ASYNC=true`. RabbitMQ also carries selected legacy outbound exchanges. | 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_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. Takes effect only when `RABBITMQ_TRANSACTION_ASYNC=true`. | `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 check a transaction with Tracer before it commits the transaction. Leave `TRACER_BASE_URL` unset to disable it. | Variable | Description | Default | Required | | - | - | - | - | | `TRACER_BASE_URL` | Address of the Tracer gRPC seam, `host:4021` (the `TRACER_GRPC_PORT` of Tracer). Setting it enables the reservation client. | — | No | | `TRACER_TIMEOUT_MS` | Reservation call deadline (milliseconds) | `250` | No | | `TRACER_TLS_MODE` | Seam security: `mesh` (default, TLS terminated by the service mesh), `mtls`, or `server` | `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` or `server`) | — | If `mtls` or `server` | | `TRACER_M2M_CLIENT_ID` | Read with `PLUGIN_AUTH_ENABLED=true`. | — | Required when `TRACER_BASE_URL` is set and `PLUGIN_AUTH_ENABLED=true`. | | `TRACER_M2M_CLIENT_SECRET` | Read with `PLUGIN_AUTH_ENABLED=true`. Sensitive. | — | Required when `TRACER_BASE_URL` is set and `PLUGIN_AUTH_ENABLED=true`. | | `TRACER_M2M_WAIT_TIMEOUT_MS` | Read only with `PLUGIN_AUTH_ENABLED=true`. Accepts `1` to `5000` (milliseconds). | `3000` | No | | `TRACER_API_KEY` | Read with `PLUGIN_AUTH_ENABLED=false`. When `TRACER_BASE_URL` is set, the ledger does not start if you also set `PLUGIN_AUTH_ENABLED=true`. Tracer accepts it only with `API_KEY_ENABLED=true` and `PLUGIN_AUTH_ENABLED=false`. Sensitive. | — | No | ## CRM and Fees The ledger process reads these variables, because 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 `token` only in `local`; use `approle` in `byoc`/`saas`. | — | When `KMS_VENDOR=hashicorp-vault` | | `KMS_VAULT_ROLE_ID` | Vault AppRole role ID (when `approle`) | — | No | | `KMS_VAULT_SECRET_ID` | Vault AppRole secret ID (when `approle`). Sensitive. | — | No | ## Configuration shared across Lerian products The following blocks are identical across Lerian products. The [BYOC configuration reference](/en/reference/byoc-configuration) documents them in full. They default off. A BYOC deployment can ignore all the optional ones. * **Access Manager authentication**: `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_HOST`. Enable in production. * **Service discovery**: `SD_*` (Consul). Off by default. * **Event streaming**: `STREAMING_*`. Off by default on the ledger. * **OpenTelemetry**: `ENABLE_TELEMETRY`, `OTEL_*`. Telemetry is OTLP push. # Installing Midaz Source: https://docs.lerian.studio/en/products/midaz/midaz-setup Install Midaz locally with Docker Compose in a few commands: the full environment, ready for local development. You run and test Midaz locally. Install it with **Docker Compose**, which sets up the full environment. ## 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) * 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. **Step 1** Open PowerShell as an administrator. **Step 2** Run: ```bash Text theme={null} wsl --install ``` **Step 3** Restart your machine if needed. **Step 4** 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. > [!CAUTION] > > 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 **1. Choose your working directory** ```bash Text theme={null} cd ~/Desktop ``` **2. Clone the repo** ```bash Text theme={null} git clone https://github.com/LerianStudio/midaz.git ``` **3. Enter the folder** ```bash Bash theme={null} cd midaz ``` ### Step 4 – Set up environment variables Start Docker before this step. `make set-env` copies the `.env` files and generates the `LCRYPTO_*` CRM keys through a Docker one-shot container. The command 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/) * Tracer API: [http://localhost:4020](http://localhost:4020/) > [!CAUTION] > > Make sure Docker is up and all `.env` files are set before you access the API. ## Try the API *** > [!TIP] > > Start with our [Quick Start Guide](/en/reference/products/midaz/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) plugin, available for enterprise institutions. > [!CAUTION] > > 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. To contribute, start with our [Contributing Guide](https://github.com/LerianStudio/midaz/blob/main/CONTRIBUTING.md). > [!TIP] > > [Talk to our team](https://lerian.studio/contact) for hands-on support. ## License *** Midaz is proprietary to Lerian Studio Ltd. Versions released under the Elastic License 2.0 keep that license. # Midaz Terraform Foundation Source: https://docs.lerian.studio/en/products/midaz/midaz-terraform-foundation Provision the base infrastructure for Midaz on AWS, GCP, or Azure with Terraform templates: network, Kubernetes cluster, and datastores. The [Lerian Terraform Foundation](https://github.com/LerianStudio/lerian-terraform-foundation) repository holds Terraform templates for the infrastructure that Midaz runs on: the network, the Kubernetes cluster, and the datastores. It supports AWS, GCP, and Azure. The repository uses the [Apache License 2.0](https://github.com/LerianStudio/lerian-terraform-foundation/blob/main/LICENSE). ## What the templates create *** | Cloud | Network and cluster | Datastores for Midaz | | - | - | - | | AWS | VPC with public and private subnets, Amazon EKS | PostgreSQL (Amazon RDS), Amazon DocumentDB, Valkey (Amazon ElastiCache), RabbitMQ (Amazon MQ) | | GCP | VPC, Cloud DNS, GKE | PostgreSQL (Cloud SQL), Valkey (Memorystore) | | Azure | Virtual network, DNS, AKS | PostgreSQL (Azure Database for PostgreSQL flexible server), Azure Cache for Redis, Cosmos DB with the MongoDB API | > [!CAUTION] > > On GCP, the templates create no MongoDB-compatible database and no RabbitMQ broker. On Azure, they create no RabbitMQ broker. Provision these services yourself on those clouds. Treat the instance classes in the example files as a starting point, not as a capacity plan. Before a production deployment, review the instance classes, storage, and connection limits against your transaction volume. Then read the important information section of the repository README. ## AWS *** On AWS, a command-line tool runs the templates. It runs each stack in order and makes sure that the live credentials match the AWS account of the environment. To install the tool and run it, follow the AWS section of the [repository README](https://github.com/LerianStudio/lerian-terraform-foundation#aws). You need Terraform 1.10.0 or later, the `aws` CLI, and `git`. The deployment of one environment runs these stacks in this order: 1. `bootstrap`: the state bucket and the lock table. Run it once per environment. 2. `infra-base`: the VPC, then the EKS cluster. 3. `midaz`: the Midaz datastores. After the `midaz` stack, a separate run of the tool with `--action helm-values` writes a Helm values file from the stack outputs. The file contains no passwords. It references the secrets that the templates store in AWS Secrets Manager. > [!NOTE] > > Amazon MQ accepts AMQPS connections only. If you write the values yourself, set `RABBITMQ_URI` to `amqps`. ## GCP and Azure *** On GCP and Azure, an interactive script runs the templates. Each component directory has its own `backend.tf` and `midaz.tfvars` files. You need Terraform and the CLI of your cloud provider: `gcloud` for GCP or `az` for Azure. **1. Create the state bucket** GCP: ``` gsutil mb -p PROJECT_ID -l REGION gs://BUCKET_NAME gsutil versioning set on gs://BUCKET_NAME ``` Azure: ``` az group create --name RESOURCE_GROUP --location LOCATION az storage account create --name STORAGE_ACCOUNT --resource-group RESOURCE_GROUP \ --location LOCATION --sku Standard_LRS az storage container create --name CONTAINER --account-name STORAGE_ACCOUNT ``` **2. Configure each component** ``` cd examples// cp midaz.tfvars-example midaz.tfvars ``` Replace the placeholders in `backend.tf` with your state bucket details. Then replace the placeholders in `midaz.tfvars` with your values. **3. Run the script** From the repository root, run: ``` ./deploy-legacy.sh ``` Select the cloud provider and the action: deploy or destroy. The script stops before it runs Terraform if a `backend.tf` file still contains a placeholder. It deploys every component in a fixed order and stops at the first failure. A destroy runs in the reverse order. | Order | Component | GCP directory | Azure directory | | - | - | - | - | | 1 | Network | `vpc` | `network` | | 2 | DNS | `cloud-dns` | `dns` | | 3 | PostgreSQL | `cloud-sql` | `database` | | 4 | Valkey or Redis | `valkey` | `redis` | | 5 | MongoDB | — | `cosmosdb` | | 6 | Kubernetes | `gke` | `aks` | ## Authenticate to your cloud provider *** Use the authentication tools of your cloud provider CLI instead of raw credentials: ``` aws sso login --profile your-profile gcloud auth application-default login az login ``` Follow the guidance of your cloud provider: * **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). Do not commit credentials to version control. ## Install Midaz *** After the infrastructure is ready, install Midaz on the cluster with Helm. On AWS, use the values file that the tool writes. On GCP and Azure, read the endpoints with `terraform output` and set them in your values file. For the chart and its values, see [Deploying using Helm](https://github.com/LerianStudio/helm/tree/main/charts/midaz). ## Security tips *** To keep your Midaz infrastructure secure, follow these recommendations: * Use **private Kubernetes clusters** to limit public exposure. The templates do not create a VPN. * Set up and **enforce RBAC** (Role-Based Access Control) to manage user permissions. * Store all secrets in the cloud provider’s **secret management service**. * Give service accounts only the **permissions they need**. ## Need help? *** * Read the README inside the directory that you deploy. * Search the existing [issues](https://github.com/LerianStudio/lerian-terraform-foundation/issues), or open a new issue. # Midaz and plugin compatibility Source: https://docs.lerian.studio/en/products/midaz/midaz-version-compatibility Which capabilities the unified Ledger serves as modules, and which ones run as separate plugins. ## Midaz application v4 *** | Capability | Runs as | API surface | | :- | :- | :- | | CRM | Unified Ledger module | `/v2` only | | Fees | Unified Ledger module | `/v2` only | | Pix and Bank Transfer | Separate plugins | See each plugin's API guide | In Midaz application v4, the unified Ledger contains CRM and Fees as modules. Use their `/v2` API through the Ledger. They are not separate services, ports, or plugins. The `/v1` compatibility surface does not run Fees or Tracer. Before Midaz v4, Fees ran as its own service. Its earlier releases do not define Midaz v4 compatibility. ## Upgrade order *** Upgrade Midaz before you upgrade its plugins. Plugins call the Midaz APIs, so a plugin that is newer than Midaz can call an API that Midaz does not serve yet. To install or upgrade Midaz, see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). # Observability Source: https://docs.lerian.studio/en/products/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. Observability in Midaz lets you monitor metrics, logs, and traces across your deployed environments. 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 receivers, processors, and exporters. The local configuration ships with `ENABLE_TELEMETRY=false`. Set it to `true` to send telemetry to the collector. For production and BYOC, install the Alloy Lerian collection agent instead; see the [Alloy Lerian chart README](https://github.com/LerianStudio/helm/tree/main/charts/alloy-lerian). * **Lerian side**: Lerian operates the managed collector and storage stack. > [!NOTE] > > 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. ## Grafana, OpenTelemetry, and tenant isolation *** * Unified view of metrics, logs, and traces in Grafana * Built on OpenTelemetry standards * Grafana preconfigured and operated by Lerian * Secure routing and tenant isolation > [!TIP] > > See [**Infra Tools → Observability**](/en/platform/observability) for advanced setup, collector configuration, and processor customization. # Security Source: https://docs.lerian.studio/en/products/midaz/security See how Midaz protects financial operations with security by design, STRIDE threat modeling, OWASP-aligned controls, and a clear shared responsibility model. 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 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/start-here/evaluate-and-deploy/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 Access Manager 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. > [!TIP] > > For step-by-step guidance, see [Security Recommendations](/en/products/midaz/security-recommendations). ## Identity and Access Management *** With authentication on, Midaz forwards each request's bearer token, together with the product, resource and action, to the configured authorization service. Midaz serves the request only when that service answers that the caller is authorized. Access Manager provides this service. Authentication is on when you set `PLUGIN_AUTH_ENABLED=true`, and Midaz refuses to start without it when you set `ENV_NAME=production`. Midaz takes the authorization service address from `PLUGIN_AUTH_HOST` or from service discovery, and protected requests fail when no authorization service is reachable. ### Option 1: External IAM An authorization service of your own must answer the same authorization request that Access Manager answers. ### Option 2: Access Manager Plugin The [Access Manager Plugin](/en/platform/access-manager) manages authentication and authorization for Midaz. It provides: * User lifecycle management * Session token handling * Refresh token rotation * Application registration and management ## Tenant isolation on Lerian Cloud *** On Lerian Cloud, 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. ### Credential isolation Each tenant has its own credentials. Midaz never shares them between tenants. One tenant can never authenticate into another tenant's data. Lerian generates and manages these credentials. They never sit in configuration files or environment variables. ### Per-tenant resource limits Lerian Cloud enforces 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. This bounds connection capacity per tenant. One tenant cannot exhaust the database connections that others need. > [!NOTE] > > 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. ## 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 posting discrepancies. ### Built-in safeguards Midaz applies strict validation across all transaction flows to keep data consistent and prevent logic errors: * Midaz refuses a transaction that would take an internal account's available balance below zero. When overdraft is enabled on the balance, Midaz records the shortfall as overdraft usage, within the overdraft limit when one is set. External accounts have no floor. * Midaz verifies account status before any operation. * Midaz requires a registered, valid asset before it posts an entry. ### Compliance with LGPD and GDPR Midaz encrypts the personal data fields of CRM holders and instruments before it stores them. Other data, such as metadata and descriptions, is stored as sent. Midaz handles transaction validation. 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. ## Responsible disclosure policy *** 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. > [!CAUTION] > > Do not disclose any findings publicly until we review and address them. The steps to report a vulnerability: **1. Report** Open a GitHub Security Advisory on the [Midaz repository](https://github.com/LerianStudio/midaz/security), or email us at [security@lerian.studio](mailto:security@lerian.studio). **2. Acknowledgment** We respond within 24 hours. **3. Verification** Our team validates the report. **4. Impact Assessment** We determine the severity and impact. **5. Resolution** We fix the issue and notify the reporter. **6. Public Disclosure** We coordinate disclosure with the researcher. # Security recommendations Source: https://docs.lerian.studio/en/products/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. At Lerian, we work under a **Security Shared Responsibility Model**. We secure the application layer: Midaz and all Lerian plugins. In BYOC, 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. ## 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. > [!TIP] > > Combine infrastructure-level protections with the built-in security validations of Midaz and its plugins. ## Midaz security configurations *** 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 Midaz runs as a consolidated ledger service. For local development, its environment template is `components/ledger/.env.example`. In a deployment, provide the production configuration through your orchestrator, and keep the secrets in your secret vault. > [!CAUTION] > > Never deploy Midaz with default credentials. Update all secret values in your environment before you launch. Keep database credentials, encryption keys, and API tokens out of the repository. Keep them in your secret vault and inject them at deployment time. Store secrets such as database credentials, encryption keys, and API tokens securely. Rotate them on a regular schedule, except `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY`. Midaz cannot read the holder and instrument data it encrypted with the previous values. ### TLS Certificate configuration When you expose Midaz services through ingress controllers, use valid TLS certificates. A certificate configured on ingress protects external-client traffic only as far as the ingress TLS termination point. It does not encrypt a plaintext ingress-to-service hop. Configure upstream TLS or mTLS when you must also protect that internal hop. Use a certificate from a trusted provider, and keep the full certificate chain in your secret vault. To set the certificate on a Midaz deployment, see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). > [!TIP] > > 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/products/midaz/updating-midaz Update your local Midaz environment, and find the upgrade procedure for a deployed environment. Keeping Midaz up to date means you work with the latest features, security patches, and improvements. Regular updates also keep Midaz compatible with other components and help prevent issues caused by outdated dependencies. Below are the steps to update Midaz in your local environment. For a deployed environment, see [Updating a deployment](#updating-a-deployment). > [!NOTE] > > Dedicated migration-runner images (`midaz-ledger-migrations`, `midaz-tracer-migrations`) apply the schema migrations. The apps do not migrate at startup. Local `make up` and the official Helm chart run these images for you. See [Database migrations](/en/products/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: **Step 1** Open your terminal and navigate to the `midaz` directory. **Step 2** Run the stop command: ```Shell Shell theme={null} make down ``` ### 2. (Optional) Clean up unused Docker resources Cleaning up unused resources frees disk space, removes stale Docker cache layers, and prevents conflicts from outdated images, containers, and volumes. To remove unused images and containers of every project on your machine: ```Shell Shell theme={null} docker system prune --all ``` To remove unused Docker volumes of every project on your machine: ```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 rebuild-up ``` ## Updating a deployment *** To install, upgrade, or uninstall a Midaz deployment, see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). It carries the procedure and the upgrade notes of each chart version. Before you upgrade, check these product rules: * In production, the Ledger does not start without authentication. See [Security](/en/products/midaz/security). * Keep `LCRYPTO_HASH_SECRET_KEY` and `LCRYPTO_ENCRYPT_SECRET_KEY` unchanged. Midaz cannot read the holder and instrument data that it encrypted with previous values. * Upgrade Midaz before its plugins. See [Midaz and plugin compatibility](/en/products/midaz/midaz-version-compatibility). * To upgrade to v4.2.0 or later, upgrade Tracer before the Ledger. While a Ledger on v4.2.0 or later runs, do not roll Tracer back to a version earlier than v4.2.0. * From v4.2.0, the Ledger reaches Tracer over gRPC only. When `TRACER_BASE_URL` is set, the Ledger does not start with `TRACER_TRANSPORT=rest`. If you set `TRACER_TRANSPORT=rest`, remove `TRACER_TRANSPORT` before you upgrade. Then set `TRACER_BASE_URL` to the Tracer gRPC address (`TRACER_GRPC_PORT`, `:4021` by default). See [Midaz environment variables](/en/products/midaz/midaz-environment-variables). * Run v4.2.0 or later on every Ledger instance before you set `deferrable` on a fee. After you set it, do not roll the Ledger back to an earlier version. See [Fee debt](/en/products/midaz/fees/fee-debt). # Midaz in Lerian Console Source: https://docs.lerian.studio/en/products/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. > **[Midaz in Lerian Console](/en/products/midaz/console/midaz-module)** > > Access the full documentation for using Midaz in the Lerian Console. ## Available actions *** From the Lerian Console, you can: * **Foundation**: Manage [organizations](/en/products/midaz/console/managing-organizations), [ledgers](/en/products/midaz/console/managing-ledgers-via-console), [assets](/en/products/midaz/console/managing-assets), and [account types](/en/products/midaz/console/managing-account-types) * **Accounting**: Configure [routes](/en/products/midaz/console/managing-accounting-routes) * **Accounts**: Manage [accounts](/en/products/midaz/console/managing-accounts), [holders](/en/products/midaz/console/managing-crm-holders), [aliases](/en/products/midaz/console/managing-crm-alias-accounts), [segments](/en/products/midaz/console/managing-segments), and [portfolios](/en/products/midaz/console/managing-portfolios) * **Transactions**: [View and create transactions](/en/products/midaz/console/managing-transactions) * **Fees**: Manage [fee packages](/en/products/midaz/fees/using-fee-engine-with-midaz-console) # Contexts and sources Source: https://docs.lerian.studio/en/products/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. You set up these two building blocks 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 to compare and which rules apply. * 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. Matching, exceptions, and reporting all depend on these two. ## What is a reconciliation context? *** A reconciliation context defines the operational boundaries of a reconciliation process. It specifies: * Which data sources to compare * Which matching rules apply * How to handle exceptions **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) Matcher reconciles each transaction against a single counterpart. **Typical use cases:** * Bank statements * Direct payment matching ### One-to-many (1:N) Matcher reconciles one transaction against multiple counterparts. **Typical use cases:** * Split payments * Batch deposits * Consolidated invoices ### Many-to-many (N:M) Matcher reconciles multiple transactions 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/products/matcher/configuration/matcher-schedules). #### 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 **name** `string` Descriptive name for the context **type** `string` Matching cardinality: `1:1`, `1:N`, or `N:M` **interval** `string` Required execution label (e.g. `daily`, `weekly`). It does not schedule runs. **feeToleranceAbs** `string` (default: 0) Absolute tolerance when Matcher compares an actual fee with the expected fee, as a decimal string (e.g. `"0.01"`) **feeTolerancePct** `string` (default: 0) Relative tolerance for the same fee comparison, as a fraction in a decimal string (`"0.02"` tolerates 2%) **feeNormalization** `string` Optional fee normalization mode: `NET` or `GROSS`. Omit it to leave fee normalization disabled. **autoMatchOnUpload** `boolean` (default: false) Automatically trigger a match run after a file upload **signConvention** `string` (default: SAME) How the context's data carries sign: `SAME` or `OPPOSITE`. With `SAME`, a payment carries the same sign as what it settles, and a negative amount is a reversal. With `OPPOSITE`, a payment carries the opposite sign of the invoice it settles. After the context carries a partial balance, Matcher refuses a change with `409`. It also refuses a change while a match run is in progress. **agedBreakThresholdDays** `integer` Optional aging limit for open items, in days (1 or more). In a `COMMIT` run, an open-item balance older than this limit raises an `AGED_BREAK` exception. Omit it to disable aging. On update, `0` disables it. **agedBreakCalendar** `string` Optional business-day calendar for `agedBreakThresholdDays`: `US_FED` or `BR_ANBIMA`. Omit it to count calendar days. #### 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, "signConvention":"SAME", "createdAt":"2026-02-02T16:31:22Z", "updatedAt":"2026-02-02T16:31:22Z" } ``` > [!TIP] > > API Reference: [Create context](/en/reference/products/matcher/create-context) ## Running reconciliation *** A context does not reconcile on its own. You trigger a **match run**. A run applies the context's rules to the transactions in its sources, then produces matches and exceptions. You can trigger runs by hand, or let a [schedule](/en/products/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. When asynchronous submission is unavailable, Matcher rejects `"async": true` with HTTP 503. > [!NOTE] > > Both modes return **HTTP 202 Accepted** on success, so read the response `status` to tell them apart. > > A synchronous run returns `COMPLETED`. When it fails, the request returns a problem response instead. 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`. > [!TIP] > > API Reference: > > * [Run match](/en/reference/products/matcher/run-match) > * [List match runs](/en/reference/products/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. The `config` object also holds import settings, such as [duplicate detection](/en/products/matcher/daily-reconciliation/matcher-uploading-files): ```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, 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" }' ``` > [!NOTE] > > `name`, `type`, and `side` are required (`name` is 1–50 characters). `config` is optional and defaults to an empty object when omitted. > [!TIP] > > API Reference: [Create source](/en/reference/products/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 | Rows the extraction engine pulls from a Discovery connection | ### Discovery sources `FETCHER` identifies a source type. Its `config` must carry `connection_id`, the id of a [Discovery](/en/products/matcher/integrations/matcher-discovery) connection, and each connection backs one `FETCHER` source. Rows that the extraction engine pulls from that connection land in this source. To pull on a schedule, add a [source binding](#source-bindings) on the query rail with the same `connectionId`. ```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": { "connection_id": "8f14e45f-ceea-4f6a-9d1b-2b6f3c7a9e10" } }' ``` ## 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 the default source listing, 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. Add `includeArchived=true` to include archived sources. | | 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 the default listing; bindings remain enabled until changed separately. | | Restore source | `POST /v1/contexts/{contextId}/sources/{sourceId}/restore` | Reactivates a previously archived source. | > [!TIP] > > API Reference: > > * [Create source](/en/reference/products/matcher/create-source) > * [Get source](/en/reference/products/matcher/retrieve-source) > * [Update source](/en/reference/products/matcher/update-source) > * [Archive source](/en/reference/products/matcher/archive-source) > * [Restore source](/en/reference/products/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 applies to each binding `kind`: * `file`: fetches files via a transport (populates `transportConfig`). * `query`: pulls rows through a discovery-engine connection (populates `connectionId`). The rows land in the `FETCHER` source whose `config.connection_id` matches, whatever source and `format` the binding names. See [Discovery](/en/products/matcher/integrations/matcher-discovery). Bindings live under `/v1/contexts/{contextId}/sources/{sourceId}/bindings`. > [!NOTE] > > Scheduled pulls run when Lerian enables the binding scheduler. A binding is dispatched only when it is enabled and 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/febraban-base", "scheduleSpec": "1h", "enabled": true }' ``` #### Fields **kind** `string` *required* Rail the scheduler pulls the source on: `file` or `query` (required). **connectionId** `string (UUID)` Query-rail discovery-engine connection. Required for `query`, rejected for `file`. **format** `string` *required* Declared format the binding produces (region/family-namespaced descriptor key, e.g. `br/cnab400/febraban-base`). **scheduleSpec** `string` *required* Go duration string the binding scheduler reads, such as `1h` or `30m`. Cron and `@every` syntax are invalid. **enabled** `boolean` Whether the scheduler can dispatch the binding when it is due. Defaults to `true`. Enabling it does not run it immediately. > [!TIP] > > API Reference: > > * [Create source binding](/en/reference/products/matcher/create-source-binding) > * [List source bindings](/en/reference/products/matcher/list-source-bindings) > * [Get source binding](/en/reference/products/matcher/get-source-binding) > * [Update source binding](/en/reference/products/matcher/update-source-binding) > * [Delete source binding](/en/reference/products/matcher/delete-source-binding) ## Managing contexts *** You can 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" }' ``` > [!TIP] > > API Reference: [Update context](/en/reference/products/matcher/update-context) ### Pause a context To temporarily keep a context out of 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 * Is reversible at any time with the [restore](#restore-a-context) endpoint > [!TIP] > > API Reference: [Archive context](/en/reference/products/matcher/archive-context) ### Restore a context Restoring reverses an archive. It moves the context from `ARCHIVED` back to `DRAFT`, so you can review and reconfigure the context before you reactivate it. ```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 > [!TIP] > > API Reference: [Restore context](/en/reference/products/matcher/restore-context) ### Clone a context To duplicate an existing context with its sources, rules, fee rules, and field maps, use the clone endpoint. Use it to create templates. Cloned fee rules keep referencing the same fee schedules as the source context. Matcher does not copy the fee schedules themselves. ```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 Matcher copied. A successful clone returns in `ACTIVE` status. > [!TIP] > > API Reference: [Clone context](/en/reference/products/matcher/clone-context) ## Context lifecycle *** A reconciliation context follows a lifecycle that controls when matching can run and how data is preserved. * A context starts in **Draft**, where you configure sources and settings. * 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 you enable fee normalization. * An active context can be temporarily **Paused** to stop execution without affecting configuration or historical data. * When you no longer need a context, move it to **Archived** with 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. ![Matcher Context Lifecycle](https://mintcdn.com/lerian-49cb71fc/a5129LsdHp16c4y9/images/en/d2/matcher-context-lifecycle.svg?fit=max\&auto=format\&n=a5129LsdHp16c4y9\&q=85\&s=9af73ee89f85f0096b92e9e4d9529a69) Lifecycle of a Matcher context ## Best practices *** > **Use descriptive names** > > Use explicit names that reflect accounts, systems, and purpose. > **Start with conservative thresholds** > > Favor accuracy over automation initially. Adjust thresholds based on observed results. > **Separate concerns** > > Use multiple contexts instead of a single broad reconciliation. ## Next steps *** > **[Field mapping](/en/products/matcher/configuration/matcher-field-mapping)** > > Define how source fields map to Matcher's schema. > **[Match rules](/en/products/matcher/configuration/matcher-match-rules)** > > Configure the rules that drive reconciliation. # Exception routing Source: https://docs.lerian.studio/en/products/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 exceptions. ## Severity classification *** Matcher classifies exceptions automatically from amount and age to support review prioritization. The amount is the absolute value of the transaction's base amount, or of its amount when it has no base amount. For an aged open item, it is the absolute value of the open balance. ### Default severity rules | Severity | Default amount or age criteria | | - | - | | **Critical** | Amount ≥ 100,000 OR age ≥ 120 hours | | **High** | Amount ≥ 10,000 OR age ≥ 72 hours | | **Medium** | Amount ≥ 1,000 OR age ≥ 24 hours | | **Low** | All other cases | Matcher caps exceptions with reason `FEE_DATA_MISSING` 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`. > [!NOTE] > > 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 stores an SLA due date only when an inbound callback supplies `dueAt`. It does not derive a deadline from severity, emit automatic SLA warnings, escalate exceptions through an SLA workflow, or route them automatically. Dashboard aggregates can report compliance for those externally supplied due dates. Define and enforce the SLA policy in the external system that sends the callback. ## Additional exception endpoints *** Matcher provides endpoints for advanced exception workflows: | Endpoint | Method | Description | | - | - | - | | [Dispatch exception](/en/reference/products/matcher/dispatch-exception) | `POST` | Attempt caller-selected dispatch without changing exception status | | [Process callback](/en/reference/products/matcher/process-exception-callback) | `POST` | Apply a token-authenticated, idempotent external update | | [Bulk assign](/en/reference/products/matcher/bulk-assign-exceptions) | `POST` | Assign exceptions to one `assignee` string | | [Bulk resolve](/en/reference/products/matcher/bulk-resolve-exceptions) | `POST` | Resolve multiple exceptions independently | | [Bulk dispatch](/en/reference/products/matcher/bulk-dispatch-exceptions) | `POST` | Dispatch multiple exceptions independently | | [List comments](/en/reference/products/matcher/list-exception-comments) | `GET` | Retrieve all comments on an exception | | [Add comment](/en/reference/products/matcher/add-exception-comment) | `POST` | Add a comment to an exception for audit and collaboration | | [Delete comment](/en/reference/products/matcher/delete-exception-comment) | `DELETE` | Remove a comment from an exception | | [List disputes](/en/reference/products/matcher/list-disputes) | `GET` | Retrieve all disputes with filtering | | [Get dispute](/en/reference/products/matcher/retrieve-dispute) | `GET` | Retrieve details of a specific dispute | | [Open dispute](/en/reference/products/matcher/open-dispute) | `POST` | Flag an exception as disputed for escalated review | | [Close dispute](/en/reference/products/matcher/close-dispute) | `POST` | Close a dispute with a resolution | | [Submit evidence ](/en/reference/products/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 expect partial success. 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. ### Dispatch targets When dispatching an exception, set the `targetSystem` field to the following value: | Target | Description | | - | - | | `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`. Dispatch does not perform bidirectional synchronization. ### Filtering by external system When listing exceptions, the `external_system` query parameter returns the exceptions whose external system equals the value exactly. Only inbound callbacks set that value. Dispatch does not. ## Queue summaries and observability *** The exception list exposes queue-scoped summary counts. Dashboard aggregates expose SLA compliance counts for externally supplied due dates. Matcher does not expose routing-rule distribution or integration success and failure analytics. Use your external observability stack for those operational signals. ## Best practices *** > **Review automatic severity** > > Use the classified severity to prioritize review, and account for the `FEE_DATA_MISSING` cap at `MEDIUM`. > **Use stable assignee values** > > Pass a stable identifier in the opaque `assignee` string and resolve ownership in your identity system. > **Track SLAs externally** > > Define deadlines, warnings, and escalation in your workflow system because Matcher does not enforce them. > **Inspect every bulk result** > > Treat bulk operations as independent per ID and handle partial success explicitly. > **Secure callbacks** > > Protect callback tokens and use stable idempotency keys when external systems update exception status. ## Next steps *** > **[Resolving exceptions](/en/products/matcher/daily-reconciliation/matcher-resolving-exceptions)** > > Resolve exceptions through the API or external systems. > **[Webhooks & callbacks](/en/products/matcher/integrations/matcher-webhooks-callbacks)** > > Advanced event delivery and callback handling. # Fee rules Source: https://docs.lerian.studio/en/products/matcher/configuration/matcher-fee-rules Configure fee schedules and predicate rules. Matcher checks matched transactions against the expected fee and, with NET or GROSS normalization, adjusts comparison amounts. Fee handling in Matcher is split across two entities: * 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. Modeling expected fees this way lets Matcher check matched transactions against the expected fee. With fee normalization enabled, Matcher also accounts for the fee before comparing transactions, when the transaction and schedule currencies match. ## 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 you expect and document the difference. Fee schedules describe the charge, and fee rules select when to use it. ## 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. You create a **fee schedule** once at the tenant level. Many rules across many contexts can reuse it. 2. You create a **fee rule** inside a context. It sets a `side`, a `feeScheduleId`, a `priority`, and a list of `predicates`. 3. When Matcher processes a context, 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 *when* the fee applies by editing the rule, without touching the schedule. Editing a schedule changes only its name, application order, rounding scale, and rounding mode. To change its items or currency, create a new schedule and point the rule's `feeScheduleId` at it. > [!NOTE] > > A fee rule alone does not change comparison amounts. It changes them only when the context sets `feeNormalization` to `NET` or `GROSS`, the relevant side has rules, and the transaction and schedule currencies match. Once a context has fee rules, every `COMMIT` run also checks matched transactions against the expected fee and raises `FEE_VARIANCE`, `FEE_DATA_MISSING`, or `FEE_CURRENCY_MISMATCH` exceptions. ## 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" } } ] }' ``` > [!TIP] > > API Reference: > > * [Create fee schedule](/en/reference/products/matcher/create-fee-schedule) > * [List fee schedules](/en/reference/products/matcher/list-fee-schedules) > * [Retrieve fee schedule](/en/reference/products/matcher/retrieve-fee-schedule) > * [Update fee schedule](/en/reference/products/matcher/update-fee-schedule) > * [Delete fee schedule](/en/reference/products/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. > [!TIP] > > API Reference: [Simulate fee calculation](/en/reference/products/matcher/simulate-fee-schedule) > [!NOTE] > > A fee schedule cannot be deleted while a fee rule or fee-variance history references it. The delete request returns `409 Conflict` with code `MTCH-0108`. When fee rules block the deletion, the response lists their contexts in the `errors[]` entry whose `location` is `fee-schedule.delete.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" } ] }' ``` > [!TIP] > > API Reference: > > * [Create fee rule](/en/reference/products/matcher/create-fee-rule) > * [List fee rules](/en/reference/products/matcher/list-fee-rules) > * [Retrieve fee rule](/en/reference/products/matcher/retrieve-fee-rule) > * [Update fee rule](/en/reference/products/matcher/update-fee-rule) > * [Delete fee rule](/en/reference/products/matcher/delete-fee-rule) ## End-to-end flow *** Expected-fee handling follows three steps: **1. Create the fee schedule** 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`. **2. Create the fee rule in the context** 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. **3. Apply during matching** Set the context's `feeNormalization` to `NET` or `GROSS`. The context then needs at least one fee rule to activate. Until it has one, activation readiness reports `context.activation.requirement.fee-rule`. 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 *** > **Reuse schedules across contexts** > > 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. > **Keep priorities unique and intentional** > > 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. > **Scope rules with precise predicates** > > 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. > **Simulate before wiring a schedule into a rule** > > Use `POST /v1/fee-schedules/{scheduleId}/simulate` to confirm a schedule produces the expected fee for representative amounts before referencing it from a rule. > **Repoint rules before deleting a schedule** > > A schedule referenced by a rule or fee-variance history cannot be deleted (`409 MTCH-0108`). When fee rules block it, the response lists their contexts in `errors[]`. Update or remove current rule references first; historical variance references continue to block deletion. ## Next steps *** > **[Match rules](/en/products/matcher/configuration/matcher-match-rules)** > > Configure how Matcher compares transactions once it accounts for expected fees. > **[Exception routing](/en/products/matcher/configuration/matcher-exception-routing)** > > Review explicit assignment, dispatch, and callback behavior for exceptions that fee handling doesn't cover. > **[Fee schedules API](/en/reference/products/matcher/create-fee-schedule)** > > Full API reference for fee schedule endpoints. > **[Fee rules API](/en/reference/products/matcher/create-fee-rule)** > > Full API reference for fee rule endpoints. # Field mapping Source: https://docs.lerian.studio/en/products/matcher/configuration/matcher-field-mapping Map each source's raw columns to Matcher's canonical fields: external_id, amount, currency, date, plus optional fields per source. A field map tells Matcher which raw column in a source carries each canonical transaction field. Every source names its columns differently: bank statements, ledger exports, gateway reports. The field map normalizes those column names into one fixed vocabulary before matching runs. ## 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. 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. ## Canonical vocabulary *** Matcher uses a closed key space. These are the only keys Matcher accepts. ### Required keys Every field map must declare all four: | Key | Description | | - | - | | `external_id` | 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 | | `direction` | Object that names a debit or credit column (see below) | > [!NOTE] > > `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. A column with any name, for example `mdr_fee`, can therefore carry fees end to end without hand-built metadata. Omit them and behavior is identical to a map without a fee slot. > [!NOTE] > > `direction` is for a file that marks each row as a debit or a credit in a column of its own. Its value is an object with three non-empty strings, for example `{"column": "DC", "debit": "D", "credit": "C"}`. `column` names that column, and `debit` and `credit` are the two values it holds. Matcher compares these values ignoring case, and they must differ. A debit row imports with a negative amount and a credit row with a positive amount. The amount column must then hold unsigned amounts. Matcher rejects a row with a signed amount, or with a direction cell that is empty or holds neither value. `direction` applies to CSV, JSON, and XML uploads only. ## Creating a field map *** You create one field map **per source**. A second field map for the same source is refused with `409`. Update the existing one instead. 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" } ``` > [!TIP] > > API Reference: [Create field map](/en/reference/products/matcher/create-field-map) ## Updating a field map *** To change a mapping, `PATCH` the field map 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" } }' ``` > [!TIP] > > API Reference: [Update field map](/en/reference/products/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. Match rules can compare them directly, even though one file called the amount `Amount` and the other called it `value`. ## Common mistakes *** > **Reversing the direction** > > 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 Matcher rejects the map. > **Using keys outside the vocabulary** > > Matcher rejects keys outside the vocabulary, such as `transaction_id`, `reference`, `counterparty`, or `type`, as unknown keys. The error names each offender. > **Wrong case** > > Keys are case-sensitive lowercase tokens. Matcher treats `External_Id`, `Amount`, or `CURRENCY` as unknown keys. > **Missing a required key** > > 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. > **Empty or non-string column names** > > A column name must be a non-empty string. Matcher rejects `null`, numbers, objects, or `""` in its place. > **Dropping a duplicate-key field** > > A source `config` can declare `duplicate_key`, the ordered list of fields that identify a duplicate row. The default key is `external_id` alone. The field map must map every field that `duplicate_key` names. Matcher rejects a field map that leaves one of them out. > **Expecting transformations** > > Field maps do not parse dates, divide amounts, or concatenate columns. Deliver values already in the expected shape from the source file, or normalize upstream before upload. ## Next steps *** > **[Match rules](/en/products/matcher/configuration/matcher-match-rules)** > > Define how Matcher compares and groups the canonical fields. > **[Uploading files](/en/products/matcher/daily-reconciliation/matcher-uploading-files)** > > Import transactions using your field maps. # Match rules Source: https://docs.lerian.studio/en/products/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. The policy sets how strict or forgiving Matcher is when it decides 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 external IDs 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. This approach stops any rule from reusing a higher-priority match. Progressively looser rules process the transactions that remain. ## 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. DATE\_LAG always proposes a match for review and never auto-confirms, whatever its score. * **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**: Truncated or reformatted external IDs, where the ID 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 **matchAmount** `Boolean` (default: true) Require exact amount match **matchCurrency** `Boolean` (default: true) Require exact currency match **matchDate** `Boolean` (default: true) Require exact date match **matchReference** `Boolean` (default: true) Require exact reference match **datePrecision** `String` (default: DAY) Date comparison precision: `DAY` or `TIMESTAMP` **caseInsensitive** `Boolean` (default: true) Case-insensitive reference comparison **referenceMustSet** `Boolean` (default: false) Require reference to be present on both sides **matchBaseAmount** `Boolean` (default: false) Require equal base (converted) amounts, in addition to the `matchAmount` check **matchBaseCurrency** `Boolean` (default: false) Require the same base currency, in addition to the `matchCurrency` check **matchScore** `Integer` (default: 100) Accepted and validated, but **inert**. It does not change the calculated confidence score (see note below) **matchBaseScore** `Integer` (default: 90) Accepted and validated, but **inert**. It does not change the calculated confidence score (see note below) > [!NOTE] > > **`matchScore` and `matchBaseScore` are 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). Setting them does **not** alter the confidence score or auto-confirm behavior. See [Confidence scoring](/en/products/matcher/reference/matcher-confidence-scoring). The response echoes the persisted rule with its assigned `id` and timestamps. > [!TIP] > > API Reference: [Create match rule](/en/reference/products/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 **percentTolerance** `Decimal` Percentage threshold applied to `percentageBase` (0.005 = 0.5%). Defaults to `0`. Matcher compares this threshold with `absTolerance` and uses the larger one **absTolerance** `Decimal` 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. **dateWindowDays** `Integer` Number of days allowed between transaction dates **roundingScale** `Integer` Decimal places for rounding **roundingMode** `String` Rounding strategy: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE` **percentageBase** `String` (default: MAX) Base for percentage calculation: `MAX`, `MIN`, `AVERAGE`, `LEFT`, or `RIGHT` **matchCurrency** `Boolean` (default: true) Require currency match **matchReference** `Boolean` (default: true) Require reference match **caseInsensitive** `Boolean` (default: true) Case-insensitive reference comparison **referenceMustSet** `Boolean` (default: false) Require reference to be present on both sides **matchBaseAmount** `Boolean` (default: false) Match on base (converted) amount **matchBaseCurrency** `Boolean` (default: false) Match on base currency **matchScore** `Integer` (default: 85) Accepted and validated, but **inert**. It does not change the calculated confidence score **matchBaseScore** `Integer` (default: 80) Accepted and validated, but **inert**. It 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 **minSimilarity** `Decimal` (default: 0.80) Minimum normalized reference similarity (0–1) required to gate as a match **matchAmount** `Boolean` (default: true) When `true`, require an exact amount match **matchCurrency** `Boolean` (default: true) When `true`, require an exact currency match **matchDate** `Boolean` (default: true) When `true`, require an exact date match **datePrecision** `String` (default: DAY) Date comparison precision: `DAY` or `TIMESTAMP` **referenceMustSet** `Boolean` (default: true) Require a non-empty reference on both sides **matchScore** `Integer` (default: 70) Accepted and defaulted to `70`, but **inert**. It does not cap or change calculated confidence or auto-confirm behavior > [!NOTE] > > 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 **maxDays** `Integer` Maximum number of days difference allowed **minDays** `Integer` (default: 0) Minimum number of days difference required **inclusive** `Boolean` (default: true) Whether the boundary days are inclusive **direction** `String` (default: ABS) How to measure the lag: `ABS` (absolute), `LEFT_BEFORE_RIGHT`, or `RIGHT_BEFORE_LEFT` **feeTolerance** `Decimal` (default: 0) Allowed amount difference to account for fees **matchScore** `Integer` (default: 80) Accepted and validated, but **inert**. It does not change the calculated confidence score. DATE\_LAG rules always score the reference component as 0, which caps the maximum score at 90 **matchCurrency** `Boolean` (default: true) 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 | | `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. Each priority is an integer from 1 to 1000 and is unique within the context. Matcher refuses a priority that another rule already uses with `409` (`MTCH-0102`). ### 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" ] }' ``` Matcher then renumbers every rule in the context from 1, in the new order. Rules that you leave out of the list follow the listed rules, in their previous order. > [!TIP] > > API Reference: [Reorder match rules](/en/reference/products/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 only the `runId` and `status`. It does not create exceptions, but Matcher completes and persists the `MatchRun` with statistics and emits its completion event. Read the statistics with `GET /v1/matching/runs/{runId}`. ## 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 Each item is the full rule, in the same shape as the create response, including `config`. ```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 } ``` > [!TIP] > > API Reference: [List match rules](/en/reference/products/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 } }' ``` `config` replaces the whole stored configuration. Matcher does not merge it with the previous one, so a key that you omit returns to its default. > [!TIP] > > API Reference: [Update match rule](/en/reference/products/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" ``` Matcher refuses the delete with `409` (`MTCH-0118`) while a confirmed match group that this rule created still has an exception attached. > [!TIP] > > API Reference: [Delete match rule](/en/reference/products/matcher/delete-match-rule) ## Best practices *** > **Start strict, then loosen** > > Lead with exact rules. Add tolerance rules only for the variance you can justify and explain. > **Dry-run every change** > > Treat rule updates as production changes. Validate match rates and exception volume before committing. > **Review rule output over time** > > If a rule never matches, it may be unnecessary. If it matches too often, it may be too broad. > **Keep loose rules at low priority** > > High tolerance increases false positives. Use it as a fallback and review results carefully. ## Next steps *** > **[Exception routing](/en/products/matcher/configuration/matcher-exception-routing)** > > Configure classification, assignment, and escalation for unmatched transactions. > **[Confidence scoring](/en/products/matcher/reference/matcher-confidence-scoring)** > > Understand how scores are calculated and how thresholds impact automation. # Schedules Source: https://docs.lerian.studio/en/products/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 starts a `COMMIT` match run for that context with its rules and sources. The run reconciles only when the context is `ACTIVE`. | 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}` | > [!TIP] > > API Reference: > > * [Create schedule](/en/reference/products/matcher/create-schedule) > * [List schedules](/en/reference/products/matcher/list-schedules) > * [Get schedule](/en/reference/products/matcher/retrieve-schedule) > * [Update schedule](/en/reference/products/matcher/update-schedule) > * [Delete schedule](/en/reference/products/matcher/delete-schedule) ## Creating a schedule *** Provide a `cronExpression`. The `enabled` field 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 }' ``` **cronExpression** `String` *required* Cron expression defining the run frequency (1–100 chars), with at least five minutes between firings. Use five fields: minute, hour, day of month, month, and day of week. Matcher rejects a seconds field, macros such as `@daily`, and per-minute and sub-minute schedules **enabled** `Boolean` 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. **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 stay 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 take a break, disable instead. ## Best practices *** > **Align cadence with source availability** > > Schedule runs to fire after all sources for the context have delivered their data for the period. Running before ingestion completes produces avoidable exceptions. > **Prefer disabling over deleting** > > To pause a reconciliation cadence, disable the schedule. It keeps history and configuration, and re-enabling is a single call. > **Use explicit cron expressions** > > Keep cron expressions readable and documented (e.g. `0 0 * * *` = daily at 00:00). Matcher evaluates them in UTC. ## Next steps *** > **[Contexts and Sources](/en/products/matcher/configuration/matcher-contexts-and-sources)** > > Configure the contexts and sources a schedule reconciles. > **[Generating Reports](/en/products/matcher/daily-reconciliation/matcher-generating-reports)** > > Review the results produced by scheduled runs. # Generating reports Source: https://docs.lerian.studio/en/products/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**: Matched and unmatched counts and amounts for the date range. * **Match detail report**: A list of matched transactions with their match group, source, amount, currency, and date. * **Unmatched report**: A list of transactions that remain unmatched for follow-up. * **Exception report**: A list of exceptions created in the date range, with severity, status, reason, and due date. * **Variance report**: Expected and actual fee totals per source, fee schedule, and currency, with the net variance and its percentage. Each variance row also carries `adjustmentsApplied`, the signed sum of the adjustments recorded against it, and `outstandingVariance`, the variance that remains after them. Each variance page carries `unattributedAdjustments`: per-currency totals of the adjustments that no single row includes. Use the reporting dashboard endpoints to access reconciliation metrics and export data. > [!TIP] > > API Reference: > > * [Dashboard aggregates](/en/reference/products/matcher/get-dashboard-aggregates) > * [Export matched report](/en/reference/products/matcher/export-matched-report) > * [Export unmatched report](/en/reference/products/matcher/export-unmatched-report) ## Dashboard analytics *** The reporting dashboard provides 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 | | `unmatchedByCurrency` | Unmatched amounts split by currency | | `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`). > [!NOTE] > > There is no bare `GET /v1/reports/contexts/{contextId}` endpoint. The typed sub-paths under `/v1/reports/contexts/{contextId}/...` carry report data, for example `dashboard`, `summary`, `matched`, `unmatched`, and `variance` (all except `dashboard` have an `/export` variant, as does `exceptions`). > [!TIP] > > API Reference: [Get dashboard aggregates](/en/reference/products/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" ``` > [!TIP] > > API Reference: [Get source breakdown](/en/reference/products/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. > [!TIP] > > API Reference: [Get cash impact](/en/reference/products/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 you provide an invalid cursor value, the API returns a `400 Bad Request` error with a message indicating the pagination parameters are invalid. ## Exports *** The `summary`, `matched`, `unmatched`, `variance`, and `exceptions` reports each have a synchronous `/export` operation that returns the file in the response. Choose the format with `format=csv` (the default) or `format=pdf`. A CSV export holds at most 100,000 records, and a PDF export holds at most 25,000. ```bash cURL theme={null} curl -X GET "https://api.matcher.example.com/v1/reports/contexts/{contextId}/matched/export?date_from=2025-01-01&date_to=2025-01-31&format=csv" \ -H "Authorization: Bearer $TOKEN" \ -o matched_report.csv ``` The matched, unmatched, variance, and exceptions reports also run as asynchronous export jobs, with a window of up to 365 days and CSV, JSON, or XML files. See [Exports & disputes](/en/products/matcher/reference/matcher-export-disputes). ## Quick counts *** Use count endpoints for lightweight status checks without fetching full result sets: | Endpoint | Description | | - | - | | [Count matches](/en/reference/products/matcher/count-matches) | Total matched items in a date range | | [Count transactions](/en/reference/products/matcher/count-transactions) | Total transactions in a date range | | [Count exceptions](/en/reference/products/matcher/count-exceptions) | Total exceptions in a date range | | [Count unmatched](/en/reference/products/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 *** > **Schedule daily summaries** > > Automate a daily summary report delivered each morning to keep stakeholders aligned. > **Archive exports for compliance** > > Store reports in secure, durable storage. Financial artifacts often require multi-year retention. > **Use filters to stay actionable** > > Generate targeted reports by date and source. Avoid exporting everything by default. > **Monitor report jobs** > > Large reports can fail or stall. Export jobs use `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED`, `EXPIRED`, and `CANCELED`. Alert on `FAILED` or prolonged `RUNNING`. ## Next steps *** > **[Contexts and Sources](/en/products/matcher/configuration/matcher-contexts-and-sources)** > > Set up reconciliation contexts and data sources. > **[Security](/en/products/matcher/reference/matcher-security)** > > Learn how access control and data protection work in Matcher. # Resolving exceptions Source: https://docs.lerian.studio/en/products/matcher/daily-reconciliation/matcher-resolving-exceptions Review, prioritize, and resolve transactions Matcher couldn't reconcile automatically using severity, lifecycle, and audit-friendly actions. 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 marks a transaction that needs your review. Common causes include: * **No candidate found**: No transaction in the other source meets the active rule criteria. * **Duplicate rejection**: A previous match was rejected and no alternative candidate remains. * **Source imbalance**: One source contains transactions that are not in 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. You must send a non-empty `assignee`. * Force match and adjust entry set `PENDING_RESOLUTION` while the operation is in progress. Success moves the exception to `RESOLVED`. * Direct resolution moves an `OPEN` or `ASSIGNED` exception to `RESOLVED`. * Dispatch writes a `DISPATCH` audit event and emits `exception.dispatched`. It does not change the exception status. ![Matcher Exception Lifecycle](https://mintcdn.com/lerian-49cb71fc/eJbUTctk-eLsW0J5/images/en/d2/matcher-exception-lifecycle.svg?fit=max\&auto=format\&n=eJbUTctk-eLsW0J5\&q=85\&s=69503da0352eca4a5625e9edeb50c176) The lifecycle of an exception in Matcher ### 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` | Records a dispatch to a target system. Body: `targetSystem` (required; `MANUAL` is the target value that records a dispatch), `queue` (optional). 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` | Marks the exception's transaction as matched and resolves the exception. Body: `overrideReason` (`POLICY_EXCEPTION`, `OPS_APPROVAL`, `CUSTOMER_DISPUTE`, or `DATA_CORRECTION`) and `notes`. Uses `PENDING_RESOLUTION` while the operation is in progress. | | 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. | | 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 up to 10,000 IDs of the exceptions that match the current filters (`contextId`, `status`, `severity`, `reason`, …). `total` gives the full count, and `truncated` is `true` when the list stops at the cap. Use it to drive a bulk selection before calling the bulk endpoints. *(read-only)* | > [!NOTE] > > `MANUAL` dispatch acknowledges the hand-off locally without calling an external system. > [!TIP] > > API Reference: > > * [Assign exception](/en/reference/products/matcher/assign-exception) > * [Dispatch exception](/en/reference/products/matcher/dispatch-exception) > * [Resolve exception](/en/reference/products/matcher/resolve-exception) > * [Force match](/en/reference/products/matcher/force-match-exception) > * [Adjust entry](/en/reference/products/matcher/adjust-entry-exception) > * [Get exception history](/en/reference/products/matcher/retrieve-exception-history) > * [Select exception IDs](/en/reference/products/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. The amount is the absolute value of the transaction's base amount, or of its amount when it has no base amount. For an aged open item, it is the absolute value of the open balance. | Severity | Criteria | | - | - | | **Critical** | Amount >= 100,000 OR age >= 120 hours | | **High** | Amount >= 10,000 OR age >= 72 hours | | **Medium** | Amount >= 1,000 OR age >= 24 hours | | **Low** | All others | Matcher caps an exception with reason `FEE_DATA_MISSING` at **Medium**, whatever its amount or age. These thresholds reprioritize the exception. They do not create an SLA deadline. An inbound callback can supply `dueAt`, and dashboard aggregates measure compliance for those externally supplied deadlines. Define the response-time policy in the external system that sends the callback. ### Severity escalation Each time a `COMMIT` matching run finds a transaction still unmatched, Matcher recalculates the severity of the transaction's unresolved exception, unless it is a duplicate-transaction exception. Age counts from the transaction date or, for an aged open item, from its obligation business date or first-seen time. The classification uses OR logic. Either the amount or the age threshold is enough to trigger a higher severity. A recalculated exception escalates as follows: * An exception under 1,000 starts as **Low**, but escalates to **Medium** after 24 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 Mark the exception's transaction as matched and resolve the exception. Force match adds the transaction to no match group and pairs it with no counterpart. To pair two transactions, create a manual match with `POST /v1/matching/manual`. **Use Force Match when:** * 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. * `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" }' ``` > [!TIP] > > API Reference: > > * [Bulk assign](/en/reference/products/matcher/bulk-assign-exceptions) > * [Bulk resolve](/en/reference/products/matcher/bulk-resolve-exceptions) > * [Bulk dispatch](/en/reference/products/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: ```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": "MANUAL", "queue": "RECON-TEAM" }' ``` ## Exception comments *** Comments give each exception an audit trail of investigation notes and team discussion. 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` | None (returns the full thread, oldest first) | | Delete comment | `DELETE /v1/exceptions/{exceptionId}/comments/{commentId}` | `commentId` in path | > [!TIP] > > API Reference: > > * [List comments](/en/reference/products/matcher/list-exception-comments) > * [Add comment](/en/reference/products/matcher/add-exception-comment) > * [Delete comment](/en/reference/products/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`. > [!TIP] > > API Reference: > > * [List disputes](/en/reference/products/matcher/list-disputes) > * [Get dispute](/en/reference/products/matcher/retrieve-dispute) > * [Open dispute](/en/reference/products/matcher/open-dispute) > * [Close dispute](/en/reference/products/matcher/close-dispute) ### Dispute states and transitions A new dispute is `OPEN`. Closing it sets `WON` or `LOST`, and both outcomes are terminal. ## Exception resolution workflow *** Use this flow to keep reviews consistent and audit-friendly. **1. Triage** Review the queue by severity and SLA. Start with Critical and High. **2. Investigate** Use the exception payload to understand what failed and what candidates exist. * Read `reason` to see why matching failed. * Call `GET /v1/matching/candidates` with the reconciliation context's `contextId` and the exception's `transactionId` to review near matches below threshold. * Look for patterns (same counterparty, recurring reference formats). **3. Resolve** 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 accept the transaction as matched without a counterpart. * **Adjust**: You need an adjusting entry for variance. **4. Document** Capture enough detail for someone else to replay your decision later: * What you checked * What you concluded * Links or IDs for supporting evidence **5. Dispatch if needed** If the exception requires external handling, dispatch it. Dispatch records the action but does not change the exception status. ## Best practices *** > **Work by severity and SLA** > > Start with Critical and High items. They carry the highest risk and the tightest deadlines. > **Make decisions auditable** > > 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 > **Fix patterns at the source** > > 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 > **Treat force matches as exceptions to the rule** > > If you force-match regularly, your rules or tolerances need attention. > **Assign work explicitly** > > Assign exceptions through the assignment endpoints. Matcher does not apply assignment rules automatically. ## Next steps *** > **[Generating Reports](/en/products/matcher/daily-reconciliation/matcher-generating-reports)** > > Create reconciliation reports, export results, and support audits. > **[Exception Routing](/en/products/matcher/configuration/matcher-exception-routing)** > > Review severity, SLA, and routing concepts for exceptions. # Reviewing matches Source: https://docs.lerian.studio/en/products/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, 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. * Engine-created matches that score 90 or above from EXACT and TOLERANCE rules are auto-confirmed immediately. Manual matches, created with `POST /v1/matching/manual`, start 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 or revoking a `CONFIRMED` group returns its transactions to the unmatched pool. The status change and the transaction return happen atomically. * When Matcher changes the status of a match's transactions, it changes all of them or none. A manual match that loses a race on a transaction gets `409`, and Matcher applies nothing. > [!NOTE] > > **FUZZY and DATE\_LAG matches are never auto-confirmed.** Auto-confirmation at score ≥ 90 applies to engine-created EXACT and TOLERANCE matches. A manual match starts in `CONFIRMED` status. A match produced by a FUZZY or DATE\_LAG rule stays in `PROPOSED`, regardless of its score, even when it scores 90+. You can reject it through the unmatch operation. There is no public manual-confirm operation. See [Confidence scoring](/en/products/matcher/reference/matcher-confidence-scoring#rule-types-that-never-auto-confirm). ![Match Status Lifecycle](https://mintcdn.com/lerian-49cb71fc/eJbUTctk-eLsW0J5/images/en/d2/matcher-match-status.svg?fit=max\&auto=format\&n=eJbUTctk-eLsW0J5\&q=85\&s=dc72053489e1132a8cd2dcde512d064d) Status lifecycle of a match. ### 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 what happens to the match. ### Confidence levels | Tier | Score range | Behavior | | - | - | - | | Auto-confirm eligible | 90-100 | 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. | ### Understanding the score Matcher calculates the confidence score 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 *** Matcher gives you three surfaces for working a review queue: match candidates, open items, and adjustments. ### List match candidates ```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 in the context and returns only 1:1 candidates from the opposite side. 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 that remain when a transaction nets only in part. 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` | An un-match took back the confirmed match that created the obligation. | The normal lifecycle flows `OPEN` → `PARTIALLY_CLEARED` → `CLEARED`, with any still-open item able to become `AGED` once it passes the aging threshold. `WITHDRAWN` is terminal. ### 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: **amount** `String` *required* Adjustment amount **currency** `String` *required* Three-character currency code. **direction** `String` *required* `DEBIT` or `CREDIT` **type** `String` *required* `BANK_FEE`, `FX_DIFFERENCE`, `ROUNDING`, `WRITE_OFF`, or `MISCELLANEOUS` **reason** `String` *required* Business reason for the adjustment **description** `String` *required* Human-readable description **matchGroupId** `UUID` Match group the adjustment applies to **transactionId** `UUID` Transaction the adjustment applies to Send at least one of `matchGroupId` or `transactionId`. To read the adjustments recorded against a context, use `GET /v1/matching/contexts/{contextId}/adjustments`. It accepts an optional `matchGroupId` filter, and pages with `cursor`. > [!TIP] > > API Reference: > > * [List match candidates](/en/reference/products/matcher/list-match-candidates) > * [List open items](/en/reference/products/matcher/list-open-items) > * [Create adjustment](/en/reference/products/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`. The next run can match them 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 mandatory and must not be empty. ### When to reject or revoke Common scenarios for rejecting or revoking a group: * **Incorrect match confirmed**: the transactions in a confirmed group 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 is a duplicate that you should remove ### What happens after unmatch When you unmatch a match group: 1. **Match status changes**: A `CONFIRMED` group becomes `REVOKED`. A still-`PROPOSED` group becomes `REJECTED`. 2. **Transactions returned**: All associated transactions revert to `UNMATCHED` status atomically with the status change. 3. **Confirmed effects withdrawn**: For a `CONFIRMED` group, Matcher also withdraws its open-item residuals, voids its fee variances, and withdraws the exceptions it raised. These changes and the status change happen in one transaction. 4. **Reason recorded**: The group stores the supplied rejection or revocation reason. 5. **Streaming event emitted**: A `match_group.unmatched` event is emitted only when the group was previously `CONFIRMED`. 6. **Re-matching possible**: after a successful unmatch, the next run can match the transactions again. ## Best practices *** > **Start with lowest confidence matches** > > Review matches with the lowest confidence scores first. These matches are the most likely to be wrong. Give them the most attention. > **Understand fixed confidence thresholds** > > The confidence thresholds do not change. Engine-created EXACT and TOLERANCE matches with scores of 90 or above are auto-confirmed. Manual matches start `CONFIRMED`. Scores between 60 and 89 remain `PROPOSED`. 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. > **Give a clear reason** > > 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. > **Reject only after checking the source data** > > Review the group and its source transactions before using the unmatch operation. Matcher exposes this operation per group. It does not expose bulk confirmation. > **Review high-value matches carefully** > > Regardless of confidence score, give extra attention to high-value matches. The impact of an incorrect match is proportional to the amount. ## Next steps *** > **[Resolving exceptions](/en/products/matcher/daily-reconciliation/matcher-resolving-exceptions)** > > Handle transactions with no automatic match. > **[Confidence scoring](/en/products/matcher/reference/matcher-confidence-scoring)** > > Learn in detail how Matcher calculates confidence scores. # Uploading files Source: https://docs.lerian.studio/en/products/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/products/matcher/imports/matcher-import-formats) for the full catalog. ## File structure requirements *** Each file must contain transaction records with fields you can map to Matcher's internal schema. ### Required fields Every transaction must have these fields (or mappable equivalents): | Field | Type | Description | | - | - | - | | `external_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 | | - | - | - | | `description` | String | Free-text description | | `fee_amount` | Decimal | Fee charged on the transaction | | `fee_currency` | String | Currency of the fee | | `direction` | Object | Column that marks each row as debit or credit. See [Debit and credit column](#debit-and-credit-column). | Matcher keeps unmapped columns as transaction metadata. ### Debit and credit column Some files mark each row as debit or credit in a separate column. To read the sign from that column, map the optional `direction` key to an object `{column, debit, credit}`: ```json theme={null} "direction": { "column": "type", "debit": "debit", "credit": "credit" } ``` A debit row imports with a negative amount, and a credit row imports with a positive amount. With `direction` mapped, the amount column must be unsigned. A signed amount, an empty direction cell, or an unknown direction value rejects the row. Matcher reads `direction` in CSV, JSON, and XML files only. ## Format examples *** ### CSV **CSV Requirements:** * First row must be column headers * UTF-8 encoding and comma delimiter by default (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:** * An array of objects, or a single object * Consistent field names across objects * UTF-8 encoding by default (configurable) **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:** * Well-formed XML * Record elements named `transaction`, `row`, `record`, `item`, or `entry` * UTF-8 encoding by default (configurable) **Code example** ```xml theme={null} BANK-2024-001 1500.00 USD 2024-01-15 Invoice #1234 credit BANK-2024-002 250.00 USD 2024-01-15 Service fee debit ``` ## 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" } ``` > [!TIP] > > API Reference: [Preview file](/en/reference/products/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" ``` > [!NOTE] > > Send the `format` field **before** the `file` part. If `file` arrives first, Matcher infers the format from the filename extension for `.csv` and `.json` only. Matcher never infers `.xml`, because it is a format family that covers plain XML and camt.053. Send the explicit `format` field for XML. Matcher rejects the upload without it. Matcher parses and stores the whole file inside the upload request. Upload returns **202 Accepted** with the finished job. When the import fails, Matcher answers with an error instead of the job. Matcher reads an idempotency key only from the `X-Idempotency` header, and only when you send one. A request that carries a key only in `X-Idempotency-Key` or `Idempotency-Key` is refused with `400` (`MTCH-0015`), and nothing is processed. A replayed answer carries the `X-Idempotency-Replayed` header. > [!NOTE] > > The upload limit is **1 GiB**. It counts the whole request body of the upload, including every part, part header, and boundary, not only the file. Matcher refuses an upload larger than the upload limit with `413` (`MTCH-0010`). It refuses an upload that stops sending data for too long with `408` (`MTCH-0311`). > [!TIP] > > API Reference: [Upload file](/en/reference/products/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": "COMPLETED", "fileName": "bank_statement_january.csv", "totalRows": 1250, "persistedRows": 1250, "droppedDuplicateRows": 0, "flaggedDuplicateRows": 0, "reviewRows": 0, "failureRatePercent": 0, "completedWithErrors": false, "startedAt": "2024-01-20T10:30:00Z", "completedAt": "2024-01-20T10:30:45Z", "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" ``` > [!TIP] > > API Reference: [Get import status](/en/reference/products/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", "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" } ``` > [!NOTE] > > 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. `reviewRows` counts rows with a blank `external_id` that Matcher imports with status `PENDING_REVIEW`. This happens when the source sets `blank_external_id` to `NEEDS_REVIEW` in its `config`. With the default, `REJECT`, a blank `external_id` fails the row. ### Import job status values | Status | Description | | - | - | | `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 **1. Format Validation** Verifies the file is valid CSV, JSON, or XML with correct structure. **2. Schema Validation** Checks that required fields are present and match the configured field map. **3. Data Type Validation** Validates amounts are valid decimals, dates are parseable, currencies are valid ISO codes. ### Common validation errors Each row error carries the row number, the field when one applies, and a message. Common messages: | Message | Cause | Solution | | - | - | - | | `missing required field` | A required field is blank in the row | Fill the field in the source file | | `invalid decimal amount` | Amount not a valid number | Check for currency symbols or thousands separators | | `invalid date format` | Date cannot be parsed | Use ISO 8601 format (YYYY-MM-DD) | | `invalid ISO 4217 currency code` | Currency code not recognized | Use ISO 4217 codes (USD, EUR, BRL) | ### Handling errors By default, Matcher imports valid rows even if some rows have errors. To fail the job when failed rows exceed a percentage, set `fail_on_error_rate_percent` (1 to 99) in the source `config`. ## Duplicate detection *** Matcher automatically detects and handles duplicate transactions to prevent double-counting. ### How duplicates are detected By default, a row is a duplicate when it repeats the `external_id` of a row from the same source, within the same upload or against already-persisted data. To detect duplicates on other fields, set `duplicate_key` in the source `config`. Its value is an ordered list of field-map names, such as `["external_id", "amount"]`. ### Duplicate handling options Set the `duplicate_policy` key in the source `config` to control handling: | Policy | Behavior | | - | - | | `FLAG_AS_EXCEPTION` (default) | Drops the repeat and raises a `DUPLICATE_TRANSACTION` exception on the surviving transaction (subset of `droppedDuplicateRows` reported as `flaggedDuplicateRows`) | | `KEEP_FIRST` | Keeps the first occurrence and silently drops repeats, counted in `droppedDuplicateRows` | | `REJECT` | Turns each repeated row into an ingestion row error, counted in `failedRows` | When `duplicate_policy` is absent, `FLAG_AS_EXCEPTION` applies. ### Viewing duplicate details The import summary shows the number of duplicates: ```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 "format=csv" \ -F "file=@bank_january.csv" # Upload ledger export curl -X POST "https://api.matcher.example.com/v1/imports/contexts/{contextId}/sources/{ledgerSourceId}/upload" \ -H "Authorization: Bearer $TOKEN" \ -F "format=csv" \ -F "file=@ledger_january.csv" ``` ### Wait for all imports Before running matching, make sure 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 } ``` > [!TIP] > > API Reference: [Search transactions](/en/reference/products/matcher/search-transactions) Supported filters include `amount_min`, `amount_max`, `date_from`, `date_to`, `currency`, `source_id`, `status`, exact `external_id` match via the `reference` parameter, and free-text search via the `q` parameter. ## Best practices *** > **Validate files before upload** > > 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 > ``` > **Use consistent date formats** > > Standardize on ISO 8601 format (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ`) across all sources to avoid parsing issues. > **Include transaction IDs** > > Always include unique transaction IDs from the source system. This enables proper duplicate detection and audit trails. > **Handle negative amounts consistently** > > Decide on a convention (negative for debits, positive for credits) and apply it consistently. When a file marks debits and credits in a separate column, map that column with the `direction` key instead. > **Upload incrementally for large files** > > 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. > **Set up automated uploads** > > For recurring reconciliation, automate file uploads using scheduled jobs. > > ```bash theme={null} > # Example: Daily upload via cron > 0 6 * * * /scripts/upload_bank_statement.sh > ``` ## Next steps *** > **[Reviewing Matches](/en/products/matcher/daily-reconciliation/matcher-reviewing-matches)** > > Learn how to interpret match results and confidence scores. > **[Field Mapping](/en/products/matcher/configuration/matcher-field-mapping)** > > Configure how source fields map to Matcher's schema. # Prerequisites Source: https://docs.lerian.studio/en/products/matcher/getting-started/matcher-prerequisites Check the credentials, network access, and file formats you need before you use Matcher for reconciliation. Before you use Matcher, confirm that you have what this page describes. ## Optional: reconciling Midaz data *** Matcher pairs with Midaz Ledger, but there is no live connector between them. Matcher opens no connection to Midaz. Reconciling Midaz data is entirely optional. Matcher works as a stand-alone product that reconciles 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. > [!NOTE] > > See the [Matcher and Midaz](/en/products/matcher/integrations/matcher-midaz-integration) guide for the full flow. ## Authentication *** Access Manager issues the token and checks the caller's permissions. ### Required permissions Fine-grained permissions control access to Matcher features: | Permission | Description | | - | - | | `contexts:create` | Create reconciliation contexts | | `contexts:read` | View context configuration | | `rules:create` | Create 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 | ## 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/products/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:** * The file must have a header row * UTF-8 encoding (configurable) * 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:** * UTF-8 encoding (configurable) * 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 (configurable) * Consistent element structure **Example:** ```xml theme={null} TXN-001 1000.00 USD 2024-01-15 Invoice payment ``` ### File size limits | Limit | Default | | - | - | | Maximum upload file size | 1 GiB | | Maximum buffered request body | 100 MiB | ## Network requirements *** ### Access to Matcher Your systems and users reach the Matcher API and the Matcher module of Lerian Console at the addresses Lerian provides. ### Access to your systems For these sources, Matcher connects to the system you name, and that system must accept connections from Matcher: | What you register | What Matcher connects to | | - | - | | Discovery connection | The database or API that the connection names. `GET /v1/discovery/connector-types` lists the available types | | Source binding on the `file` rail | The SFTP server, HTTPS endpoint, S3 bucket, or IMAP mailbox | Aggregator connections need your account with Pluggy or Belvo. Event delivery is optional. When Lerian enables it for you, Matcher publishes the events in [Matcher events](/en/reference/events/matcher) as CloudEvents. ## Checklist *** Before you start, confirm that: * **Credentials are available**: you have a token from Access Manager * **Network access works**: the systems you register accept connections from Matcher * **A web browser is available**: you use it to open the Matcher module of Lerian Console * **Sample data is ready**: you have transaction files for your first test (see [Quick Start](/en/products/matcher/getting-started/matcher-quick-start)) ## Next steps *** > **[Quick start](/en/products/matcher/getting-started/matcher-quick-start)** > > Run your first reconciliation. # Getting started with Matcher Source: https://docs.lerian.studio/en/products/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/products/matcher/matcher-developer-quick-start). ## The reconciliation lifecycle *** Every reconciliation in Matcher follows the same five-stage lifecycle: **1. Define scope** Create a context that describes what you reconcile and records its reconciliation interval. **2. Connect sources** Register the systems whose transactions you want to compare. **3. Set rules** Configure the criteria Matcher uses to pair transactions. **4. Run matching** Upload data and let Matcher find pairs, starting with a preview before committing. **5. Resolve exceptions** 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 do you reconcile?** 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. > [!TIP] > > API reference: > > * [Create context](/en/reference/products/matcher/create-context) > * [Update context](/en/reference/products/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 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 | > [!TIP] > > API reference: [Create source](/en/reference/products/matcher/create-source) | [Create field map](/en/reference/products/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. > [!TIP] > > API reference: [Create match rule](/en/reference/products/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 for `EXACT` and `TOLERANCE` groups. `FUZZY` and `DATE_LAG` groups always require review | | 60–89 | Medium confidence | Flagged for manual review | Scores derive from the matched components and their fixed weights. Exact and tolerance rules use the same weighting framework. Neither rule type inherently yields higher scores. > [!TIP] > > API reference: [Run match](/en/reference/products/matcher/run-match) | [List match run groups](/en/reference/products/matcher/list-match-run-groups) ## Resolve exceptions *** ### Exception severity Matcher classifies each exception by severity based on its amount and its age. The amount is the absolute value of the transaction's base amount, or of its amount when it has no base amount. For an aged open item, it is the absolute value of the open balance. | Severity | Criteria | | - | - | | **Critical** | Amount >= 100,000 or age >= 120 hours | | **High** | Amount >= 10,000 or age >= 72 hours | | **Medium** | Amount >= 1,000 or age >= 24 hours | | **Low** | All others | ### Resolution options * **Force match**: Mark the transaction as matched and resolve the exception. It takes only `overrideReason` and `notes`, and it pairs the transaction with no counterpart. To pair transactions, create a manual match with `POST /v1/matching/manual`. * **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**: Record a manual hand-off of the exception in its audit trail. This caller-directed action does not change its status. > [!TIP] > > API reference: > > * [List exceptions](/en/reference/products/matcher/list-exceptions) > * [Unmatch group](/en/reference/products/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 *** > **[Contexts and sources](/en/products/matcher/configuration/matcher-contexts-and-sources)** > > Full guide to setting up reconciliation contexts. > **[Match rules](/en/products/matcher/configuration/matcher-match-rules)** > > Learn about all rule types and configuration options in detail. > **[Field mapping](/en/products/matcher/configuration/matcher-field-mapping)** > > Map different file formats to Matcher's standard schema. > **[Resolving exceptions](/en/products/matcher/daily-reconciliation/matcher-resolving-exceptions)** > > Strategies for handling unmatched transactions. # Setup progress Source: https://docs.lerian.studio/en/products/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. 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` (in setup), `ACTIVE` (running), `PAUSED` (suspended), or `ARCHIVED` (retired). * **`sources`**: source counts split by matching side: `total`, `left`, `right`. * **`fieldMaps.mappedSources`**: number of mapped sources. This count covers sources with a field map, plus self-mapped CAMT.053 sources. For those, 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`, 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": [ "context.activation.requirement.source-mapping", "context.activation.requirement.match-rule" ] } ``` `missing` holds stable requirement IDs you can map to checklist items: * `context.activation.requirement.left-source`: the context needs at least one LEFT-side source. * `context.activation.requirement.right-source`: the context needs at least one RIGHT-side source. * `context.activation.requirement.source-mapping`: the context needs at least one source, and every source needs a mapping. A source is mapped when it has a field map or is a self-mapped CAMT.053 source. * `context.activation.requirement.match-rule`: the context needs at least one match rule. * `context.activation.requirement.fee-rule`: the context enables fee normalization but has no fee rule. This requirement is conditional. It appears only when you set `feeNormalization` to `NET` or `GROSS`. It mirrors the run precondition, which requires fee rules, not fee schedules, to be non-empty. > [!NOTE] > > If you move a context to `ACTIVE` before it is ready, the update fails with `409 Conflict` and code `MTCH-0103`. Its `detail` is a fixed sentence. Its `errors` array has one entry for each missing requirement, and the `location` of each entry is the requirement ID. Re-read setup progress to render the remaining setup guidance. ## The next action *** `next` turns `readiness.missing` into a concrete call. It comes from `missing[0]`, the first unsatisfied requirement in the stable order above. It is `null` when the context is ready: ```json theme={null} { "requirementId": "context.activation.requirement.source-mapping", "operationId": "createFieldMap", "method": "POST", "path": "/v1/contexts/{contextId}/sources/{sourceId}/field-maps", "requiredFields": ["mapping"], "forSource": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Bank statement" } } ``` * **`requirementId`**: the requirement ID this action satisfies. * **`operationId`**, **`method`**, **`path`**: the endpoint to call to satisfy the requirement. * **`requiredFields`**: the names of the fields that the create request requires. They never carry values. You supply those. * **`forSource`**: present only for the `context.activation.requirement.source-mapping` action, naming the first unmapped source so you can fill `{sourceId}` without a separate lookup. The full requirement-to-endpoint table: | `requirementId` | Endpoint | `requiredFields` | | - | - | - | | `context.activation.requirement.left-source` | `POST /v1/contexts/{contextId}/sources` | `name`, `type`, `side` | | `context.activation.requirement.right-source` | `POST /v1/contexts/{contextId}/sources` | `name`, `type`, `side` | | `context.activation.requirement.source-mapping` | `POST /v1/contexts/{contextId}/sources/{sourceId}/field-maps` | `mapping` | | `context.activation.requirement.match-rule` | `POST /v1/contexts/{contextId}/rules` | `priority`, `type`, `config` | | `context.activation.requirement.fee-rule` | `POST /v1/contexts/{contextId}/fee-rules` | `side`, `feeScheduleId`, `name` | Both source-side requirements resolve to the same `createSource` operation. The `side` field distinguishes them. The fee-rule action needs the `feeScheduleId` of an existing fee schedule. [Create a fee schedule](/en/reference/products/matcher/create-fee-schedule) first. ## 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`.** Do not reimplement the requirement order client-side. Call the operation that `next` names. Then re-read setup-progress for the next 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 produces results. The whole state comes from one call. You can poll this endpoint to keep the wizard live, with no 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/products/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 reaches reconciliation 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. > [!NOTE] > > When Lerian enables document extraction for your environment, you can enqueue documents. When the feature is not available in your environment, the operation answers `503`. That answer comes before any storage or egress of document bytes. ## Enqueue a document for extraction *** Upload a source document (PDF) to run deterministic + AI extraction. Matcher sends a capped slice of the document's text layer to the language-model provider Lerian configures for your environment. When the PDF has no text layer, Matcher sends the PDF itself. The resulting transaction candidates go into a review queue. Nothing reaches reconciliation 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). On the `text_layer` lane, field values are tokens from the document text. On the `vision` lane, the model reads them from the image. 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 closes the review, and nothing enters ingestion. 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. When Lerian enables the mapping advisor for your environment, Matcher sends a capped slice of the sample to the language-model provider Lerian configures for your environment. 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 and streams each into the trusted-content ingestion pipeline. `kind` is `sftp` (the default), `https`, `s3`, or `imap`. The body carries the connection coordinates and a required `credentialRef`. An SFTP fetch needs the server's host key in `connectOptions.known_hosts` and reads at most 256 matching objects. ```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": "", "format": "br/cnab240/febraban-base", "connectOptions": { "known_hosts": "sftp.bank.example ssh-ed25519 AAAA..." } }' ``` The response (`202 Accepted`) returns a per-file outcome in fetch order. A per-file intake failure does not stop the batch. The response reports each one: ```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` holds the uncapped failure total. `truncated` is `true` when the total 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 document body, invalid pagination) | | `403` | Consent for document extraction is not granted for your environment | | `404` | Review or job not found | | `409` | Invalid review state transition | | `422` | No candidates could be extracted, or invalid request (bad status filter, missing mapping sample, structurally invalid body) | | `503` | Extraction, review, proposal, or fetch not available in your environment, document extractor unavailable, or transport failure | # Formats and templates Source: https://docs.lerian.studio/en/products/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 gives a read-only inventory of the formats the ingestion engine can parse. It is global-first and static: built-in parsers carry no tenant, so every authenticated caller sees the same response. The catalog uses a `region → family → variant` tree that matches 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 built-in catalog holds these format keys: | Family | Format keys | | - | - | | Generic (region-neutral) | `xx/delimited/generic` (CSV), `xx/json/generic`, `xx/xml/generic`, `xx/ofx/generic`, `xx/camt/camt053` | | CNAB | `br/cnab240/febraban-base`, `br/cnab240/pagamento`, `br/cnab400/febraban-base` | | Acquirer EDI | `br/acquirer_edi/cielo-v15`, `br/acquirer_edi/rede`, `br/acquirer_edi/getnet-v10-1`, `br/acquirer_edi/stone-xml`, `br/acquirer_edi/adyen`, `br/acquirer_edi/vero-bknfin1`, `br/acquirer_edi/vero-bknfin2` | | Acquirer CSV | `br/acquirer_csv/stone-pix`, `br/acquirer_csv/mercadopago` | | Receivables | `br/receivables/tag` | > [!NOTE] > > The catalog takes no path, query, or body parameters. The tenant has no effect on 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. The parse path resolves it 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 mis-marked money column, or an unknown `kind` reject with `422`. A zero-field record rejects with `400`. Matcher never stores that template. Matcher does not use a template whose key matches a built-in format. > [!NOTE] > > Money third rail: the field named `amount` is the money column, and it must declare `kind: "decimal"`. The submission gate rejects an `amount` field that omits or mis-marks its kind. Matcher reads `amount` as a number with an explicit decimal point and applies no implied decimal places: `000000010050` reads as `10050`. ### 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`. Offsets are zero-based. * `fields[].name`: `external_id`, `amount`, `currency`, `date`, and `description` map to the transaction. Every other name goes into metadata. Without a `currency` field, the currency is `BRL`. * `fields[].kind`: `string`, `decimal` (money/numeric verbatim token, parsed downstream), or `date`. * `requiredFields` (optional): 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 has no pagination, because layout templates form a 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 stays 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`. ## Response codes *** | Status | Meaning | | - | - | | `200` | Catalog, template list, get, or update returned | | `201` | Template created | | `204` | Template soft-deleted | | `400` | Structurally malformed field/layout (including a zero-field record) | | `404` | Template not found | | `409` | Format/variant key already claimed | | `422` | Layout failed the well-formedness gate (overrun, overlap, missing-required, mis-marked money), a field has an unknown `kind`, or a required body field is missing | | `503` | Format catalog or template store not wired on this deployment | # Aggregator connections Source: https://docs.lerian.studio/en/products/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, link a reconciliation source to it, and mint a webhook token bound to the connection. The aggregator's webhooks then drive inbound pulls. This guide covers the full lifecycle. > [!NOTE] > > Credentials (`clientId`/`secret`) are inbound-only: Matcher seals them before persistence and never returns them in a response, a log, or an error. The tenant always comes 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. It is optional. Leave it out to connect the account through [hosted consent](#bind-an-account-through-hosted-consent). * `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", "awaitingConsent": false } ``` ## Link a reconciliation source *** Matcher ingests the pulled data into the reconciliation source that names the connection. On exactly one source, set `config.connection_config_name` to the `configName` of the connection: ```json theme={null} { "name": "Pluggy Main Account", "type": "BANK", "side": "RIGHT", "config": { "connection_config_name": "pluggy-main" } } ``` > [!TIP] > > API Reference: [Create source](/en/reference/products/matcher/create-source) ## 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" ``` Each list row carries `id`, `vendor`, `configName`, `baseUrl`, `accountRef`, `testable`, and `awaitingConsent`. ### 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`. The other fields are optional too: a field you leave out keeps its stored value. ```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" }' ``` ### Bind an account through hosted consent A connection created without `accountRef` waits for consent: it shows `"awaitingConsent": true`. `POST /v1/discovery/aggregator-connections/{id}/connect-token` returns a token for the consent widget of the vendor. Send the item or link id that the widget returns as `accountRef` in the update call above. [Discovery](/en/products/matcher/integrations/matcher-discovery#connect-an-account-through-hosted-consent) describes the full flow. ### 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 `configName`. Matcher reads the vendor from the stored connection. This call takes no credential and returns none. ```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 '{ "configName": "pluggy-main" }' ``` ```json theme={null} { "vendor": "pluggy", "configName": "pluggy-main", "healthy": true } ``` > [!NOTE] > > A credentials-don't-work outcome is an expected test result surfaced as `"healthy": false` with a `200`, not an error. A missing connection returns `404`. Matcher cannot test Belvo connections, so a Belvo connection returns `422` (`MTCH-0210`). A connection that waits for consent returns `422` (`MTCH-0211`). ## Connector types *** List the connector types the engine registry has 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", "requiredFields": ["apiKey"] } ] } ``` Each `rest` entry lists in `requiredFields` the credential fields that a [REST connection](/en/products/matcher/integrations/matcher-discovery#create-a-rest-connection) of that type needs. This list excludes aggregator-vendor types (Pluggy/Belvo). The aggregator-connections surface above provisions them. ## Mint a webhook token *** Mint a webhook token bound to an existing aggregator connection. The response carries the raw token and its provider-facing webhook URL once. Matcher stores only the token's SHA-256 hash. ```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`. Matcher verifies each vendor delivery by an HMAC-SHA256 signature of the raw body in `X-Webhook-Signature`, or by source IP. A delivery that fails verification receives `401`. ## Response codes *** | Status | Meaning | | - | - | | `200` | Get, list, test, or connector-types returned | | `201` | Connection created, webhook token minted, or consent token minted | | `204` | Connection soft-deleted | | `400` | Partial credential pair, malformed credential, or invalid pagination cursor | | `401` | Tenant could not be resolved | | `404` | Connection not found (or not an aggregator) | | `422` | Request fails validation, for example a missing field or a vendor other than `pluggy` or `belvo` | | `422` (`MTCH-0210`) | Matcher cannot test this vendor (Belvo) | | `422` (`MTCH-0211`) | The connection waits for consent, so no test ran | | `422` (`MTCH-0212`) | The vendor did not issue a consent token | | `422` (`MTCH-0213`) | Matcher has no consent flow for this vendor | | `409` | Connection with that config name already exists | # Discovery Source: https://docs.lerian.studio/en/products/matcher/integrations/matcher-discovery Use Discovery to detect external data sources, inspect their schemas, and pull transactions into Matcher automatically. Discovery automates data source detection and extraction through Matcher's embedded extraction engine. Instead of manually uploading files, Discovery connects to external systems, identifies available data, and extracts transactions directly into Matcher. ## What Discovery solves *** With manual file uploads, 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 the extraction engine, reads their schemas, and pulls transactions into Matcher on demand. ## How Discovery works *** Discovery runs inside Matcher. The embedded engine manages connections to external databases and runs extractions locally. Discovery exposes those connections, coordinates the extraction process, and hands results directly to Ingestion. The workflow has eight steps: 1. **Create a connection**: Register a database with its address and user, or a REST connection to a provider API. 2. **Check status**: See how many connections the tenant has and when they last synced. 3. **Browse connections**: See the tenant's database connections. 4. **Inspect a connection**: Review the schema to understand what fields are available. 5. **Test a connection**: Validate the connection before committing to an extraction. 6. **Create an extraction**: Request that Matcher pull data from a specific source. 7. **Monitor progress**: Track extraction status as data flows in. 8. **Refresh connections**: Re-sync connections from the embedded engine. ## Discovery workflow *** ### Create a connection Register a database connection for the tenant. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/connections" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "configName": "prod-ledger-db", "databaseType": "POSTGRESQL", "host": "db.internal.example.com", "port": 5432, "databaseName": "ledger", "userName": "matcher_ro", "password": "..." }' ``` `configName`, `databaseType`, `host`, `port`, `databaseName`, and `userName` are required. `schema` and `password` are optional, and Matcher never returns `password`. A successful create returns `201`. Update a connection with `PATCH` and delete it with `DELETE` on `/v1/discovery/connections/{connectionId}`. ### Create a REST connection Create a REST connection to extract data from a provider API instead of a database. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/rest-connections" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "configName": "stripe-main", "connectorType": "STRIPE", "apiKey": "..." }' ``` `configName` and `connectorType` are required. `connectorType` is `PAGBANK`, `STRIPE`, or `PIX_BCB`. `baseURL` is optional and sets the provider API address for this connection. Each connector type also requires its own credential fields: | `connectorType` | Required credential fields | | - | - | | `PAGBANK` | `establishmentID`, `ediToken` | | `STRIPE` | `apiKey` | | `PIX_BCB` | `clientID`, `clientSecret`, `clientCert`, `clientKey` | Matcher stores the credentials encrypted. [List connector types](#list-connector-types) returns the same field names as `requiredFields`. A successful create returns `201`, and the connection appears in the connection list like any other connection. On a REST connection, `PATCH` accepts only `configName`. To change anything else, delete the connection and create it again. ### Check Discovery status Read the tenant's connection count and the time of the last sync. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/status" \ -H "Authorization: Bearer $TOKEN" ``` > [!TIP] > > API Reference: [Get Discovery status](/en/reference/products/matcher/discovery-status) ### Browse connections List all data sources available through the embedded extraction engine. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connections" \ -H "Authorization: Bearer $TOKEN" ``` The response lists each database connection with its `configName`, `databaseType`, host, database, `status`, and whether its schema was discovered. > [!TIP] > > API Reference: [List connections](/en/reference/products/matcher/list-discovery-connections) ### Get a connection Retrieve a single discovered 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 same fields for one connection. Use it when you already hold a `connectionId`, for example from a source binding's query rail. It gives the current details without a list of every connection. > [!TIP] > > API Reference: [Get discovery connection](/en/reference/products/matcher/retrieve-discovery-connection) ### Inspect a connection Before you extract, review the schema of a specific connection to understand what data fields are available. ```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. > [!TIP] > > API Reference: [Get connection schema](/en/reference/products/matcher/get-connection-schema) ### Get schema history Read the earlier generations of a connection's schema, oldest first. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/discovery/connections/{connectionId}/schema/history" \ -H "Authorization: Bearer $TOKEN" ``` The response carries `connectionId` and a `generations` list. Each generation carries `archivedAt`, the time it stopped being the live schema, and its `tables`. When all tables of a generation share one cause, the generation also carries `retirementCause`: | `retirementCause` | Meaning | | - | - | | `ABSENT_FROM_CATALOG` | The catalog of the source stopped listing the table. | | `COLUMN_SET_SUPERSEDED` | The catalog still listed the table, but with a different set of columns. | | `CONNECTION_RETIRED` | Someone retired the connection. | | `UNKNOWN_LEGACY` | An earlier release archived the generation and recorded no cause. | An unknown connection returns `404`. ### 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" ``` The test connects to the database and pings it. It does not check read access to tables. A failed test returns `200` with `"healthy": false` and an `errorMessage`. Always test before creating an extraction, especially for new or recently modified connections. > [!TIP] > > API Reference: [Test connection](/en/reference/products/matcher/test-discovery-connection) ### Create an extraction Request that Matcher pull data from a connection. The request names no context. Matcher ingests the result into the `FETCHER` source linked to the connection. ```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": { "public.transactions": {} } }' ``` Each key in `tables` must be a table name exactly as the connection's schema lists it. The response returns an extraction ID. Use it to monitor progress. > [!TIP] > > API Reference: [Create extraction](/en/reference/products/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`). An extraction fails when its result exceeds 2 GiB. The response carries the extraction `status`, an `errorMessage` when it failed, and the linked `ingestionJobId` once the extraction bridges into ingestion. > [!TIP] > > API Reference: [Get extraction](/en/reference/products/matcher/retrieve-extraction) ### Refresh available connections Refresh re-syncs the tenant's connections from the embedded engine. A connection that the engine no longer reports becomes `UNREACHABLE`. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/refresh" \ -H "Authorization: Bearer $TOKEN" ``` > [!TIP] > > API Reference: [Refresh connections](/en/reference/products/matcher/refresh-discovery) ### List connector types List the connector (datasource) types the engine registry has registered for your environment. Each entry carries a backend-derived `category` (`database` or `rest`). The registry is live. Only connectors registered at boot appear. This list excludes aggregator vendors (Pluggy/Belvo). 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": "..." }' ``` Send five required fields: `vendor`, `configName`, `baseUrl`, `clientId`, and `secret`. The `accountRef` field (the Pluggy item id or Belvo link id) is optional. Without it, Matcher creates a connection that waits for consent. See [Connect an account through hosted consent](#connect-an-account-through-hosted-consent). The `vendor` field is one of `pluggy` or `belvo`. The `configName` field is the tenant-scoped name the webhook token-mint endpoint binds to. A successful create 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", "awaitingConsent": false } ``` ### 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" ``` ### Connect an account through hosted consent Use this flow when you do not have the vendor's item or link id yet. The end customer authorizes access in the vendor's own consent widget. 1. Create the connection without `accountRef`. The response carries `"awaitingConsent": true`. 2. Get the connection `id` from the list response. 3. Request a consent token for the connection. The request has no body. 4. Open the vendor's consent widget with the `token` from the response. 5. Send the item or link id that the widget returns as `accountRef` in `PUT /v1/discovery/aggregator-connections/{id}`. You do not need to send other fields. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections/{id}/connect-token" \ -H "Authorization: Bearer $TOKEN" ``` The `201` response carries `vendor`, `configName`, `token`, `expiresAt`, `reconsent`, and `accountRef`. For a connection that already has an `accountRef`, the token renews consent for that same item or link. Then `reconsent` is `true`, and `accountRef` holds the bound id. Give that `accountRef` to the update mode of the widget. The answer shows the consent token once, and Matcher does not store it. Use it before `expiresAt`. After that time, the vendor refuses it. A connection id that is not an aggregator connection returns `404`. If the vendor does not issue a token, the call returns `422` (`MTCH-0212`). If Matcher has no consent flow for the vendor, the call returns `422` (`MTCH-0213`). ### Test an aggregator connection Run a live connectivity check against an existing connection's already-sealed credential. Send only the `configName`: Matcher reads the vendor from the stored connection. This call takes no credential and returns none. Invalid stored credentials give an expected test result: `200` with `"healthy": false`, not an error. A missing connection returns `404`. Matcher cannot test Belvo connections, so a Belvo connection returns `422` (`MTCH-0210`). A connection that waits for consent returns `422` (`MTCH-0211`). In these cases, no test runs. Each row of the list response carries `testable`, which tells you if a test can run. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/discovery/aggregator-connections/test" \ -H "Authorization: Bearer ***" \ -H "Content-Type: application/json" \ -d '{ "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 Matcher returns the raw token and its provider-facing URL exactly once. Matcher stores only the token's SHA-256 hash. ```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). Two layers authenticate it: the opaque path token plus a per-provider source check. When the provider has an HMAC secret, that check is a valid hex HMAC-SHA256 of the raw body in the `X-Webhook-Signature` header. Otherwise it is membership in the provider's source-IP allowlist. Both layers fail closed. A valid first delivery returns 202 Accepted. Matcher then pulls the signaled data asynchronously into the ingestion pipeline. A redelivery of an event that Matcher already received returns 200 OK. ## Best practices *** > **Always test connections before extracting** > > 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. > **Inspect schemas before mapping fields** > > 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. > **Monitor extractions actively for large datasets** > > Large extractions take time. Don't assume completion. Poll the extraction status, then check the linked ingestion job before starting a match run. Starting a run on incomplete data generates incorrect exceptions. ## Next steps *** > **[External sources](/en/products/matcher/integrations/matcher-external-sources)** > > Configure the external data sources that Discovery connects to. > **[Field mapping](/en/products/matcher/configuration/matcher-field-mapping)** > > Map fields from extracted data to Matcher's transaction model. > **[Discovery API reference](/en/reference/products/matcher/discovery-status)** > > Full API reference for Discovery endpoints. # External sources Source: https://docs.lerian.studio/en/products/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 | Data that [Discovery](/en/products/matcher/integrations/matcher-discovery) extracts from a connection | ## 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/products/matcher/integrations/matcher-discovery) extracts data from discovered connections into ingestion | | **Aggregator webhooks** | [Open-Finance aggregators](/en/products/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. > [!TIP] > > API Reference: [Upload transaction file](/en/reference/products/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", "side": "RIGHT", "config": { "bank_name": "Chase", "account_number": "****1234", "currency": "USD", "statement_format": "CSV", "timezone": "America/New_York" } } ``` > [!NOTE] > > The `config` object is free-form descriptive metadata. Matcher stores it but does not interpret keys like `bank_name` or `statement_format`. Instead, Matcher acts on these reserved keys when you set them: `dialect`, `fail_on_error_rate_percent`, `duplicate_policy`, `duplicate_key`, `blank_external_id`, `camt053`, `connection_id`, and `connection_config_name`. > [!TIP] > > API Reference: [Create source](/en/reference/products/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", "side": "LEFT", "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/products/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", "side": "RIGHT", "config": { "provider": "stripe" } } ``` ### Adyen ```json theme={null} { "name": "Adyen Settlements", "type": "GATEWAY", "side": "RIGHT", "config": { "provider": "adyen", "merchant_account": "CompanyECOM" } } ``` Export settlement reports from your payment processor and upload them through Matcher's file upload endpoint. For Stripe, PagBank, and Pix BCB, Matcher can also extract the data through a [REST connection](/en/products/matcher/integrations/matcher-discovery#create-a-rest-connection). ### Card networks For card network settlement files (Visa, Mastercard, Elo), use the `CUSTOM` source type: ```json theme={null} { "name": "Visa Settlement", "type": "CUSTOM", "side": "RIGHT", "config": { "network": "VISA" } } ``` ## Connection security *** ### Webhook signatures Matcher verifies inbound aggregator webhooks with an HMAC-SHA256 signature or a source IP allowlist, as [Aggregator connections](/en/products/matcher/integrations/matcher-aggregator-connections) describes. Source entities do not have a `settings.webhook` configuration. ## Data format requirements *** ### Required fields 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 | | `direction` | Object | Debit or credit indicator: `{column, debit, credit}` | Matcher accepts no other keys. Use `direction` when a file marks each row as debit or credit in a separate column. `column` names that column, and `debit` and `credit` are the values it holds. Matcher imports a debit row with a negative amount and a credit row with a positive one. When you map `direction`, the amount cells must carry no sign. A signed amount, an empty direction cell, or an unknown direction value rejects the row. Matcher applies `direction` to CSV, JSON, and XML input only. ### 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/products/matcher/configuration/matcher-field-mapping) for details. ## Best practices *** > **Validate files before uploading** > > Check that uploaded files contain columns for the required canonical fields (external\_id, amount, currency, date) before uploading. This prevents ingestion errors. > **Use consistent file formats** > > Standardize on a single format (CSV, JSON, or XML) per source to simplify field mapping and reduce errors. > **Secure credentials properly** > > Never include credentials in configuration payloads. > **Test with sample data first** > > Validate field mapping and data quality with sample files before uploading production data. ## Next steps *** > **[Field Mapping](/en/products/matcher/configuration/matcher-field-mapping)** > > Configure how source fields map to Matcher. > **[Uploading Files](/en/products/matcher/daily-reconciliation/matcher-uploading-files)** > > Manual file upload procedures. # Matcher and Midaz Source: https://docs.lerian.studio/en/products/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. > [!NOTE] > > 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/products/midaz/what-is-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 complement each other and stay decoupled. You use Midaz and Matcher together by reconciling ledger data against an external dataset (a bank statement, a gateway settlement report). The data moves between them by export and import, with no 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 Matcher's category for "internal ledger / accounting system" data. It is not a Midaz driver. ```json theme={null} { "name": "Midaz Cash Ledger", "type": "LEDGER", "side": "LEFT", "config": {} } ``` > [!TIP] > > API Reference: [Create source](/en/reference/products/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. Then define a [field map](/en/products/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`). A Midaz operation carries its type, `DEBIT` or `CREDIT`. If your export keeps the type in its own column and the amount unsigned, map that column with the optional `direction` key. Matcher then imports a debit row with a negative amount and a credit row with a positive one. > [!TIP] > > API Reference: [Upload transaction file](/en/reference/products/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. No automatic mapping happens. See [Field mapping](/en/products/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 Access Manager and tokens used across the platform apply. * **Tenant isolation.** Matcher keeps each tenant's data separate. These commonalities are platform-level and form no Matcher-to-Midaz data channel. ## What this integration is *not* *** * 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 the smallest relevant slice** > > Export only the ledger accounts and period you need to reconcile. Smaller, targeted exports keep contexts fast and matching precise. > **Keep export cadence aligned with reconciliation** > > If you reconcile daily, export daily. Match the export schedule to the counterparty data (bank statement) availability. > **Normalize before upload where you can** > > Produce exports whose amounts, dates, and currency codes are already in the shapes your match rules expect. > **Carry references through metadata columns** > > 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 *** > **[External sources](/en/products/matcher/integrations/matcher-external-sources)** > > Connect the bank or gateway side of the reconciliation. > **[Field mapping](/en/products/matcher/configuration/matcher-field-mapping)** > > Map exported ledger columns to Matcher's canonical fields. # Webhooks and callbacks Source: https://docs.lerian.studio/en/products/matcher/integrations/matcher-webhooks-callbacks Consume Matcher lifecycle events from the streaming backbone, and receive resolution callbacks so external systems keep exceptions in sync. This guide covers the events Matcher publishes and the inbound callbacks that external systems send to Matcher. ## Overview *** * **Outbound events**: Matcher publishes its lifecycle event catalog on the platform's streaming backbone. Those events reach you as a stream, not as HTTP webhooks. * **Inbound callbacks**: External systems, such as a ticketing tool, notify Matcher when they act on an exception. Matcher then updates the exception status. ## Outbound events *** Matcher emits events when significant actions occur in the reconciliation process, and publishes them on the streaming backbone. Each event follows its own schema on the event stream. ### Available events The tables below list the event catalog by domain. **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_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** | 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 the extraction engine | Extraction monitoring | | `extraction_request.completed` | Extraction completed and handed to ingestion | Data readiness | | `extraction_request.failed` | Extraction 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 dispatched to a target system | Audit trail | | `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 | ## 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 `X-Callback-Token` header authenticates the callback endpoint. An operator JWT does not authenticate it. The header carries an opaque token from the [callback credentials](#callback-credentials) surface. Every field below is mandatory. `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: 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" } ``` > [!TIP] > > API Reference: [Process callback](/en/reference/products/matcher/process-exception-callback) When Matcher processes a callback, it updates the exception status and records the resolution in the audit trail. The `X-Idempotency` header is required. Matcher reads the idempotency key only from this header. A request that carries a key only in `X-Idempotency-Key` or `Idempotency-Key` is refused with `400` (`MTCH-0015`), and nothing is processed. Matcher deduplicates a callback by its external system, its exception, and its key together. The same key on another exception or from another external system is a new callback. ### Callback status * `status` moves the exception to `ASSIGNED` or `RESOLVED`. Matcher reads the value without case sensitivity. * To move the exception to `ASSIGNED`, send a non-blank `assignee`. * When `status` is the current status of the exception, Matcher keeps the status and updates `assignee` and `dueAt` if you send them. ### Callback refusals | Status | Code | Meaning | | - | - | - | | `400` | `MTCH-0015` | The key arrived only in `X-Idempotency-Key` or `Idempotency-Key`. | | `400` | `MTCH-0001` | The callback is not valid. For example, the `X-Idempotency` value is not 1 to 128 letters, digits, `:`, `_`, or `-`, or an `ASSIGNED` callback has no `assignee`. | | `409` | `MTCH-0504` | Another callback with the same key is still in process. Send the request again later. | | `409` | `MTCH-0505` | Matcher cannot confirm the state of an earlier callback with the same key. Send the request again later. | | `429` | `MTCH-0506` | The external system sent more callbacks than its rate limit allows. Send the request again later. | An unknown or revoked callback token answers `404`. When Matcher cannot check the token, it answers `503`. ### 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. You don't need to generate a new idempotency key when retrying a failed callback. Resend the same request and Matcher handles the recovery. The retry behavior applies only to callbacks with an internal `failed` state. Matcher still deduplicates callbacks that completed successfully. ## Callback credentials *** An opaque bearer token authenticates inbound callbacks. The external system sends this token in the `X-Callback-Token` header. You mint, list, rotate, and revoke these **callback credentials** through a dedicated CRUD surface under `/v1/exceptions/callbacks/credentials`. Each credential belongs to the caller's tenant. Matcher stores only the token's SHA-256 hash server-side. The mint and rotate responses return the raw token exactly once. | 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. | > [!TIP] > > API Reference: > > * [Mint callback credential](/en/reference/products/matcher/mint-callback-credential) > * [List callback credentials](/en/reference/products/matcher/list-callback-credentials) > * [Rotate callback credential](/en/reference/products/matcher/rotate-callback-credential) > * [Revoke callback credential](/en/reference/products/matcher/revoke-callback-credential) ### Mint a credential The request needs a JSON body. Send `{}` if you do not set a label. The `externalSystem` field is optional. Provide it 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. | > [!WARNING] > > Only the mint and rotate responses show the raw `token`. Store it securely on receipt. You cannot retrieve it again. If you lose or leak the token, 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` (`201`, new raw token) and revokes the previous credential in the same transaction. Callbacks that still send the old token fail, so update the external system with the new token at once. ### 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. ## Next steps *** > **[Exception Routing](/en/products/matcher/configuration/matcher-exception-routing)** > > Configure rules that route exceptions. > **[External Sources](/en/products/matcher/integrations/matcher-external-sources)** > > Set up data sources. Among them, only aggregator connections receive webhooks. # Architecture Source: https://docs.lerian.studio/en/products/matcher/matcher-architecture Explore Matcher's modular monolith built on DDD, hexagonal architecture, and CQRS, with bounded contexts that evolve independently. Matcher is a **modular monolith** with Domain-Driven Design (DDD) and hexagonal architecture. CQRS separates commands (writes) from queries (reads). This keeps operations simple and boundaries clear. Each module can evolve independently without the complexity of microservices. ## Architecture overview *** ![Matcher Architecture](https://mintcdn.com/lerian-49cb71fc/pdfpHj06Qv6abx_7/images/en/d2/matcher-architecture.svg?fit=max\&auto=format\&n=pdfpHj06Qv6abx_7\&q=85\&s=a3d5acf47c4ec626225354ef42294707) Matcher architecture overview ## Bounded contexts *** Matcher is split into 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 Matcher's embedded extraction engine * **Ingestion**: Getting data in (parsing, validation, normalization) * **Matching**: The engine (rule execution, confidence scoring) * **Exception**: Handling unmatched items (workflow, assignment, resolution) * **Governance**: Audit trails (append-only logs for compliance) * **Reporting**: Visibility (reports, exports, dashboards) ### Configuration Defines what you're reconciling and how. **Handles:** * Contexts (what you reconcile) * 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 the extraction engine. Discovery runs inside Matcher. **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 * Manage REST API connections and keep each connection's schema history **Key entities:** * `ExtractionRequest`: Tracks an extraction lifecycle run by the embedded engine > [!NOTE] > > See [Discovery](/en/products/matcher/integrations/matcher-discovery) for how Discovery connects to external databases with the extraction engine. ### Ingestion The Ingestion bounded context handles data intake and normalization. **Responsibilities:** * Parse uploaded files * 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, fuzzy, 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 * Assign exceptions to your team * Support manual overrides, adjustments, and disputes * Track resolution status * Receive resolution callbacks from external systems **Key entities:** * `Exception`: An unresolved transaction ### Governance The Governance bounded context preserves reconciliation traceability. **Responsibilities:** * Record configuration changes and exception resolutions in append-only audit logs * Provide queryable audit history * Support regulatory and compliance reporting **Key entities:** * `AuditLog`: Append-only record of instrumented auditable mutation workflows > [!WARNING] > > Audit logs are append-only. Archival exports old monthly partitions to object storage and then drops them from the database. ### 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: **1. Configuration** You define reconciliation contexts, sources, field mappings, and rules through the API. **2. Discovery** Discovery connects to external sources, detects their schemas, and runs extractions in-process with the extraction engine. Discovery hands extracted results directly to Ingestion. **3. Ingestion** Ingestion parses, validates, normalizes, and deduplicates uploaded files and the data that Discovery extracts. Ingestion emits an `ingestion.completed` event. **4. Matching** Matching applies rules to eligible transactions and produces match groups with confidence scores on an integer scale of 0 to 100. Groups from `EXACT` and `TOLERANCE` rules with a confidence of at least 90 out of 100 auto-confirm. Groups from `FUZZY` and `DATE_LAG` rules always require manual review. Unmatched items become exceptions. **5. Exception handling** The Exception context classifies exceptions, and your team assigns and resolves them. External systems can report resolutions through callbacks. Resolution updates return to Matcher. **6. Governance** Governance records configuration changes and exception resolutions in append-only audit logs. **7. Reporting** 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 | | **Valkey (Redis-compatible)** | Cache and coordination | Deduplication, locks, idempotency keys | | **Streaming backbone** | Business-event publication | Domain events | | **Object storage (S3-compatible)** | File storage | Export files, dispute evidence, documents queued for extraction, audit-log archives | ### Database architecture * **Eventual consistency** for reporting views ### Tenant resolution Matcher resolves the tenant as follows: * Matcher never accepts tenant identifiers from request parameters > [!NOTE] > > For details, see [Security](/en/products/matcher/reference/matcher-security). ## Design patterns *** ### Outbox pattern Matcher uses per-event delivery policies. Matcher persists an outbox record for outbox-backed events and dispatches them asynchronously. Other events can use direct delivery with an outbox fallback when the circuit is open. ## Next steps *** > **[Quick start](/en/products/matcher/getting-started/matcher-quick-start)** > > Explore the architecture through a guided example. > **[Security](/en/products/matcher/reference/matcher-security)** > > Review authentication, authorization, and tenant isolation mechanisms. # Matcher concepts Source: https://docs.lerian.studio/en/products/matcher/matcher-concepts Learn Matcher's five core concepts that shape every reconciliation you build: contexts, sources, field maps, match rules, and matches. The five core concepts in Matcher: **contexts**, **sources**, **field maps**, **rules**, and **matches**. ## Context *** A **context** defines what you reconcile. It is the configuration container for sources and rules. Its inline sources and rules are optional. > [!NOTE] > > A context answers: *what do I match against what?* > [!NOTE] > > 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 you enable fee normalization, 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 | The type also fixes the sources per side. A `1:1` context needs exactly one `LEFT` and one `RIGHT` source, a `1:N` context needs exactly one `LEFT` source, and an `N:M` context needs at least one source on each side. A matching run fails when the sources do not fit the type. ### 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 Discovery 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 | Transaction description | | `fee_amount` | No | Decimal | Optional fee amount column | | `fee_currency` | No | String | Optional fee currency column | | `direction` | No | Object | `{column, debit, credit}`: the column that marks each row as debit or credit. A debit row imports with a negative amount, a credit row with a positive one. CSV, JSON, and XML only | The canonical vocabulary stays closed. Matcher rejects a field map that declares any other key. When you map `direction`, use an unsigned amount column. Matcher rejects a row with a signed amount, an empty direction cell, or an unknown direction value. When you upload a file with format `camt053`, Matcher uses its built-in ISO 20022 mapping and ignores the field map. Activation treats a source that declares `camt053` options as mapped. ### Example mapping You map a bank statement that exposes `TXN_ID`, `VALUE`, `CCY`, and `POST_DATE` like this: ```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. Currency and reference must also match by default. Omitted amount tolerances and `dateWindowDays` default to `0`, so the rule allows no drift or date window until you configure one. * **DATE\_LAG**: Matches within a configured day-difference band. `minDays` and `maxDays` both default to `0` (same day). DATE\_LAG only proposes. It never auto-confirms. * **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% | > [!NOTE] > > 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). `percentTolerance` is a fraction: `0.005` is 0.5%. The allowed difference is the larger of `absTolerance` and `percentTolerance` times the larger absolute amount, with the default `percentageBase`. 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 both bounds. Amounts must be equal unless you set `feeTolerance`. | | FUZZY | `minSimilarity`: normalized-reference threshold from `0` to `1` (defaults to `0.80`), plus amount, currency, and date checks, which are on by default. | ## Match *** A **match** is when transactions from different sources are reconciled together. ### 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 `UNMATCHED` | ### 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. It skips a candidate bucket that holds more than 40 transactions on a side. ``` 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. The amount is the absolute value of the transaction's base amount, or of its amount when it has no base amount. For an aged open item, it is the absolute value of the open balance. | Severity | Default criteria | | - | - | | **Critical** | Amount ≥ 100,000 or age ≥ 120 hours | | **High** | Amount ≥ 10,000 or age ≥ 72 hours | | **Medium** | 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. Matcher caps an exception with reason `FEE_DATA_MISSING` at **Medium**, whatever its amount or age. ### 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 and notes 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% | Full weight when the amounts match under the rule or the rule does not check amounts, else `0` | | Currency match | 30% | Full weight when the currencies match, else `0` | | Date tolerance | 20% | Full weight when the dates fall inside the rule's window or the rule does not check dates, else `0` | | Reference | 10% | Normalized reference alignment (graded 0–1 for FUZZY rules) | ### Confidence tiers | Tier | Score range | System behavior | | - | - | - | | **Auto-approved** | ≥ 90 | Confirmed automatically for EXACT and TOLERANCE matches. FUZZY and DATE\_LAG matches always go to review | | **Needs review** | 60–89 | Flagged for manual review | > [!NOTE] > > Confidence weights and tier thresholds are fixed by the engine. They 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. 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 | > [!WARNING] > > Audit logs are append-only. Archival exports old monthly partitions to object storage and then drops them from the database. ## Next steps *** > **[Architecture](/en/products/matcher/matcher-architecture)** > > See how the bounded contexts implement these concepts. > **[Quick start](/en/products/matcher/getting-started/matcher-quick-start)** > > Apply these concepts in a guided, hands-on flow. # Rule suggestions Source: https://docs.lerian.studio/en/products/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. Matcher creates a rule only when a human approves a suggestion. This guide covers the human-in-the-loop (HITL) rule-suggestion queue. > [!NOTE] > > When Lerian enables rule suggestions for your environment, Matcher sends each suggestion request to the language-model provider Lerian configures for your environment. When the feature is not available in your environment, the operation answers `503`. The payload is aggregates only. No raw transaction, money, or PII leaves Matcher. ## 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": { "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 come 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 transaction, but its config can set monetary thresholds such as `absTolerance`. ## 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 and creates no rule. The request body is mandatory, 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" }' ``` Matcher records the approving/rejecting principal 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`. > [!TIP] > > Preview how a candidate rule would behave before you approve it with the read-only simulate endpoint. See [Simulation](/en/products/matcher/matching/matcher-simulate). ## Response codes *** | Status | Meaning | | - | - | | `200` | Suggestions produced, listed, approved, or rejected | | `403` | Missing permission, inactive context, or (producing suggestions only) consent for rule suggestions not granted for your environment | | `404` | Rule suggestion not found | | `409` | Invalid state transition / already linked | | `422` | Invalid status filter or id, or approved suggestion failed validation | | `503` | Rule suggestion or advisor not available (author rules manually) | # Simulation Source: https://docs.lerian.studio/en/products/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. They are **match simulation** (will this rule match?) and **fee simulation** (what fees would this schedule charge?). Neither persists anything. ## Match simulation *** Preview how a single rule would match a context's unmatched transactions, without committing anything. Choose an existing configured rule (`ruleId`) or an inline candidate rule (`rule`). ```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": { "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 and the per-side unmatched counts. It also returns a bounded sample of would-match pairs, each with a confidence score. In `why`, the booleans report whether the two amounts, currencies, and dates are equal, not the score components: ```json theme={null} { "ruleType": "TOLERANCE", "matchedGroups": 12, "unmatchedLeft": 3, "unmatchedRight": 5, "sampleTruncated": false, "sample": [ { "left": { "id": "...", "amount": "100", "currency": "BRL", "date": "2025-06-01T00:00:00Z" }, "right": { "id": "...", "amount": "100", "currency": "BRL", "date": "2025-06-01T00:00:00Z" }, "score": 100, "why": { "amountMatch": true, "currencyMatch": true, "dateMatch": true, "referenceScore": 1 }, "amountDelta": "0", "dateDeltaDays": 0 } ] } ``` > [!NOTE] > > Scope: the simulation reads at most 5,000 of the context's unmatched transactions, and the context's sources must resolve into two sides. It scores by the deterministic rule engine over the raw transaction amounts. It does not apply run-time fee normalization, FX conversion, or the FX-variance band, and it previews only 1:1 pairwise grouping (no 1:N/N:M allocation). The simulation does not count a pair that only matches after fee normalization, after FX conversion, inside the FX band, or via allocation. ## Fee simulation *** Calculate fees for a given gross amount using a specific fee schedule. `currency` must equal the schedule's currency. 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", "netAmount": "97.7", "totalFee": "2.3", "currency": "USD", "items": [ { "name": "interchange", "fee": "1.5", "baseUsed": "100" }, { "name": "processing", "fee": "0.8", "baseUsed": "100" } ] } ``` > [!NOTE] > > 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 endpoints remain strictly read-only. They never persist a run, group, item, rule, or transaction. ## Response codes *** | Status | Meaning | | - | - | | `200` | Simulation returned | | `400` | Invalid input (ambiguous/missing rule, sides that do not resolve, invalid gross amount) | | `404` | Context, rule, or fee schedule not found | | `422` | Malformed field, such as an invalid id or an unknown rule type | | `503` | Match simulation not available | # Confidence scoring Source: https://docs.lerian.studio/en/products/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 Matcher handles the match: * High scores (90+) from EXACT and TOLERANCE rules are auto-approved * All other proposed matches require review ![Matcher Confidence Scoring](https://mintcdn.com/lerian-49cb71fc/DDbrUgbtobpkB1Zl/images/en/d2/matcher-confidence-scoring.svg?fit=max\&auto=format\&n=DDbrUgbtobpkB1Zl\&q=85\&s=672c817e7dae37cfd577e4d13063c474) Matcher confidence scoring. ## Score components *** Matcher uses a weighted scoring system with four components. The amount, currency, and date components each score a full match (1.0) or no match (0.0). The reference component does the same for EXACT, TOLERANCE, and DATE\_LAG rules. For FUZZY rules, it scores the graded reference similarity, from 0.0 to 1.0. | 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 | On a proposed match, the amount and date components always score their full points. A rule that checks them rejects a pair that fails them. A rule that does not check them scores them as matched. Only the currency and reference components can lower the score. ### 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, and it respects `datePrecision`. A DATE\_LAG rule accepts a day difference inside its `[minDays, maxDays]` band. The `inclusive` flag controls whether `minDays` and `maxDays` themselves count. ### Reference match (10%) For EXACT and TOLERANCE rules, reference comparison is binary, and `matchReference` is on by default. With `matchReference` off, an EXACT rule scores the full 10 points and a TOLERANCE rule scores `0`. | 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 | > [!NOTE] > > **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`. This value is a similarity measure, so it is not limited to a strict `0` or `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. > [!WARNING] > > **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 DATE\_LAG score is therefore 90 (40 + 30 + 20 + 0). ## Calculation formula *** The confidence score formula: ``` confidence = round( (amountMatch × 0.40 + currencyMatch × 0.30 + dateMatch × 0.20 + referenceScore × 0.10) × 100 ) ``` `amountMatch`, `currencyMatch`, and `dateMatch` are each `1.0` (match) or `0.0` (no match). `referenceScore` is between `0.0` and `1.0`. 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 a proposed match scores one of these values: **60, 70, 90, 100** A DATE\_LAG match scores 60 or 90, because its reference component is always `0`. An FX rate-variance match has a fixed score of 75. See [Multi-currency matching](/en/products/matcher/reference/matcher-multi-currency#fx-rate-variance-band). 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**. Regardless of the resulting score, a FUZZY match is **never auto-confirmed**. See [Rule types that never auto-confirm](#rule-types-that-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) Amount, currency, and date match. The rule is TOLERANCE with `matchReference` off, so the reference scores `0`: | 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, date, and reference match. The currencies differ, and the rule does not check currency: | Component | Comparison | Points | | - | - | - | | Amount | 1,000.00 vs 1,000.00 ✓ | 40 | | Currency | USD vs EUR ✗ | 0 | | Date | 2024-01-15 vs 2024-01-15 ✓ | 20 | | Reference | PAY-001 vs PAY-001 ✓ | 10 | **Final Score: 70** → Needs review ## Confidence tiers *** Matcher categorizes matches into tiers based on score: | Tier (score) | System behavior | | - | - | | **Auto-Approved** (≥ 90) | Automatically confirmed when the rule is EXACT or TOLERANCE. FUZZY and DATE\_LAG matches are queued for manual review | | **Needs Review** (60-89) | Queued for manual review | Every proposed match scores at least 60, because the amount and date components always score their full points on a proposal. ### 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** and the rule is EXACT or TOLERANCE, Matcher confirms the match automatically. 2. Otherwise, Matcher queues the match for manual review. ### Rule types that never auto-confirm Matches produced by **FUZZY** and **DATE\_LAG** rules are **never auto-confirmed**, regardless of their confidence score. Even a FUZZY or DATE\_LAG match scoring 90 or above is always queued for manual review. A fuzzy reference only contributes the 10% reference slot, so the financial fields (amount + currency + date) alone can already reach the 90 threshold. A DATE\_LAG rule does not score the reference, so its score is at most 90. ## Confidence thresholds *** Matcher uses fixed thresholds to determine how it handles matches: | Threshold | Score | Behavior | | - | - | - | | Auto-confirm | >= 90 | Match is automatically confirmed when the rule is EXACT or TOLERANCE | | Match | >= 60 | Match is proposed for manual review | These thresholds are not configurable per context. ## Weights *** The component weights (40/30/20/10) are hardcoded constants. You cannot adjust them 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 *** > **Monitor tier distribution** > > Track the percentage of transactions in each tier. Unusual shifts may indicate data quality issues or rule misconfiguration. > **Use dry-run mode to test rules** > > Since confidence depends on which rules match, always dry-run rule changes before committing. This prevents unexpected increases in manual review volume. > **Understand scores as rule outcomes** > > For EXACT, TOLERANCE, and DATE\_LAG rules, scoring is binary. A score of 70 means that amount, date, and reference matched and currency did not. A score of 90 means that the reference scored `0`. Use this to diagnose matching issues. > > FUZZY rules are the exception. Their graded reference component can yield intermediate scores (e.g. 97). Read a FUZZY score as "financial fields matched plus a partial reference similarity." ## Next steps *** > **[Match Rules](/en/products/matcher/configuration/matcher-match-rules)** > > Configure rules that influence scoring. > **[Multi-Currency](/en/products/matcher/reference/matcher-multi-currency)** > > How FX affects confidence scoring. # Exports & disputes Source: https://docs.lerian.studio/en/products/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. With **export jobs** you queue a report, poll until it succeeds, and download the file. With **disputes** you open a dispute against an exception, attach evidence, then close it won or lost. ## 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-446655440000" } ``` * `reportType`: one of `MATCHED`, `UNMATCHED`, `VARIANCE`, `EXCEPTIONS` (the aliases `MATCHES` and `UNMATCHED_TRANSACTIONS` normalize to these values). * `format`: `CSV`, `JSON`, or `XML`, in uppercase. * `dateFrom` / `dateTo`: optional `YYYY-MM-DD`. `dateFrom` defaults to 30 days before `dateTo`, and `dateTo` defaults to tomorrow (UTC). * `sourceId`: optional source filter. > [!NOTE] > > Async export jobs do not support `SUMMARY` (`400`) or `PDF` (`422`). The date window spans at most 365 days, and a longer window returns `400`. Job creation returns `503` when asynchronous exports are not enabled for your environment. ### 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", "contextId": "969a11cd-6b7d-4e71-b82b-0828e0603149", "reportType": "MATCHED", "format": "CSV", "status": "SUCCEEDED", "recordsWritten": 4250, "bytesWritten": 524288, "fileName": "MATCHED_550e8400_20250101-20250131.csv", "createdAt": "2025-01-15T10:30:00Z", "startedAt": "2025-01-15T10:30:05Z", "finishedAt": "2025-01-15T10:35:00Z", "expiresAt": "2025-01-22T10:30:00Z", "downloadUrl": "/v1/export-jobs/550e8400-e29b-41d4-a716-446655440000/download" } ``` `contextId` names the context the job exports. `error` carries the job's last failure reason, when it has one. `downloadUrl` is the download route below, and it 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_550e8400_20250101-20250131.csv", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "expiresIn": 3600 } ``` > [!WARNING] > > Export files are purged after `expiresAt`, 7 days after the job was created. After that, the file is no longer downloadable. Re-run the export to regenerate it. ## Disputes *** You open a dispute against a specific **exception** when you need to contest a reconciliation discrepancy. A new dispute is `OPEN`. Closing it sets `WON` or `LOST`, and both outcomes are terminal. ### Open a dispute `POST` to the exception's disputes collection. Responds `201 Created`. ```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", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ``` `category` is one of `BANK_FEE_ERROR`, `UNRECOGNIZED_CHARGE`, `DUPLICATE_TRANSACTION`, `AMOUNT_MISMATCH`, or `OTHER`. The response omits `evidence` until the dispute has some. The `openedBy` field records the opening principal. Matcher refuses to open a dispute with `409` (`MTCH-0511`) when the exception is `RESOLVED` or `PENDING_RESOLUTION`. It answers `409` (`MTCH-0510`) while a settlement is in progress on the exception. Retry that request after the settlement completes. ### Submit evidence by URL Attach a reference to an already-hosted evidence file plus a describing comment. `fileUrl` must be an `https` URL. ```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 has a 10 MiB limit. ```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 ``` The dispute's `evidence` array lists each stored evidence item. For an uploaded file, `fileUrl` holds the object-storage key, not a download URL: ```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": "/dispute-evidence/550e8400-e29b-41d4-a716-446655440001/.pdf", "submittedAt": "2025-01-15T10:30:00Z" } ``` > [!NOTE] > > The upload endpoint returns `503` when evidence storage is unavailable. It rejects oversize bodies with `413` and content types outside the allowlist with `415`. ### Close a dispute Record the outcome. `won: true` sets the state to `WON` and `won: false` sets it to `LOST`. A `resolution` note is required. ```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` and fetch one with `GET /v1/disputes/{disputeId}`. The list endpoint filters by `state`, `category`, and date range, and it supports sorting and cursor pagination. ## Response codes *** | Status | Meaning | | - | - | | `200` | Export/dispute data returned | | `201` | Dispute opened or evidence file uploaded | | `202` | Export job accepted | | `400` | Invalid input (`SUMMARY` report type, bad date range, invalid category) | | `404` | Context, export job, exception, or dispute not found | | `409` | Export job not ready for download, or already in a terminal state when canceled. Exception not open to a new dispute, or a settlement in progress on it | | `410` | Export file expired | | `413` | Evidence file exceeds the 10 MiB cap | | `415` | Evidence content type not in the allowlist | | `422` | Malformed field (including a lowercase or unsupported `format`), or invalid dispute state transition | | `429` | Rate limit exceeded | | `503` | Asynchronous exports not enabled for your environment, or export or evidence storage unavailable | # Multi-currency matching Source: https://docs.lerian.studio/en/products/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. ![Multi-currency matching flow.](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/matcher-multicurrency-matching.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=9f773cf7a55bda246a46349536efbbe4) Multi-currency matching flow. ## How it works *** Multi-currency support lives in the existing context types (`1:1`, `1:N`, `N:M`) and match rules. There is no separate "multi-currency" context type. At match time, Matcher computes a base amount and a base currency for each transaction from FX hints in the transaction's own metadata (see [FX from transaction metadata](#fx-from-transaction-metadata) below). Rules that set `matchBaseAmount` or `matchBaseCurrency` compare these base values. You cannot upload a base amount directly. 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 | | - | - | - | | Base amount / base currency | Computed at match time, not stored | Values that base-amount rules compare, derived from FX metadata | | `matchBaseAmount` / `matchBaseCurrency` | Rule config | Make a rule compare base amounts and base currencies | | `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. The checks on the original values stay active. An EXACT rule compares `amount` while `matchAmount` is on, and EXACT and TOLERANCE rules compare `currency` while `matchCurrency` is on. Both are on by default, so a cross-currency rule turns them off. ### 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": { "matchAmount": false, "matchCurrency": false, "matchBaseAmount": true, "matchBaseCurrency": true, "matchDate": true, "matchReference": false } }' ``` `matchBaseAmount` compares the base amounts, and `matchBaseCurrency` compares the base currencies. A transaction without a base amount fails both checks. ### 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": { "matchCurrency": false, "matchBaseAmount": true, "matchBaseCurrency": true, "matchReference": false, "percentTolerance": 0.02, "absTolerance": 10.0 } }' ``` With `matchBaseAmount`, a TOLERANCE rule compares base amounts in place of the original amounts, and `absTolerance` is in base-currency units. ### Confidence scoring Base-amount rules use the same fixed component weights as all other rules. For the full scoring model, see [Confidence scoring](/en/products/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. Conversion never changes the original `amount` and `currency`, because it changes only the comparison. Matcher rounds the converted amount to the largest `roundingScale` among the context's TOLERANCE rules, or to 2 decimal places when the context has none. Match simulation does not convert, so base-amount rules find no matches in a simulation. ### Metadata fields | Metadata field | Required | Purpose | | - | - | - | | `fx_base_currency` | Yes (to trigger) | The base currency the amount is converted into. The comparison is case-sensitive, so use the same code on both sides. | | `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`. | ### Example transaction with FX metadata ```json theme={null} { "external_id": "txn_001", "amount": 1000.00, "currency": "EUR", "date": "2024-01-15", "fx_base_currency": "USD", "fx_rate": "1.085" } ``` > [!NOTE] > > When the hints are absent or invalid (unparseable rate, failed expression), the transaction gets no base amount. It cannot match under a base-amount rule and stays unmatched. The run continues. ## FX rate-variance band *** Cross-currency amounts often disagree slightly, because each side converts 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. Matcher flags both transactions with the `FX_RATE_VARIANCE` exception reason. The residual then becomes 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 checks (date window, reference, composite fields) still pass. The rule must set `matchCurrency` to `false`, because the currency check rejects pairs whose original currencies differ. 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": { "matchCurrency": false, "matchBaseAmount": true, "matchBaseCurrency": true, "matchReference": false, "percentTolerance": 0.01, "fxVarianceBand": "0.005" } }' ``` ## 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 | Both transactions carry `fx_base_currency: "USD"`. txn\_001 carries `fx_rate: "1.085"` and txn\_002 carries `fx_rate: "1"`. With a TOLERANCE rule (`matchCurrency: false`, `matchBaseAmount: true`, `matchReference: false`, `percentTolerance: 0.02`): * Base amounts: $1,085.00 vs $1,095.00 * Difference: \$10.00 * Threshold: 2% of the larger amount, \$21.90 * Result: **Match** ($10.00 < $21.90) ## Best practices *** > **Supply stable FX metadata per transaction** > > 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, with no runtime rate lookups. > **Combine with tolerance rules** > > FX conversions introduce small variances. Use TOLERANCE rules with matchBaseAmount to allow for rounding and rate timing differences. > **Document your base currency choice** > > Use a consistent base currency across all contexts. USD is common for international operations. Use your reporting currency for domestic + international. ## Next steps *** > **[Confidence Scoring](/en/products/matcher/reference/matcher-confidence-scoring)** > > How match scores work and what thresholds apply. > **[Match Rules](/en/products/matcher/configuration/matcher-match-rules)** > > Full reference for rule types and config fields. # Split and aggregate matches Source: https://docs.lerian.studio/en/products/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 *** The **context type** controls matching cardinality. 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) | One left-side transaction to many right-side transactions, for a **split** or an **aggregate** | Bulk payment covering invoices; deposits consolidated at a bank | | **N:M** (many to many) | Any combination of sources and targets | Complex netting | > [!NOTE] > > There is no separate `N:1` context type. The single transaction is always on the left side. For an aggregate, put the source that carries the consolidated entry on the left side. ## How it works *** Two mechanisms control split and aggregate behavior: 1. **Context type**: determines the matching cardinality (`1:1`, `1:N`, or `N:M`). 2. **Rule allocation flags**: control how Matcher distributes amounts within a match group. There is no separate "split" or "aggregate" setting on the context. The context type defines the allowed patterns, 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 left-side transaction to many right-side transactions | | `N:M` | Any combination of sources and targets | ### Rule allocation settings All rule types accept allocation flags in their `config`. Matcher applies them in `1:N` contexts: | Field | Type | Description | | - | - | - | | `allowPartial` | Boolean | Allow partial allocation of transaction amounts | | `allocationToleranceMode` | String | How tolerance is measured: `ABS` (absolute) or `PERCENT` | | `allocationToleranceValue` | Decimal | Tolerance threshold for allocation residuals | | `allocationUseBaseAmount` | Boolean | Allocate base (converted) amounts. | | `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 in `N:M` contexts 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: split rule with allocation In a `1:N` context, each right-side transaction must pass the rule's checks against the left-side transaction. A rule that compares amounts matches only legs equal to the whole, so a split rule turns `matchAmount` off and lets allocation check the total. TOLERANCE rules always compare amounts. ```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": 2, "config": { "matchAmount": false, "matchReference": false, "allowPartial": true, "allocationToleranceMode": "ABS", "allocationToleranceValue": 10.0 } }' ``` ## 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" }' ``` > [!TIP] > > API Reference: [Create context](/en/reference/products/matcher/create-context) ## 1:N split matching *** One left-side transaction matches many right-side 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 **Left side (Bank Statement):** | ID | Amount | Reference | | - | - | - | | bank\_001 | \$15,000.00 | BULK-PAY-2024-001 | **Right side (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) *** Many transactions match one consolidated transaction. This uses the `1:N` context type with the consolidated source on the left side. There is no 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 **Right side (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 | **Left side (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" }' ``` A `DRY_RUN` run reports its statistics but stores no match groups. To list match groups, run with `COMMIT`. ### 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 You must send the `contextId` query parameter. 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. Matcher rejects a `PROPOSED` group and revokes a `CONFIRMED` group. In both cases, its transactions return to `UNMATCHED`. Revoking a `CONFIRMED` group also withdraws its open-item residuals, voids its fee variances, and withdraws the exceptions it raised, in the same transaction. You must send the `contextId` query parameter and a `reason` 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" }' ``` ## 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**: Matcher sorts transactions deterministically to ensure reproducible results across runs. 2. **Collect**: For each left-side transaction, Matcher collects the right-side transactions that pass the rule's checks against it. 3. **Allocate**: Matcher allocates the collected transactions in sort order and stops when the remaining amount is within the allocation tolerance. It skips a transaction in another currency. It allocates only transactions with the settling sign of the context's `signConvention`: the sign of the left-side transaction under `SAME` (the default), the opposite sign under `OPPOSITE`. 4. **Track residuals**: Matcher tracks any remaining unallocated amounts. If `allowPartial` is `true`, Matcher caps an overshooting leg 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. The solver buckets candidates by the rule's match identity. It then searches for a subset of left transactions and a subset of right transactions that reconcile against each other. A group holds at most 4 transactions per side. Matcher skips a bucket that holds more than 40 transactions on either side. Only EXACT and TOLERANCE rules run in `N:M` contexts. Selection remains deterministic over the sorted input. Each proposed group must clear the fixed confidence gate (minimum score 60). 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 Matcher cannot fully reconcile surface as typed exceptions: * `SPLIT_INCOMPLETE`: allocations exist but do not fully cover the target amount, regardless of `allowPartial`. * `OVER_SETTLED`: a transaction over-settled an obligation that an earlier run carried open. Matcher surfaces the over-settled remainder as a typed break. You can filter the exceptions list by these `reason` values. ## Best practices *** > **Start with 1:N before N:M** > > Many-to-many matching is complex. Start with simpler patterns and enable N:M only when necessary. > **Use allocation tolerance for rounding** > > Small rounding differences are common in split payments. Set allocationToleranceValue to a few cents to avoid false exceptions. > **Enable partial allocation deliberately** > > Set allowPartial to true only when you expect partial matches. This prevents false matches from incomplete data. > **Dry-run before committing** > > Always test split and aggregate matching in DRY\_RUN mode first, and read the run statistics. A dry run stores no match groups. > **Monitor residuals** > > Track residual amounts over time. Growing residuals may indicate systematic matching issues. ## Next steps *** > **[Match Rules](/en/products/matcher/configuration/matcher-match-rules)** > > Configure rules and allocation settings. > **[Security](/en/products/matcher/reference/matcher-security)** > > Security and access control. # Context field maps Source: https://docs.lerian.studio/en/products/matcher/ui/context-field-maps Map each source's columns to the fields Matcher compares, from the Field Maps tab of a context in the Matcher module of Lerian Console. Use the **Field Maps** tab of a context to tell Matcher how to read each source. A field map translates a source's columns into the canonical fields that the matching engine compares. ## Accessing the Field Maps tab *** **Step 1** In the Matcher module, select **Configure → Contexts**. **Step 2** Click a context to open its detail page. **Step 3** Select the **Field Maps** tab. The **Map fields** step of the [Context setup wizard](/en/products/matcher/ui/context-setup-wizard) shows the same tab. ## Field map list *** The tab shows one row for each source of the context. Archived sources do not appear. Each row shows the source name, its type, its side, and a summary of its map: * **No field map yet**: the source has no map. Click **Add map** to create one. * A count of mapped fields, followed by the first canonical fields. Use the pencil button to edit the map and the trash button to delete it. If the context has no sources, the tab asks you to add them first. Click **Go to sources** to open the **Sources** tab. > [!NOTE] > > Matcher activates a context only when each source on this tab has a field map. A source with a **camt.053 options** value other than **Auto** needs no field map. The camt.053 parser uses the ISO 20022 mapping. See [Context sources](/en/products/matcher/ui/context-sources). ## Creating or editing a field map *** **Step 1** Click **Add map** on a source row, or the pencil button on a mapped source. The **Create field map** or **Edit field map** side sheet opens. **Step 2** Add one mapping row for each canonical field. In each row, type the canonical field on the left and the source column on the right. Click **Add field** to add a row. Click the trash button on a row to remove it. **Step 3** Optionally, fill in the **Debit and credit column** fields. See [Debit and credit column](#debit-and-credit-column). **Step 4** Click **Create map** for a new map, or **Save map** for an existing one. Matcher refuses a map that misses a required field or uses a field outside this list: | Canonical field | Required | Description | | - | - | - | | `external_id` | Yes | The transaction reference in the source | | `amount` | Yes | The transaction amount | | `currency` | Yes | The transaction currency | | `date` | Yes | The transaction date | | `description` | No | A description of the transaction | | `fee_amount` | No | The fee charged on the transaction. Fee verification reads it | | `fee_currency` | No | The currency of the fee | Matcher keeps the source columns that the map does not use as transaction metadata. [Fee rules](/en/products/matcher/ui/context-fee-rules) can test those metadata fields. ### Debit and credit column Some files do not put a minus sign on debit amounts. They mark each row as a debit or a credit in a separate column. For these files, fill in the three fields: | Field | Description | | - | - | | **Column** | The source column that holds the debit or credit marker | | **Value for a debit** | The marker of a debit row, for example `D` | | **Value for a credit** | The marker of a credit row, for example `C` | If you fill in one of the fields, fill in all three. The debit and credit values must be different. Matcher compares them without regard to case. Matcher imports debit rows as negative amounts and credit rows as positive amounts. The amount column must then hold amounts without a sign. A row with a signed amount, or with a marker that matches neither value, fails to import. ## Suggesting mappings with AI *** The editor has a **Suggest mappings with AI** panel. It reads a sample of the source file and proposes source columns for the canonical fields. Matcher sends a capped slice of the sample to the language-model provider Lerian configures for your environment. **Step 1** Paste a sample of the source file into **Source sample**. A header row and a few lines are enough. **Step 2** Click **Suggest mappings**. The panel lists each proposed mapping with a confidence meter. **Step 3** Click **Accept** on a proposal to copy it into the mapping rows. **Step 4** Review the rows, then save the map. A proposal changes nothing in Matcher. Only the map you save takes effect. If the panel shows **AI assist unavailable**, the mapping advisor is not available. Map the fields by hand. ## Deleting a field map *** Click the trash button on a mapped source. A **Delete field map?** dialog opens. Click **Delete** to remove the map. Matcher keeps the source. # Context setup wizard Source: https://docs.lerian.studio/en/products/matcher/ui/context-setup-wizard Set up a reconciliation context step by step in the Matcher module of Lerian Console: create it, add sources, map fields, add rules, activate it, and run the first match. The **Manual Setup** wizard in the Matcher module of Lerian Console guides you through the setup of one context. It takes you from a new context to a first completed match run. The source, field map, and rule steps use the same screens as the context detail page. ## Opening the wizard *** You can open the wizard from two places: * On the **Configure → Contexts** page, click **Set up by hand**. The wizard starts with a new context. * On the Matcher dashboard, the **Getting started** checklist has a **Set up by hand** button. The wizard opens on the active context, at the first step that you have not completed. ## How the wizard works *** The wizard shows the six steps in a rail. The header shows how many steps are complete. * The wizard opens on the first step that you have not completed. * You can open a completed step or the next step to do. The other steps stay locked. * Matcher decides when you complete the source, field map, and rule steps. The wizard reads the setup progress of the context from Matcher. * Click **Back** to go to the previous step. Click **Continue** to go to the next step. **Continue** becomes available when you complete the current step. * Click **Exit setup** to leave the wizard and go to the Matcher dashboard. Your work stays saved. You can resume later. ## Setup steps *** | Step | What you do | Done when | | - | - | - | | **1. Create context** | Fill in the context form and click **Create context**. See [Managing contexts](/en/products/matcher/ui/managing-contexts) for the fields | The context exists | | **2. Add sources** | Add at least one left-side source and one right-side source. See [Context sources](/en/products/matcher/ui/context-sources) | Matcher no longer asks for a source | | **3. Map fields** | Add a field map for each source. See [Context field maps](/en/products/matcher/ui/context-field-maps) | Matcher no longer asks for a field map | | **4. Create rules** | Add at least one match rule. See [Context rules](/en/products/matcher/ui/context-rules) | Matcher no longer asks for a match rule or a fee rule | | **5. Activate context** | Review the setup and click **Activate context** | The context is **Active** | | **6. Run first match** | Upload data on both sides and click **Run first match** | A match run over data from both sides completes | ### Step 1: Create context The step shows the same form as **New context**. Click **Create context** to save it. The new context becomes the active context in the Matcher sidebar. The wizard then moves to **Add sources**. When you resume the wizard on an existing context, this step shows the context name, type, and interval. ### Step 4: Create rules The step shows the **Rules** tab first. Add your match rules there. A context with a fee normalization mode also needs a fee rule. After you add the match rules, Matcher can ask for a fee rule. The step then shows the **Fee Rules** tab. Add a fee rule there. See [Context fee rules](/en/products/matcher/ui/context-fee-rules). ### Step 5: Activate context The step shows how many sources and match rules the context has. If Matcher still needs something, a **Setup incomplete** panel lists each missing item. For example: "Configure at least one fee rule." The **Activate context** button becomes available when Matcher reports that the context is ready. Click it to activate the context. ### Step 6: Run first match **Step 1** Upload a transaction file for a left-side source. Then upload a file for a right-side source. See [Uploading reconciliation data](/en/products/matcher/ui/uploading-reconciliation-data). **Step 2** Wait until each side shows its imported rows. **Run first match** becomes available when both sides have a completed import. **Step 3** Click **Run first match**. The wizard follows the run until it ends. When the run completes, the wizard shows **First match complete** with the run figures. Click **View exceptions** to open the exceptions of the run. Click **Go to dashboard** to open the Matcher dashboard. If the run fails, the wizard shows the reason. Click **Retry run** to start it again. # Context sources Source: https://docs.lerian.studio/en/products/matcher/ui/context-sources Add, edit, and archive the data sources of a reconciliation context from its Sources tab in the Matcher module, and run per-source ingestion actions. Use the **Sources** tab of a context in the Matcher module of Lerian Console to set up the data sources of a reconciliation. Sources feed the two sides of the context. Each source declares the side it belongs to and how Matcher reads its files. ## Accessing the Sources tab *** **Step 1** In the Matcher sidebar, go to **Configure → Contexts**. **Step 2** Click the name of a context to open its page. **Step 3** Select the **Sources** tab. ## Sources list *** The tab groups sources under **Left side** and **Right side**. Each row shows the source name and a type badge. A live source row can show these buttons: | Button | Action | | - | - | | Calendar | [Manage ingestion schedules](#managing-ingestion-schedules) | | Download | [Run a manual fetch](#running-a-manual-fetch) | | Document | [Extract from a document](#extracting-from-a-document) | | Pencil | [Edit the source](#editing-a-source) | | Archive | [Archive the source](#archiving-and-restoring-a-source) | Select **Show archived** to include archived sources. Archived sources show an **Archived** badge. Before you can activate a context, it needs at least one live source on each side. Archived sources do not count. ## Creating a source *** **Step 1** On the **Sources** tab, click **Add source**. The **Add source** sheet opens. **Step 2** Fill in the source details: | Field | Description | | - | - | | **Name** (required) | A name for the source, up to 50 characters | | **Type** | The kind of system the data comes from, for example **Bank** or **Ledger**. A new source starts as **Bank** | | **Side** | **Left** or **Right**. A new source starts as **Left** | **Step 3** Set how Matcher reads and imports the files of the source: | Section | Description | | - | - | | **File dialect** | **Encoding**, **Delimiter**, **Decimal style**, and **Date style**. **Auto** uses the defaults: UTF-8, comma delimiter, dot decimals, and ISO dates | | **Duplicate key** | The mapped fields that identify the same row. See [Duplicate detection](#duplicate-detection) | | **Duplicate handling** | What Matcher does with a row that repeats the duplicate key. See [Duplicate detection](#duplicate-detection) | | **Blank reference handling** | What Matcher does with a row that has no external ID. **Reject as errors** (the default) records an import error. **Import for review** imports the row as a transaction with pending-review status | | **camt.053 options** | For camt.053 files: the **Date basis** (booking date or value date) and the **Entry reference** field. **Auto** uses the booking date and `NtryRef` | | **Advanced config (JSON)** | Optional parser settings as a JSON object. Leave it empty for the defaults | **Step 4** Click **Save**. > [!NOTE] > > You map source columns to Matcher fields on the context's [Field Maps](/en/products/matcher/ui/context-field-maps) tab, not in the source form. Each live source needs a field map before you can activate the context. A source with a camt.053 option other than **Auto** does not need one. Matcher reads camt.053 files with the ISO 20022 mapping. ## Duplicate detection *** The **Duplicate key** fieldset sets which mapped fields make two rows the same row. Select one or more of `external_id`, `amount`, `currency`, `date`, `description`, `fee_amount`, and `fee_currency`. The default key is `external_id` alone. You can select `description`, `fee_amount`, and `fee_currency` only when the field map of the source fills them. A new source has no field map yet, so these three stay unavailable until you map them and edit the source. The form can show **Detection last changed** with a date. Rows that Matcher imported before that date keep the key they had. Matcher does not detect a duplicate that spans that date. Matcher does not import a row that repeats the duplicate key of an earlier row. **Duplicate handling** sets what happens next: | Option | Result | | - | - | | **Flag as exception (default)** | Matcher keeps the original row and opens a duplicate exception for review. You find it in [Exceptions](/en/products/matcher/ui/managing-exceptions) | | **Keep first** | Matcher keeps the original row and drops the repeat. No exception opens | | **Reject as errors** | Matcher records each repeat as an import error | ## Editing a source *** Click the pencil button on a source. The **Edit source** sheet opens with the same fields. Change the fields and click **Save**. ## Managing ingestion schedules *** An ingestion schedule sets when a source pulls in data by itself. It is separate from the [matching schedules](/en/products/matcher/ui/context-schedules) of the context, which set when matching runs. **Step 1** Click the calendar button on a source. The **Ingestion schedules** sheet opens. It lists the schedules of the source, enabled and disabled. **Step 2** Click **Add ingestion schedule**. **Step 3** Fill in the schedule: | Field | Description | | - | - | | **Rail** | **File** pulls files over a transport. **Query** reads data through a [discovery connection](/en/products/matcher/ui/discovery-connections). You cannot change the rail later | | **Format** (required) | The format of the data that the schedule pulls in | | **Schedule** (required) | How often the source pulls, as a duration such as `15m`, `1h`, or `24h` | | **Enabled** | On by default. A disabled schedule does not run | | **Transport** | For a **File** rail: the **Protocol**, **Host**, **Path**, and the other fields of the remote endpoint. **Host** is required | | **Connection** | For a **Query** rail: the discovery connection to read from | **Step 4** Click **Create schedule**. To change a schedule, click its pencil button, change the fields, and click **Save**. To delete a schedule, click its trash button. In the **Delete ingestion schedule?** dialog, click **Delete**. The schedule stops running, and you cannot undo the delete. ## Running a manual fetch *** A manual fetch pulls files from a remote endpoint and imports them now. **Step 1** Click the download button on a source. The **Run a manual fetch** sheet opens. **Step 2** Fill in the **Transport** fields. The sheet marks the required fields. **Step 3** Optionally, type a **Format hint (optional)**, such as `csv`. Matcher uses it for files that do not declare a format. **Step 4** Click **Run fetch**. The sheet then lists each file it processed, with its transaction count or its error. When no file matches the path, the sheet says so. When manual fetch is off, the sheet shows a message and fetches nothing. ## Extracting from a document *** Use document extraction to get transactions out of a PDF statement. Matcher sends a capped slice of the document's text layer to the language-model provider Lerian configures for your environment. When the PDF has no text layer, Matcher sends the PDF itself. Matcher proposes candidate transactions. Matcher imports nothing until you approve them. **Step 1** Click the document button on a source. The **Extract from a document** dialog opens. **Step 2** Select one PDF file in **PDF document**. The file can be up to 100 MB. **Step 3** Click **Extract**. **Step 4** When the dialog shows the number of candidates, click **Open review**. You review the candidates on the [Extraction Review](/en/products/matcher/ui/extraction-review) page. When document extraction is off, the dialog shows a message and extracts nothing. ## Archiving and restoring a source *** You cannot delete a source. You archive it instead. An archived source leaves the live sources, the setup readiness, and matching of the context. Matcher keeps its imported transactions and its field map. **Step 1** Click the archive button on a source. The **Archive source?** dialog opens. **Step 2** Click **Archive** followed by the source name. **Step 3** To bring the source back, select **Show archived** and click the restore button on the source. In the **Restore source?** dialog, click **Restore** followed by the source name. # Managing contexts Source: https://docs.lerian.studio/en/products/matcher/ui/managing-contexts Create and manage reconciliation contexts in the Matcher module of Lerian Console: set the type, interval, sign convention, aging and fee normalization, then follow the next setup step. Use the **Contexts** page in the Matcher module of Lerian Console to create and manage reconciliation contexts. A context groups the sources, field maps, rules, and schedules of one reconciliation. ## Accessing the Contexts page *** In the Matcher module, select **Configure → Contexts**. ## Context list *** The table hides archived contexts until you set the **Status** filter to **Archived**. You can also filter by **Type**. The list shows 50 contexts per page, with **Previous** and **Next** controls. | Column | Description | | - | - | | **Name** | The context name. Click it to open the context detail page | | **Type** | Matching cardinality: **One-to-one**, **One-to-many**, or **Many-to-many** | | **Interval** | The cadence label of the context | | **Status** | Current state of the context | | **Created** | When the context was created | ### Context statuses | Status | Description | | - | - | | **Draft** | You configure the context before you activate it | | **Active** | The context is operational and accepts match runs | | **Paused** | The context is temporarily suspended | | **Archived** | The context is retired. Matcher keeps its history | You archive and restore contexts. You do not delete them. See [Context actions](#context-actions). ## Creating a new context *** The Contexts page gives you two ways to create a context: * **New context** opens a side sheet with the context form. Fill in the fields and click **Save**. The Console then opens the detail page of the new context. * **Set up by hand** opens the guided setup. It takes you from the context form to a first match run in six steps. See [Context setup wizard](/en/products/matcher/ui/context-setup-wizard). Both paths create the context in the **Draft** status. The form has these fields: | Field | Description | | - | - | | **Name** (required) | A descriptive name for the context, up to 100 characters (e.g., "Bank Reconciliation Q1") | | **Type** (required) | Matching cardinality: **One-to-one** (`1:1`), **One-to-many** (`1:N`), or **Many-to-many** (`N:M`). You cannot change it after you create the context | | **Interval** (required) | A free-text cadence label of up to 100 characters, such as `daily` or `weekly`. It does not start runs. To run matching on a timetable, add a [matching schedule](/en/products/matcher/ui/context-schedules) | | **Auto-match on upload** | When on, a match run starts after each upload to an **Active** context completes | | **How a payment appears** | The sign that a payment carries next to the invoice or charge it pays. See [Sign convention](#sign-convention) | | **Aging threshold (days)** | Under **Open-item aging**. The number of days after which Matcher flags an open-item residual as aged. Leave it empty to disable aging for this context | | **Aging calendar** | Under **Open-item aging**. How Matcher measures the age: **Raw calendar days**, **US Federal**, or **Brazil ANBIMA** business days | | **Mode** | Under **Fee normalization**. **None**, **Net**, or **Gross**. **Net** compares amounts after fees. **Gross** compares amounts before fees | | **Abs tolerance** and **Pct tolerance** | Under **Fee normalization**. They appear after you select a mode. They set how far the actual fee can differ from the expected fee. Matcher applies the larger of the two | If you select a fee normalization mode, add a fee rule on the [Fee Rules](/en/products/matcher/ui/context-fee-rules) tab. You define the fee schedules themselves in **Settings → Fee Schedules**. See [Managing fee schedules](/en/products/matcher/ui/managing-fee-schedules). ### Sign convention The **How a payment appears** field has two options: * **Same sign as what it pays** (default): a payment carries the same sign as the item it settles. A negative amount is a reversal. * **Opposite sign of what it pays**: a payment carries the opposite sign of the invoice it settles. Choose the convention before the first match run. Matcher refuses a change after the context carries an open-item balance. It also refuses a change while a match run is in progress. ## Context detail page *** Click a context name in the list to open its detail page. ### Next step card While a context is not ready, a **Next step** card appears above the tabs. It tells you what Matcher needs next, for example: "Add at least one right-side source." Click **Continue setup** to open the tab where you complete that step. The card disappears when the context meets every activation requirement. ### Tabs | Tab | Description | | - | - | | **General Information** | The current settings of the context and when it was created and updated | | **Sources** | The data sources of the context. See [Context sources](/en/products/matcher/ui/context-sources) | | **Field Maps** | How each source's columns map to the fields that Matcher compares. See [Context field maps](/en/products/matcher/ui/context-field-maps) | | **Rules** | The match rules of the context. See [Context rules](/en/products/matcher/ui/context-rules) | | **Matching schedules** | When matching runs automatically. See [Context schedules](/en/products/matcher/ui/context-schedules) | | **Fee Rules** | Which fee schedule applies to which transactions. See [Context fee rules](/en/products/matcher/ui/context-fee-rules) | ### Context actions The actions sit in the page header, so you can use them from every tab: | Action | Description | | - | - | | **Edit context** | Opens a side sheet with the context form. You can change every field except **Type**. Click **Save** to apply | | **Clone context** | Creates a new context from this setup. Enter a **Clone name**. **Include sources** copies the sources and their field maps. **Include rules** copies the match rules and fee rules. Click **Create clone** | | **Archive context** | Retires the context. Matcher keeps its history, and you can restore the context later | | **Restore context** | Appears on an archived context. It returns the context to **Draft**. Activate it again to resume matching | The edit form also has a **Status** field with **Active**, **Paused**, and **Archived**. A **Draft** context also lists **Draft**. To pause or resume a context, change its status there. To activate a **Draft** context, set its status to **Active**. Matcher refuses the activation until the setup is complete. The **Next step** card names the next item to set up. > [!NOTE] > > You manage sources, field maps, rules, matching schedules, and fee rules inside the context detail page. They have no separate pages. # Matcher dashboard Source: https://docs.lerian.studio/en/products/matcher/ui/matcher-dashboard Watch reconciliation health for the active Matcher context: match rate, cash impact, exception SLA, match-rate trend, and source breakdown. The Matcher module in Lerian Console opens on the **Reconciliation dashboard**. The dashboard shows the health of one reconciliation context over a period. It shows how much of the pool matched and what stays open. It also shows the unmatched cash exposure and how fast your team resolves exceptions. ## Accessing the dashboard *** Click **Dashboard** in the Matcher sidebar. The dashboard shows the **Active context** that you select in the Matcher sidebar. To see another context, change the **Active context**. Choose the period with the buttons in the page header. The options are **Last 7 days**, **Last 30 days**, and **Last 90 days**. The default is **Last 90 days**. > [!NOTE] > > The dashboard refreshes every 30 seconds. The status rail shows when the figures last updated, with a **Refresh** button. If a refresh fails, the rail says so and shows a **Retry** button. The figures on the page can then be out of date. The sections below follow the order of the page, from top to bottom. ## Status rail *** A rail across the top of the page shows these items: | Item | Description | | - | - | | Context name | The active context. | | **Total pending** | Pending exceptions, within SLA and overdue together. | | **Pending overdue** | Pending exceptions past their SLA. The badge turns red when the count is above zero. | | Last update | The time of the last refresh, for example **Updated just now**, and the **Refresh** button. | ## Match rate *** The **Match rate** card is the main figure of the page. A bar splits the transactions of the context into matched and unmatched, by count. A marker on a 0 to 100 ruler shows the match rate. Below the bar, two columns show the **Matched** and **Unmatched** totals. Each column holds a transaction count and an amount. The unmatched amount shows in red when unmatched transactions exist. When the figures share one currency, a **By amount** line shows the match rate by amount. It also shows the difference from the rate by count (**Δ vs count**). With more than one currency in the figures, the amounts show a dash. The note **Currency not available for this total** explains the dash. ## Cash impact and exception SLA *** Two cards sit next to the **Match rate** card: | Card | Description | | - | - | | **Cash impact** | The unmatched cash exposure for the period, per currency, with the transaction count of each currency. A **By aging** list splits the exposure into age buckets. With more than one currency, the bucket amounts show a dash. | | **Exception SLA** | **SLA compliance** as a percentage, then **Resolved on time** (with the late count), **Pending (within SLA)**, **Pending overdue**, and **Average resolution**. An **Exception aging** list counts exceptions in three age buckets: `<24h`, `1-3d`, and `>3d`. | ## Match rate over time *** The **Match rate over time** card plots the daily match rate for the period. Days with no imported data do not appear on the line. Above the line, **pts vs prior day** shows the change from the previous day on the line, in percentage points. Watch the line for a fall in the match rate. A fall can point to a rule that no longer fits your data, or to a problem in the data. With only one day of activity, the card shows the rate of that day instead of a line. With no activity in the period, the card says that the line needs at least two days. ## Source breakdown *** The **Source breakdown** card lists each source with its match rate and a small balance bar. It also gives the matched and unmatched transaction counts of each source. Use it to find the sources that leave the most items unmatched. The card does not show when the period has no data per source. ## First-run states *** The dashboard needs a context with reconciliation data. Until then, it shows one of these states: | State | What the page shows | | - | - | | No context exists | A message that asks you to pick a reconciliation context, and a **Go to contexts** button that opens the **Contexts** page, where you create one. | | The context has no data in the period | A message that the context has no reconciliation data yet. One button opens the next setup step, for example **Add sources** or **Run a match**. | ## Getting started checklist *** When the context has data but its setup is not complete, a **Getting started** checklist shows below the status rail. It counts the complete steps and lists these five steps: 1. **Create a reconciliation context** 2. **Add both data sources** 3. **Create a match rule** 4. **Activate the context** 5. **Complete your first match run** The current step has a button that opens the place where you complete it. Use **Set up by hand** to open the guided setup for the context. See [Context setup wizard](/en/products/matcher/ui/context-setup-wizard). Use the arrow button to collapse or expand the list. The checklist closes when all five steps are complete. Click **Dismiss** to close it at once. After either event, the checklist does not come back in the same browser. ## Where the runs live *** The dashboard summarizes health. It does not list runs. To start a run, use **Operate → Reconciliations**. See [Running reconciliations](/en/products/matcher/ui/executing-reconciliation). For the full run and import history, see [Operational history](/en/products/matcher/ui/operational-history). # Matcher module pages Source: https://docs.lerian.studio/en/products/matcher/ui/matcher-module Find each screen of the Matcher module in Lerian Console by sidebar group: configure contexts, import data, run matches, resolve exceptions, and report. The **Matcher module** of Lerian Console is where you operate Matcher, Lerian's reconciliation engine. You configure contexts and match rules, import transaction data, run reconciliations, review results, and resolve exceptions. This page lists the main pages of the module by sidebar group. If you're new to it, start with [Matcher in Lerian Console](/en/products/matcher/ui/matcher-ui-overview). > [!WARNING] > > Matcher is only available if you've acquired the Matcher product. For access, [get in touch with our team](https://lerian.studio/contact). ## Dashboard *** The module opens on the **Dashboard**, which shows the reconciliation health of the active context. > **[Matcher dashboard](/en/products/matcher/ui/matcher-dashboard)** > > See a walkthrough of the Dashboard: the match rate card, the match-rate trend, cash impact, and exception SLA. ## Available pages *** The sidebar groups pages by workflow phase. ### Configure > **[Contexts](/en/products/matcher/ui/managing-contexts)** > > Create and manage reconciliation contexts. Each context groups sources, field maps, rules, schedules, and fee rules. > **[Guided setup](/en/products/matcher/ui/context-setup-wizard)** > > Build a context step by step, from its creation to its first match run. It opens from the Contexts page. > **[Rule Suggestions](/en/products/matcher/ui/rule-suggestions)** > > Review the match rules Matcher proposes for a context, then approve or reject them. ### Operate > **[Reconciliations](/en/products/matcher/ui/executing-reconciliation)** > > Start reconciliation runs for the active context and follow their progress. Each run's detail page holds its match groups. > **[Operational History](/en/products/matcher/ui/operational-history)** > > Track reconciliation runs and import jobs for the active context. > **[New Reconciliation](/en/products/matcher/ui/uploading-reconciliation-data)** > > Upload a transaction file into a source of the active context and follow the import. > **[Match groups](/en/products/matcher/ui/managing-matches)** > > Review the match groups of a run on its detail page, which opens from Reconciliations. ### Resolve > **[Unmatched](/en/products/matcher/ui/manual-matching)** > > Work the transactions the engine could not match. Pair them manually or ignore them. > **[Open Items](/en/products/matcher/ui/open-items)** > > Follow the remainders that partial matches carry forward. > **[Extraction Review](/en/products/matcher/ui/extraction-review)** > > Check the transactions Matcher extracted from a document before they enter a source. ### Exception Handling > **[Exceptions](/en/products/matcher/ui/managing-exceptions)** > > View and resolve the transactions that the engine could not match. > **[Disputes](/en/products/matcher/ui/managing-disputes)** > > Manage disputed transactions that need evidence and a decision. ### Reporting > **[Portfolio, Reports, and Exports](/en/products/matcher/ui/reports-and-exports)** > > Compare contexts, read reconciliation reports, and download exports. > **[Audit Logs](/en/products/matcher/ui/managing-audit-logs)** > > View the hash-chained record of configuration and operational changes. > **[Archives](/en/products/matcher/ui/managing-archives)** > > Browse and download completed audit-log archives. ### Settings > **[Layout Templates](/en/products/matcher/ui/layout-templates)** > > Describe fixed-width file layouts so Matcher can read them. > **[Fee Schedules](/en/products/matcher/ui/managing-fee-schedules)** > > Define expected fee structures (flat, percentage, tiered, or expression-based) for fee verification during reconciliation. > **[Actor Mappings](/en/products/matcher/ui/managing-actor-mappings)** > > Link opaque actor IDs to a display name or email. > **[Discovery](/en/products/matcher/ui/discovery-connections)** > > Connect external data sources, browse their schemas, and extract data from them. > **[Aggregators](/en/products/matcher/ui/aggregator-connections)** > > Connect open-finance data aggregators that feed transactions into Matcher. # Matcher in Lerian Console Source: https://docs.lerian.studio/en/products/matcher/ui/matcher-ui-overview Operate Matcher from its Lerian Console module: configure contexts, sources, and rules, import files, run matches, and resolve exceptions without writing API calls. Matcher, Lerian's reconciliation engine, has a module in Lerian Console. The **Matcher module** puts the reconciliation workflow behind a visual interface. You configure contexts, sources, and match rules, import transaction files, run reconciliations, review matches, and resolve exceptions, all without writing API calls. > [!WARNING] > > Matcher is only available if you've acquired the Matcher product. For access, [get in touch with our team](https://lerian.studio/contact). ## Opening the Matcher module *** To use the module, turn on Matcher for your organization in **Settings → Products**. See [Products](/en/platform/console/settings-products). To open the module, select the **Matcher** card on the Lerian Console home page. If Matcher is off for your organization, each Matcher page shows a message that says so. ## Active context *** The Matcher sidebar holds the **Active context** selector. Screens that work on one reconciliation context, such as the Dashboard, Reconciliations, and Unmatched, use the active context. Switch it to change the data these screens show. The Console keeps your choice in this browser. If the stored context no longer exists, the Console selects the first context in the list. ## Main work surfaces *** > **[Dashboard](/en/products/matcher/ui/matcher-dashboard)** > > Reconciliation health at a glance for the active context: match rate, cash impact, and exception SLA. > **[Reconciliation setup](/en/products/matcher/ui/managing-contexts)** > > Create contexts and manage their sources, field maps, rules, schedules, and fee rules from each context's detail page. > **[Imports and runs](/en/products/matcher/ui/executing-reconciliation)** > > Import transaction files and start reconciliation runs, then follow their progress. > **[Matches](/en/products/matcher/ui/managing-matches)** > > Review the match groups of each run, or pair the remaining transactions in the Unmatched workbench. > **[Exceptions and disputes](/en/products/matcher/ui/managing-exceptions)** > > Work the queue of transactions that did not match automatically. Resolve them, or escalate them into disputes. > **[Reporting](/en/products/matcher/ui/reports-and-exports)** > > Portfolio health, reports, exports, audit logs, and archives. For a directory of the module's screens by sidebar group, see [Matcher module pages](/en/products/matcher/ui/matcher-module). ## Beyond the Matcher module *** The Matcher module works through the Matcher API, so you can mix surfaces: * **API**: automate any workflow programmatically. Start with the [developer quick start](/en/reference/products/matcher/matcher-developer-quick-start). * **MCP server**: let an AI assistant operate Matcher through the [Model Context Protocol](/en/products/matcher/mcp/matcher-mcp-overview). # Pix reconciliation Source: https://docs.lerian.studio/en/products/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 Matcher context that reconciles your Pix transactions against BACEN's SPI settlement extracts every day. ## Pix transaction flows *** The two flows below show what Matcher needs to reconcile on each side. ### Pix sent (cash-out) ![Pix sent cash-out flow](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/matcher-pix-cashout-flow.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=fe721fcb3a4d1d78dc81387176d2fea4) Cash-out flow: from client initiation through SPI settlement to reconciliation in Matcher. 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 becomes committed. 5. **Matcher reconciles**: Matcher compares the committed Midaz transaction against the corresponding entry in the BACEN SPI settlement extract. ### Pix received (cash-in) ![Pix received cash-in flow](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/matcher-pix-cashin-flow.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=66eac6d7ed7a511890619ecd5c42a989) Cash-in flow: from inbound SPI notification through Midaz credit to reconciliation in Matcher. 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. > [!NOTE] > > 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 *** **1. Create the context** 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", "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. The match rules below compare amounts exactly. Setting `autoMatchOnUpload` to `false` gives you control over when matching runs. Use that control when you need both sources ingested before the run executes. > [!TIP] > > See the full request schema at [Create context](/en/reference/products/matcher/create-context). **2. Create the sources** 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. You then upload the export to this source, manually or via an automated pipeline. See [Matcher and Midaz](/en/products/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 you upload the SPI settlement file 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. Assign one side to Midaz and the other to BACEN. Keep the assignment consistent across both sources. > [!TIP] > > See the full request schema at [Create source](/en/reference/products/matcher/create-source). **3. Create field maps** 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 whose keys come from Matcher's closed vocabulary: `external_id`, `amount`, `currency`, and `date`, plus the optional `description`, `fee_amount`, `fee_currency`, and `direction`. The `direction` key takes an object, `{column, debit, credit}`, for a file that marks each row as debit or credit in a separate column. Every other key takes a raw column name in the source. Lookups are flat. A mapping value must name a top-level column in the row. The Midaz export must therefore carry `endToEndId` as its own flat column (see [Matcher and Midaz: custom field mapping](/en/products/matcher/integrations/matcher-midaz-integration#custom-field-mapping)). > [!WARNING] > > `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" } } ``` > [!TIP] > > See [Create field map](/en/reference/products/matcher/create-field-map) for the full request schema and [Field mapping](/en/products/matcher/configuration/matcher-field-mapping) for the canonical vocabulary. **4. Create match rules** 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 } } ``` This rule resolves approximately 95% of cases. The `endToEndId` is unique per Pix transaction across the entire ecosystem. A match on reference, amount, currency, and date gives a confirmed reconciliation with maximum confidence. The rule sets `caseInsensitive` to `false` because `endToEndId` values are case-sensitive. The rule sets `referenceMustSet` to `true`, so both sides must carry the `endToEndId` before comparison. 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, "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. This rule relies on amount and currency matching only. Rule 1 handles the `endToEndId` comparison. > [!TIP] > > See [Create match rule](/en/reference/products/matcher/create-match-rule) for the full request schema and all available rule types. **5. Activate and schedule** 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. It also gives you time to upload the daily file before the matching run executes. > [!TIP] > > See [Update context](/en/reference/products/matcher/update-context) and [Create schedule](/en/reference/products/matcher/create-schedule) for the full request schemas. ## Daily operation *** Once configured, the daily reconciliation workflow follows five steps. **1. Upload BACEN extract** 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/products/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 "format=csv" \ -F "file=@bacen_spi_2026-03-17.csv" ``` You can automate this step with a pipeline that fetches the SPI file and uploads it before the scheduled matching run. **2. Upload the Midaz export** Export the previous day's committed Pix transactions from Midaz, including the `endToEndId` metadata as a flat column. Upload the export to the Midaz source the same way. The same pipeline usually automates this step. See [Matcher and Midaz](/en/products/matcher/integrations/matcher-midaz-integration) for the export/import flow. **3. Matcher runs at 07:00 (or manually)** 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" }' ``` **4. Review results** 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?contextId={contextId}" \ -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. **5. Resolve exceptions** 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. > [!TIP] > > Always run a `DRY_RUN` first when testing new rules or after configuration changes. This prevents Matcher from committing unintended matches. ## 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 | Proposed, D+1 (DATE\_LAG, score: 90) | | E11122...0007 | txn-004 ↔ none | Exception (no rule) | | E44455...0055 | none ↔ liq-8804 | Exception (no rule) | ### 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) proposed this pair with confidence score 90. Matcher never confirms a `DATE_LAG` group automatically: it proposes the group, whatever its score. To pair the two transactions, un-match the proposed group, then create a manual match. * **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. This inbound Pix did not reach the ledger. 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). | Investigate original records. Create a manual match if the difference is acceptable. | | Date mismatch (>1 day) | Held transaction, reprocessing | Verify it is the same Pix. Create a manual match, or ignore the transaction. | ### Manual match When you have confirmed that two records represent the same Pix transaction but Matcher could not match them automatically, pair them with a manual match. Matcher creates a `CONFIRMED` match group at confidence 100. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/matching/manual?contextId={contextId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "transactionIds": ["{midazTransactionId}", "{bacenTransactionId}"], "notes": "Same Pix confirmed via endToEndId lookup in SPI. D+2 settlement delay confirmed with BACEN" }' ``` ### Ignore transaction When you must exclude a transaction 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" }' ``` > [!TIP] > > See [Create manual match](/en/reference/products/matcher/create-manual-match) and [Ignore transaction](/en/reference/products/matcher/ignore-transaction) for the full request schemas. ## Pix refunds (devoluções) *** Pix refunds generate reverse transactions that also need reconciliation. When the Pix plugin processes a refund, it 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, so Matcher can reconcile them against the corresponding Midaz refund transactions. For low refund volumes, you can reconcile these 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. > [!NOTE] > > 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/interfaces/pix/pix-overview). ## Best practices *** > **Use endToEndId as the primary reference** > > The `endToEndId` is the unique Pix identifier across the entire ecosystem, from the initiating institution through SPI to the receiving institution. Store it in Midaz transaction metadata and make sure the BACEN extract carries it. Without it, reconciliation falls back to amount and date matching, which is far less reliable. > **Run reconciliation the following day** > > Pix transactions near end of day may settle in BACEN on D+1. Scheduling Matcher for 07:00 UTC ensures the BACEN extract includes all settlements from the previous day before matching runs. This eliminates false exceptions caused by timing. > **Separate Pix IN and Pix OUT for high volume** > > 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. > **Monitor match rate** > > 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. > **Zero tolerance is the default** > > 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 `feeTolerance` at `0` on the `DATE_LAG` rule. > **Preview before committing** > > 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 | > [!TIP] > > Use Matcher's dashboard endpoints to monitor these metrics. See [Dashboard metrics](/en/reference/products/matcher/get-dashboard-metrics). ## Next steps *** > **[Contexts and sources](/en/products/matcher/configuration/matcher-contexts-and-sources)** > > Learn how to configure and manage reconciliation contexts and data sources. > **[Match rules](/en/products/matcher/configuration/matcher-match-rules)** > > Explore all available rule types and advanced matching configurations. > **[Midaz integration](/en/products/matcher/integrations/matcher-midaz-integration)** > > Learn about automatic field mapping and the Midaz Ledger data source. > **[Resolving exceptions](/en/products/matcher/daily-reconciliation/matcher-resolving-exceptions)** > > Detailed guide on investigating, force matching, and managing reconciliation exceptions. # What is Matcher? Source: https://docs.lerian.studio/en/products/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 (The process of comparing two sets of records (for example, your internal ledger against bank statements) to verify they match and flag discrepancies.) engine that automates transaction reconciliation between any data sources: internal systems, banks, payment processors, ERPs (Enterprise Resource Planning, business management software (SAP, Oracle, NetSuite) that integrates accounting, procurement, and inventory processes.), or payment gateways. Matcher is available to licensed institutions. Lerian maintains its repository 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 * **Stay compliant**: An append-only audit log records configuration changes and exception resolutions ## What Matcher does *** * **Match automatically**: Apply your match 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, assigned, and tracked until resolved * **Audit changes**: An append-only audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) records configuration changes and exception resolutions ### 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 * **Adjustments**: Record a balancing entry, such as a bank fee or an FX difference, on a match group or a transaction ### 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/products/matcher/imports/matcher-import-formats) * Automatic validation and duplicate detection * Flexible field mapping for heterogeneous data sources * Pull transactions through database and REST API connections * Optional: reconcile Midaz Ledger exports that you import as a `LEDGER` source ### Workflow integration * Receive resolution callbacks from external systems ## Common use cases *** > **Bank reconciliation** > > 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. > **Payment processors** > > Reconcile Stripe or Adyen settlements against your ledger or ERP. Handle splits (one payment, many invoices) and aggregates (many payments, one deposit) automatically. > **Card networks** > > Match Visa, Mastercard, or Elo settlement files against your transaction records. Tolerance rules handle fee variances and timing differences between authorization and settlement. > **Acquiring reconciliation (Interchange Plus)** > > 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. > **ERP sync** > > Verify SAP, Oracle, or NetSuite transactions against your other systems. Catch discrepancies before month-end close or audits. ## How Matcher works *** **1. Configure** Define what you're reconciling (contexts), where data comes from (sources), and how to match (rules). **2. Ingest** Upload files or pull data automatically. Matcher validates, normalizes, and deduplicates everything. **3. Match** Run a matching job. Matcher applies your rules and assigns confidence scores to each proposed match. **4. Review** Matcher confirms `EXACT` and `TOLERANCE` matches with a score of 90 or above automatically. Matcher never confirms a `FUZZY` or `DATE_LAG` match automatically: it sends the match to review, whatever its score. Lower scores and unmatched items go to your review queue. **5. Resolve** Handle exceptions: force a match, create an adjustment, or open a dispute. **6. Report** Export reconciliation reports. The audit log records configuration changes and exception resolutions. ## 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/products/midaz/about-midaz) | Optional: Reconcile exported Ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.) data that you import as a `LEDGER` source | > [!NOTE] > > 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. > [!NOTE] > > Matcher reads an idempotency key only from the `X-Idempotency` header, and only when you send one. Matcher does not use a key that you send in `X-Idempotency-Key` or `Idempotency-Key`. ## Next steps *** > **[Core concepts](/en/products/matcher/matcher-concepts)** > > Understand the key concepts: contexts, sources, rules, matches, and exceptions. > **[Architecture](/en/products/matcher/matcher-architecture)** > > Explore Matcher's architecture and data flow. > **[Quick start](/en/products/matcher/getting-started/matcher-quick-start)** > > Run your first reconciliation, then review matches and exceptions with confidence. > **[Match rules](/en/products/matcher/configuration/matcher-match-rules)** > > Learn how to configure exact, tolerance, and date-based matching rules. > [!TIP] > > [Explore the Matcher API](/en/reference/products/matcher/matcher-developer-quick-start). Browse endpoints for reconciliation contexts, sources, and matching rules. # Billing Runs Source: https://docs.lerian.studio/en/products/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. > [!NOTE] > > Enable the Fees Engine in [Midaz module settings](/en/platform/console/plugin-settings) before you use this page. ## Key concepts *** * **Preview only**: A Billing Run shows an extract of what it *would* bill for the selected period. It posts nothing. * **Period type**: The length of the period, either **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. * **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. * **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 *** **1. Open the Fees Engine** In the Midaz Module sidebar, expand **Fees Engine**. **2. Select Billing Runs** Choose **Billing Runs** to open the run page. **3. Set the run parameters** Pick the **period type** and **reference date**. Choose the **package type**. **4. Calculate the preview** 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 *** > **[Fees Engine overview](/en/products/midaz/fees/console/fees-engine-overview)** > > Understand how the Fees Engine, packages, and rules fit together. # APIX 001 guidance withdrawn Source: https://docs.lerian.studio/en/products/reporter/apix-001 The previous APIX 001 template guidance is withdrawn pending an authoritative Pix contract. The previous APIX 001 template instructions were withdrawn. The current Reporter product source does not establish the Pix data model, APIX fields, submission schedule, or regulatory mapping needed to validate those instructions. Reporter can render templates from configured data sources, but it does not turn an unverified field mapping into a regulatory contract. Do not use the previous examples to produce or submit an APIX 001 report. Define the data mapping and output with an authoritative Pix or BACEN contract, then validate it with your compliance team before operational use. # APIX 002 guidance withdrawn Source: https://docs.lerian.studio/en/products/reporter/apix-002 The previous APIX 002 template guidance is withdrawn pending an authoritative Pix contract. The previous APIX 002 template instructions were withdrawn. The current Reporter product source does not establish the Pix data model, APIX fields, submission schedule, or regulatory mapping needed to validate those instructions. Reporter can render templates from configured data sources, but it does not turn an unverified field mapping into a regulatory contract. Do not use the previous examples to produce or submit an APIX 002 report. Define the data mapping and output with an authoritative Pix or BACEN contract, then validate it with your compliance team before operational use. # CADOC 4010 and 4016: validation required Source: https://docs.lerian.studio/en/products/reporter/cadoc-4010-and-4016 Use authoritative regulatory requirements to validate any CADOC 4010 or 4016 mapping before operational use. The previous ready-to-submit template guidance on this page has been withdrawn. Reporter can render templates from configured data sources, but the current product source does not establish a CADOC regulatory mapping. In particular, Midaz treats `OperationRoute.code` as an unconstrained legacy string. It does not make that field a 10-digit COSIF account code. The source also does not establish `available_balance_after` (including a value grouped by account and route) as a supported regulatory balance. Do not use the previous examples as a basis for a CADOC submission. ## What to do instead *** 1. Obtain the applicable official CADOC layout and validation rules. 2. Define the mapping from your institution's source data to that layout. 3. Validate field semantics, aggregation, dates, and identifiers with your compliance team. 4. Test the generated file against the authoritative validator before operational use. # CADOC 4111: validation required Source: https://docs.lerian.studio/en/products/reporter/cadoc-4111 Use authoritative regulatory requirements to validate any CADOC 4111 mapping before operational use. The previous ready-to-submit template guidance on this page has been withdrawn. Reporter can render templates from configured data sources, but the current product source does not establish a CADOC 4111 regulatory mapping. Midaz treats `OperationRoute.code` as an unconstrained legacy string. It does not make that field a 10-digit COSIF account code. The source also does not establish `available_balance_after`, including a value selected or grouped by account and route, as a supported regulatory balance. Do not use the previous examples as a basis for a CADOC 4111 submission. ## What to do instead *** 1. Obtain the applicable official CADOC 4111 layout and validation rules. 2. Define the mapping from your institution's source data to that layout. 3. Validate field semantics, aggregation, dates, and identifiers with your compliance team. 4. Test the generated file against the authoritative validator before operational use. # CCS Reports Source: https://docs.lerian.studio/en/products/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 generates 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 with Reporter: ACCS001, ACCS005, and ACCS010. You combine CRM data (holders and aliases) with Midaz structures. ## Before using the CCS templates *** The examples below map the reserved CRM datasource named `plugin_crm`. Configure that datasource before importing or saving a CCS template: it needs the CRM MongoDB connection and `DATASOURCE_CRM_MIDAZ_ORGANIZATION_ID`, which scopes CRM records to one Midaz organization. Follow [Reporter via Helm](https://github.com/LerianStudio/helm/tree/main/charts/reporter) or the [environment-variable reference](/en/products/reporter/reporter-environment-variables#reserved-crm-datasource). `plugin_crm` is environment-managed. Do not try to create or PATCH it through the regular datasource API. Manager can seed the entry and fill a missing or empty organization ID at startup. Manager keeps a non-empty persisted value. Before treating a CCS template as ready, import or save it, generate a report, confirm that CRM fields decrypt, and verify that the output contains records only from the intended Midaz organization. `RPT-0075` is a fail-closed scope check: it can mean the organization metadata is absent or empty, but it can also mean Reporter could not read the datasource registry. Use [RPT-0075 troubleshooting](/en/reference/products/reporter/reporter-error-list#rpt-0075-crm-organization-scope-unresolved) instead of assuming a missing environment variable. > [!CAUTION] > > 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. It lets regulators and other authorized entities 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. ## 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. It keeps the registry in sync with daily operations. ### Full template ``` {# reporter:output-encoding=utf-16be #} 12345678 00000000 ACCS001 12233444 {%- for alias in plugin_crm.aliases -%} {%- for holder in plugin_crm.holders -%} {%- if holder.document == alias.document %} I N {%- 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) to accurately associate account data with their respective owners. * The `` field is conditional: * F for **natural persons** * J for **legal entities** * **Fixed values in this example:** * `` = I (Inclusion) This scenario sets the field to I. The field must still reflect the actual operation that you report.\ 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 ``` 12345678 00000000 ACCS001 12233444 I N F 12345678900 2025-02-01 1 2025-02-01 ``` ## ACCS005: Relationship detail file *** ### What it is You generate the ACCS005 with Reporter when BACEN requests detailed information on client relationships. It lists all the links between a customer and financial assets (BDVs: Bens, Direitos e Valores). It can also include **related persons** such as partners or legal representatives. ### Full template ``` {# reporter:output-encoding=utf-16be #} 12345678 00000001 ACCS005 {% 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 Nested loops iterate through accounts, aliases, and holders to extract all relevant entities in a structured way. * The `` field uses conditional logic to determine the account type: * 1 = deposit * 2 = savings * 3 = payment * 6 = others * For CNPJ extraction, slice:"0:8" retrieves only the first 8 digits (base of the CNPJ). The example below includes the **mother’s name** as a placeholder for a vinculated person. The `` element does not refer to the mother. The example shows the feature in action. * The `date_time` function generates timestamps at runtime, so the report reflects the current execution context. ### Rendered example ``` 12345678 00000001 ACCS005 202502011230 12345678 000000 12345678 1 001 12345-6 1 João Silva 2020-01-01 3 12345678900 Maria Silva 2020-01-01 2025-02-01 ``` ## ACCS010: Relationship transfer file *** ### What it is You generate the ACCS010 with Reporter when institutions transfer client relationships, which is common in portfolio sales or mergers. It lists the transferred relationships and identifies the new responsible institution. ### Full template ``` {# reporter:output-encoding=utf-16be #} 12345678 00000000 ACCS010 {% date_time "YYYYMMdd" %}0001 12345679 {%- 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 ``` 12345678 00000000 ACCS010 202502010001 87654321 12345678 F 12345678900 2019-05-10 1 ``` > [!WARNING] > > 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/products/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 list the accounts from the `midaz_onboarding` data source: ```text theme={null} {% for account in midaz_onboarding.account %}{{ account.name }}{% endfor %} ``` 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. > [!WARNING] > > 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):** onboarding sets up the connection to your Lerian product data. Confirm the data source name and availability with your Lerian contact before you build templates. * **Self-hosted:** your operator can seed the connection at startup with `DATASOURCE__*` variables and owns its network access, credentials, certificates, and database permissions. In every model, report authors reference the configured data source by its `configName`. ## Configure a named data source *** [Create the data source](/en/reference/products/reporter/create-data-source) through the Reporter API, and set its `configName` to the stable name that templates and filters will use. In a self-hosted deployment, you can also seed it at startup with `DATASOURCE__*` environment variables, where `DATASOURCE__CONFIG_NAME` sets that name. 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/products/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/products/reporter/list-data-sources) and confirm that the expected `configName` appears. 2. Copy the data source ID from the response. 3. [Get the data source schema](/en/reference/products/reporter/get-data-source-schema) and confirm that Reporter discovers the expected 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/products/reporter/reporter-quick-start) * [Build templates and filters](/en/products/reporter/using-reporter) * [See Reporter in a complete core banking architecture](/en/products/building-a-complete-core-banking) # Add a Data Source Source: https://docs.lerian.studio/en/products/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/products/reporter/console/editing-a-data-source) and [Test a Data Source Connection](/en/products/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 * the SSL mode that your database requires * a clear connection name, such as `production-postgres` or `compliance-readonly` > [!WARNING] > > Use credentials intended for reporting access. A Data Source gives Reporter access to read data used in Templates and Reports. ## Step by step *** **1. Open Data Sources** Go to **Data Sources** and click **Add Data Source**. **2. Fill in connection details** In **Add Data Source**, enter **Connection Name**, **Database Type**, **Host**, **Port**, **Database Name**, **Username**, and **Password**. **3. Configure schemas when needed** For PostgreSQL, add the schemas that Reporter should inspect. Type a schema name and click **Add**, or press **Enter**. Without schemas, Reporter uses `public`. **4. Select the SSL mode** Select **SSL Mode**. The form starts at **Require**. **5. Add metadata when useful** Expand **Metadata** to add key-value fields that help operators classify the connection. **6. Create the Data Source** Click **Create**. The Console saves the Data Source and tests its connection. **7. Read the test result** When the test succeeds, the dialog closes and a message shows the latency. When it fails, the Data Source stays saved and the dialog shows the reason. Click **Edit connection** to fix it, or **Keep anyway**. When the test cannot run, the dialog closes and a message says that the connection could not be checked. ## Field guide *** | Field | What to enter | Example | | - | - | - | | **Connection Name** | Required operator-facing name. Technical field: `configName`. Up to 100 characters: letters, numbers, `_`, and `-`. You cannot use the name of a Data Source that the platform provides. | `production-postgres` | | **Database Type** | Required database engine: **PostgreSQL** or **MongoDB**. It fills in the default port. 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 PostgreSQL. Entries are trimmed, lowercased, and deduplicated. | `public`, `pix`, `transactions` | | **SSL Mode** | Required. Use **Disable**, **Require**, **Verify CA**, or **Verify Full**. Technical field: `sslMode`. | **Verify CA** | | **Connection Options** | Optional MongoDB URI options, under **Advanced**. Technical field: `options`. | `authSource=admin` | | **Metadata** key/value fields | Optional operator classification data. Each row needs a key, and a key cannot repeat. Technical field: `metadata`. | `environment = production` | ## Expected result *** The new external Data Source appears on the **Data Sources** page with the result of its connection test. After a successful test, operators can use its fields in Templates and Reports. ## Common errors and care points *** > **Using an application URL as Host** > > **Host** should be the database server address, such as `db.example.com`, not the URL of an application or admin panel. > **Skipping schemas** > > If the database uses schemas and the required schema is not listed, operators may not see the expected tables and fields. > **Ignoring SSL requirements** > > If the database requires SSL, the connection can fail until **SSL Mode** matches that requirement. > **Using unclear names** > > 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/products/reporter/console/testing-a-data-source) to test the connection again later. * Use [Template Builder](/en/products/reporter/console/template-builder) to use fields from the Data Source in a Template. * Use [Generate a Report](/en/products/reporter/console/generating-a-report) after the Template and Data Source are ready. # Add a Template Source: https://docs.lerian.studio/en/products/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 set 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/products/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** * you know the expected output format: 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 > [!NOTE] > > A Template is the reusable definition that Reporter applies later during report generation. It is not the generated Report itself. ## Option 1: Build in Template Builder *** Use Template Builder when an operator needs to assemble the Template visually from blocks. **1. Open Template Builder** Go to the **Templates** page and click **Template Builder**. **2. Name the Template** Use a name operators can recognize later, such as `Monthly tax CSV` or `Daily transaction PDF`. Then enter the **Category**, the report family, such as `cadoc`. **3. Choose the output format** Select the format that the generated Report must produce: XML, HTML, CSV, TXT, or PDF. **4. Add the Template blocks** 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. **5. Connect fields to data** 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. **6. Save the Template** 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. **1. Open the upload wizard** Go to the **Templates** page and click **New Template**. **2. Upload the file** In the **File** step, drag the `.tpl` file to **Drag file here**, or click **to select** to browse your computer. **3. Configure the Template** In the **Config** step, fill in **Template Name**, **Output Format**, and **Category**. **4. Review the summary** In the **Review** step, confirm the file, name, output format, and category. Use the edit buttons if any field is wrong. **5. Save the Template** Click **Save Template**. > [!TIP] > > **API equivalent**: [Upload template endpoint](/en/reference/products/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 1 MiB by default. 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` | | **Category** | Required report family. Pick one already in use or type a new one: letters, numbers, `_`, and `-`, up to 100 characters. The Console stores it in lowercase. | `cadoc` | ### 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 *** > **Uploading a file that is not `.tpl`** > > Upload only accepts `.tpl` files. If the Template is not final, use **Template Builder** first or prepare the `.tpl` file outside the Console. > **Choosing the wrong output format** > > The output format controls what kind of file operators will download after generating a Report. Confirm the required format before saving. > **Creating a Template before the Data Source is ready** > > Templates can reference fields from Data Sources. Operators may not know which fields are safe to use when the required source is absent or has no test result. ## Next steps *** * Use [Generate a Report](/en/products/reporter/console/generating-a-report) to create a Report from the Template. * Use [Update a Template](/en/products/reporter/console/updating-a-template) if the structure, file, name, or output format needs correction. * Use [Template Builder](/en/products/reporter/console/template-builder) to build another Template visually. # Deadlines in the Console Source: https://docs.lerian.studio/en/products/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/products/reporter/console/reporter-module), on the **Overview** page. This keeps delivery tracking next to the reports and data sources it relates to. Use this page to understand how reporting deadlines surface visually in the Console. For creating, updating, or delivering deadlines programmatically, see [Managing deadlines](/en/products/reporter/managing-deadlines). > [!WARNING] > > Reporter is only available if you've acquired the Reporter product. [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**, the default landing page, is the home for all deadline tracking. There is no separate "Deadlines" navigation entry. This dashboard holds all delivery tracking. ## Overview widgets *** Two widgets on the Overview page show reporting obligations: * **Upcoming Deadlines**: Lists the reporting reminders with their due dates and current status, so operators can see what comes next. * **Delivery Calendar**: Shows recurring and one-off reporting deadlines by date. ## 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**: messages on the Overview that begin a configured number of days before the due date, so a deadline doesn't slip unnoticed. * **Delivery state**: the **Delivered** check box of each reminder. After the due date passes, a delivered recurring reminder moves to its next due date. ## Manage reminders *** * Click **Add Reminder**. In **New Reminder**, enter **Name**, **Type**, **Due Date**, **Frequency**, and **Notify Days Before** (0 to 30). A **Regulatory** reminder also needs a **Template**. When the tenant has several [institutions](/en/products/reporter/console/managing-institutions), it also needs an **Institution**, or **All institutions**. **Semiannual** and **Annual** reminders need at least one month in **Months of Year**. * Select **Delivered** when you deliver the report. Clear it to revert the delivery. * Use the menu () of a reminder to **Edit**, **Deactivate**, **Activate**, or **Delete** it. An inactive reminder shows no notifications. ## Console vs API *** Use the **Console Overview** when an operator needs to track reminders, follow the delivery calendar, and mark deliveries. Use the **API** when a system needs to manage or deliver deadlines programmatically, for example to sync obligations from another system or automate delivery tracking. > **[Managing deadlines (API)](/en/products/reporter/managing-deadlines)** > > Full request and response reference for creating, listing, updating, deleting, and delivering deadlines. # Delete a Data Source Source: https://docs.lerian.studio/en/products/reporter/console/deleting-a-data-source Remove a Data Source from Reporter when no live Template depends on its database connection. Use this page to remove a Data Source that should no longer be available for future Templates or Reports. ## Objective *** Remove a Data Source connection from Reporter. For example, delete a Data Source when an external reporting database was retired. Delete it when a temporary migration connection is no longer necessary, or when operators have moved to a replacement connection. ## When to use *** Use this task when: * you retired the database connection * no current Template or reporting process should use it * a replacement Data Source exists, if the process continues * the connection exists by mistake and should not remain available You cannot delete a Data Source marked **Provided by the platform** or **Managed by environment**. Do not delete a Data Source just because a connection test failed. If you still need the connection, 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 does not fail only temporarily because of credentials, network, schema, or SSL settings > [!CAUTION] > > Reporter refuses to delete a Data Source that a live Template still references. Update or delete those Templates first. ## Step by step *** **1. Open Data Sources** Go to **Data Sources**. **2. Find the connection** Locate the card for the Data Source you want to delete. **3. Open the delete action** Click the three vertical dots () on the card and select **Delete**. **4. Confirm deletion** In the **Delete data source** dialog, review the connection name and click **Delete**. When Templates still read from the Data Source, the dialog shows **Data source still in use** and lists them instead. **5. Check the list** 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` | | Data source ID | Technical UUID used by the API as the `dataSourceId` path parameter. | `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 data source** | Final confirmation dialog. After you click **Delete**, the Data Source is removed from the active **Data Sources** list. | Confirm the temporary connection removal. | ## Expected result *** The Data Source disappears from the active list. Reporter only completes the deletion after no live Template references the connection. ## Common errors and care points *** > **Deleting a connection still used by Templates** > > Reporter blocks the deletion when a live Template references the connection. Update or delete those Templates first. > **Deleting instead of fixing credentials** > > If your operation still needs the connection, edit credentials or SSL settings and run **Test Connection**. > **Deleting without a replacement** > > 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/products/reporter/console/adding-a-data-source) if you need a replacement connection. * Use [Test a Data Source Connection](/en/products/reporter/console/testing-a-data-source) to validate replacement connections. * Use [Managing Data Sources](/en/products/reporter/console/managing-data-sources) to review the remaining active connections. # Delete a Template Source: https://docs.lerian.studio/en/products/reporter/console/deleting-a-template Remove a Template from Reporter when it is no longer needed for any regulatory report generation. Use this page to remove a Template that you no longer need for future Report generation. ## Objective *** Delete a retired Template from the **Templates** page. For example, delete a Template when a new layout replaces the old one. Delete it when the report obligation no longer exists. Delete a Template created by mistake that 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 is incorrect 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 do not use it for the current reporting cycle. * a replacement Template exists when you still need the report. * you downloaded or archived any generated Reports that need external storage. > [!WARNING] > > 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. Reporter also deletes the reminders linked to the Template. ## Step by step *** **1. Open the Templates page** Go to **Templates**. **2. Find the Template** Locate the Template you want to remove. Check its name carefully if multiple Templates have similar names. **3. Open the delete action** Click the three dots () in the **Actions** column and select **Delete**. **4. Confirm deletion** 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 *** > **Deleting a Template still used by operators** > > You cannot generate future Reports from the deleted Template. Confirm the replacement workflow before deletion. > **Deleting instead of updating** > > Update the Template instead of deleting it if you still need it. Take this path when the layout, output format, or name is wrong. > **Assuming generated Reports disappear** > > Generated Reports remain available on the **Reports** page. Deletion removes the Template from future use. ## Next steps *** * Use [Add a Template](/en/products/reporter/console/adding-template) if you need a replacement Template. * Use [Update a Template](/en/products/reporter/console/updating-a-template) to correct the Template instead of removing it. * Use [Managing Templates](/en/products/reporter/console/managing-templates) to review the active Template list. > **API equivalent** > > [Delete template endpoint](/en/reference/products/reporter/delete-template) # Download a Report Source: https://docs.lerian.studio/en/products/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. You can then submit, archive, review, or share it 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`, or a regulatory filing is `Final` or a `Draft` with every required field filled. * the operator needs the generated file outside the Console. * you must store the file, send it through another process, or check it locally. Do not use download while the Report is still `Processing`. Wait for completion first. ## Before you start *** Confirm: * the Report status is `Finished`, or a regulatory filing is `Final` or a `Draft` with every required field filled. * you chose the intended Template and filters for generation. * the receiving process accepts the output format. * you corrected and regenerated the `Error` Reports. > [!NOTE] > > Use preview to check the Report inside the Console. Download is the action that saves the generated file for use outside the Console. ## Step by step *** **1. Open the Reports page** Go to **Reports**. **2. Find the Report** Locate the Report you need to download. Check its name, Template, generation date, and status. **3. Confirm the status** Make sure the status is `Finished`, `Final`, or `Draft` with every required field filled. **4. Open the action menu** Click the three dots () in the **Actions** column. **5. Download the file** Select **Download Report**. ## Field guide *** | Field or label | What to check | Example | | - | - | - | | Report status | Download after the Report reaches `Finished`. A regulatory filing also offers **Download Report** in `Draft` or `Final`. A `Draft` downloads only when no required field is blank. 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. | `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 *** > **Trying to download a `Processing` Report** > > Wait until the status changes to `Finished`. A Report in `Processing` is not ready yet. > **Downloading a Report generated with the wrong filters** > > Download does not change Report content. If the filters were wrong, generate a new Report with corrected inputs. > **Using preview as the final file** > > 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/products/reporter/console/editing-a-report) if the downloaded file has wrong data, layout, or format. * Use [Generate a Report](/en/products/reporter/console/generating-a-report) to create a corrected file. > **API equivalent** > > [Download report endpoint](/en/reference/products/reporter/download-report) # Edit a Data Source Source: https://docs.lerian.studio/en/products/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. Edit it when the host changes after an infrastructure migration. Edit it when a new schema must be visible, or when SSL settings change. ## When to use *** Use this task when: * credentials changed. * host, port, or database name changed. * schema access changed. * SSL mode or connection options changed. * metadata needs better operator context. You cannot edit a Data Source marked **Provided by the platform** or **Managed by environment**. Delete a retired connection instead of editing it. ## 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**. > [!NOTE] > > A Data Source update affects future Report generation. Reports already generated stay unchanged. ## Step by step *** **1. Open Data Sources** Go to **Data Sources**. **2. Find the connection** Locate the card for the Data Source you need to edit. **3. Open the edit form** Click the three vertical dots () on the card and select **Edit**. **4. Update the fields** In **Edit Data Source**, change the fields that need correction. **5. Save changes** Click **Save Changes**. The Console saves the changes and tests the connection. **6. Read the test result** When the test succeeds, the dialog closes. When it fails, the changes stay saved and the dialog shows the reason. Click **Edit connection** to fix it, or **Keep anyway**. When the test cannot run, the dialog closes and a message says that the connection could not be checked. > [!NOTE] > > The **Password** field appears empty for security reasons. Leaving it blank keeps the current password, unless you change **Database Type**, **Host**, **Port**, **Database Name**, **Username**, **SSL Mode**, or **Connection Options**. Then re-enter the password, even when it did not change. ## Field guide *** | Field | What to change | Example | | - | - | - | | **Connection Name** | Required operator-facing name. Technical field: `configName`. Up to 100 characters: letters, numbers, `_`, and `-`. You cannot rename it while Templates read from it. | `production-postgres-readonly` | | **Database Type** | Database engine used by the connection: **PostgreSQL** or **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 (leave blank to keep current)** | Leave blank to keep the current password. Required when a connection setting changes. | New rotated password | | **Schemas** | Optional schema list for PostgreSQL. Entries are trimmed, lowercased, and deduplicated. | `public`, `pix` | | **SSL Mode** | SSL behavior. Use **Disable**, **Require**, **Verify CA**, or **Verify Full**. | **Verify Full** | | **Connection Options** | MongoDB URI options, under **Advanced**. | `authSource=admin` | | **Metadata** key/value fields | Optional classification data. Each row needs a key, and a key cannot repeat. | `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 unintentionally** > > Leaving **Password** blank keeps the existing password only when no connection setting changes. When one changes, re-enter the password. > **Changing host without testing** > > Host and port changes can point Reporter to a different database. Read the test result that the Console shows after saving. > **Removing a schema that Templates use** > > If a Template or filter depends on fields from a schema, removing that schema can break future generation. > **Editing when the connection should be retired** > > If no reporting process uses the database, delete the Data Source instead of editing it. ## Next steps *** * Use [Test a Data Source Connection](/en/products/reporter/console/testing-a-data-source) to test the connection again later. * Use [Generate a Report](/en/products/reporter/console/generating-a-report) only after the connection is healthy. * Use [Delete a Data Source](/en/products/reporter/console/deleting-a-data-source) when the connection is retired. # Correct a Report Source: https://docs.lerian.studio/en/products/reporter/console/editing-a-report Understand why generated Reports stay unchanged, when a regulatory filing can be corrected, 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. A regulatory filing in `Draft` is the exception: open it and use **Correct a figure** before you **Approve** it. A `Final` filing refuses corrections. ## Objective *** Correct the source setup that produced a wrong Report, then generate a new Report. For example, if a CSV file has 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 Reporter cannot change a generated Report directly. * a generated Report has wrong data, layout, or output format. * an `Error` 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` | Report details, Template, Data Source, filters | > [!NOTE] > > The original Report remains available for audit history. The corrected file becomes a new generated Report. ## Step by step *** **1. Open the Report details** Go to **Reports** and open the generated Report details. **2. Review how it was generated** Check the Template, filters, output format, status, and any error details. **3. Fix the source setup** Update the Template, test or edit the Data Source, or adjust the filters you will use during the next generation. **4. Generate a new Report** Use the corrected Template, Data Source, and filters to generate a new Report. **5. Download the corrected file** 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, 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. Reporter generates a new corrected Report from the fixed setup. ## Common errors and care points *** > **Trying to edit the generated file** > > Reporter keeps generated Reports as historical outputs. Correct the Template, Data Source, or filters, then generate again. > **Fixing the Template when the filter was wrong** > > Missing rows often come from filters, not from the Template. Review the generation inputs before changing the Template. > **Regenerating without testing the Data Source** > > If values are wrong or schema access changed, test the Data Source before generating a corrected Report. ## Next steps *** * Use [Update a Template](/en/products/reporter/console/updating-a-template) when layout or output format is wrong. * Use [Testing a Data Source](/en/products/reporter/console/testing-a-data-source) when values or schema access are suspect. * Use [Generate a Report](/en/products/reporter/console/generating-a-report) after fixing the source setup. > **[Create report](/en/reference/products/reporter/create-report)** > > Generate the corrected Report. > **[Check report status](/en/reference/products/reporter/check-report-status)** > > Monitor the corrected Report before download. # Generate a Report Source: https://docs.lerian.studio/en/products/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 you can monitor, view, and download. ## 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. * the Template declares parameters, such as a reference month. 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. Test the connection when the report depends on one that may have changed. * you know which institution the Report is for, when the tenant has several. See [Managing Institutions](/en/products/reporter/console/managing-institutions). * you know whether the Report should include all data or a filtered subset. * the filter values are precise enough for the reporting period. > [!NOTE] > > Reporter generates Reports asynchronously. A new Report starts in `Processing` and only becomes available for download when it reaches `Finished`. ## Step by step *** **1. Open the Reports page** Go to **Reports** and click **New Report**. **2. Select the Template** In the **Template** step, choose the Template that defines the output structure and format. Click **Next**. **3. Choose the institution, parameters, and filters** In the **Filters** step, complete the fields that apply: * **Institution**: appears when the tenant has several institutions. Choose one institution or **All institutions**. With one institution, the step shows it and uses it. * **Parameters**: appear when the Template declares parameters. Fill in each required parameter. The step fills in the default values that the Template declares. * **Filters**: optional. Add one or more filters to narrow the data. Click **Next**. **4. Review the setup** In the **Review** step, check the Template, the institution, the parameters, and the filters. **5. Generate the Report** Click **Generate Report**. The wizard moves to the **Generate** step and tracks progress while Reporter creates the file. **6. Monitor the status** 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 you download it. ## 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` | | **Institution** | Required when the tenant has several institutions. Choose one institution or **All institutions**. The payload uses this value as `institution`. | `Banco Exemplo S.A.` | | Template parameters | One field for each parameter that the Template declares. The field name is the parameter name. A required parameter needs a value. A month parameter takes `YYYY-MM`, and a date parameter takes `YYYY-MM-DD`. | `2026-05` | | **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 failure` | Some data sections failed. | Open **View Details** to see the failed sections, fix their Data Sources, and generate again. | | `Error` | Reporter found an error during generation. | Review the message, check Data Source health, 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 *** > **Generating with the wrong Template** > > The Template controls layout and output format. If you selected the wrong Template, generate a new Report with the correct Template. > **Using a Data Source that was not tested** > > 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. > **Using broad filters by mistake** > > Empty filters can include all available data. Confirm whether the Report should cover all records or only a specific period, status, or entity. > **Expecting immediate download** > > Download is only available after the Report reaches `Finished`. ## Next steps *** * Use [Download a Report](/en/products/reporter/console/downloading-a-report) when the status is `Finished`. * Use [Correct a Report](/en/products/reporter/console/editing-a-report) if the generated file has wrong data, layout, or format. * Use [Testing a Data Source](/en/products/reporter/console/testing-a-data-source) if generation fails because of connection or schema issues. > **[Create report](/en/reference/products/reporter/create-report)** > > Generate Reports programmatically. > **[Check report status](/en/reference/products/reporter/check-report-status)** > > Poll generation status before download. # Managing Data Sources Source: https://docs.lerian.studio/en/products/reporter/console/managing-data-sources Review, test, and manage the database connections that supply data to Reporter. 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 is a 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. * **Database type**: the database engine used by the connection: PostgreSQL or MongoDB. * **Access configuration**: host, port, database name, credentials, and SSL mode, when the Data Source is an external database connection. * **Schemas**: for PostgreSQL connections, 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 that you prepare? ## Supported databases *** | Database | Default port | Schema support | | - | :-: | :-: | | PostgreSQL | 5432 | ✅ | | MongoDB | 27017 | ❌ | ## 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 absent. Start here when you must add an external database, 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**, **Connecting**, or **Not checked**) and the time of the last check. * Configured schemas (if any). * Latency from the last check. * Host, database, user, and SSL mode. A Data Source that your Lerian products provide shows **Provided by the platform**. You cannot test, edit, or delete it. You can test a Data Source marked **Managed by environment**, but you cannot edit or delete it. You can expand each card to view the database schema, including available tables and columns. ### Available actions From the Data Sources page, you can: > **[Add a Data Source](/en/products/reporter/console/adding-a-data-source)** > > Create a reusable external database connection for Reporter. > **[Edit a Data Source](/en/products/reporter/console/editing-a-data-source)** > > Update credentials, host, schemas, SSL, or metadata. > **[Test connection](/en/products/reporter/console/testing-a-data-source)** > > Confirm Reporter can reach the database and load schema information. > **[Delete a Data Source](/en/products/reporter/console/deleting-a-data-source)** > > Remove retired connections that you no longer use. > [!TIP] > > If a Report fails because data is absent or unreachable, first test the Data Source. Then check whether the expected schema and table appear in the expanded card. # Managing Institutions Source: https://docs.lerian.studio/en/products/reporter/console/managing-institutions Register the institutions your tenant reports for in the Reporter module of the Lerian Console: CNPJ, data scope per Data Source, and proposals read from Midaz. An **institution** is one regulatory declarant of your tenant. Each institution files under its own CNPJ and reads only the rows that its scope names. Manage institutions on the **Institutions** page of Reporter in the [Lerian Console](/en/platform/console/about-lerian-console). Reports, regulatory deadlines, and regulatory filings use the institutions that you register here. See [Where institutions apply](#where-institutions-apply). ## Accessing the Institutions page *** To open the **Institutions** page, click **Institutions** in the left-side menu of the **Reporter** page. The page lists the institutions in a table with these columns: **Name**, **CNPJ**, **Data Sources**, **Last Modified**, and **Actions**. The **Data Sources** column names each Data Source in the scope of the institution. It shows **None** when the scope is empty. The menu () of each row has **Edit** and **Delete**. Each action on the Institutions screen needs the matching `institutions` permission: `get` to see and propose institutions, `post` to create and confirm, `put` to edit, and `delete` to delete. ## Add an institution *** **1. Open the form** On the **Institutions** page, click **New Institution**. **2. Identify the institution** Enter the **Business name** and the **CNPJ**. The CNPJ can be numeric or alphanumeric, with or without punctuation. **3. Define the scope** Click **Add data source** and select a Data Source. Then tell Reporter which of its rows belong to the institution. See [Scope rules](#scope-rules). **4. Save** Click **Save**. If Reporter refuses the institution, the form shows its reason and keeps what you typed. ### Scope rules The scope names the rows of each Data Source that belong to the institution. An institution with no Data Source in its scope reads nothing. For each Data Source, choose one of these: * **The whole data source belongs to this institution**: the institution reads every table of the Data Source. * **Tables**: click **Add table** and enter the table name. For each table, choose one of these: * **Shared: every institution reads this table whole**: use it for reference data that every institution needs. * **Conditions**: click **Add condition**, type the **Field**, select an **Operator**, and type the **Values**, comma separated. A table with conditions needs at least one condition with the **Equal** or the **In** operator. These two operators claim the rows of the institution. ### Field guide | Field | What to enter | Example | | - | - | - | | **Business name** | Required. The name of the declarant, up to 255 characters. | `Banco Exemplo S.A.` | | **CNPJ** | Required. The form checks the check digits. | `12.345.678/0001-95` | | **Data source** | A Data Source registered on the **Data Sources** page. A Data Source appears once in a scope. | `midaz-postgres` | | **Table** | A table of the Data Source. A table appears once in a Data Source. | `account` | | **Field** | The column that identifies the rows of the institution. | `organization_id` | | **Operator** | **Equal**, **In**, **Not In**, **Greater Than**, **Greater or Equal**, **Less Than**, **Less or Equal**, or **Between**. | **Equal** | | **Values** | One or more values, comma separated. | `00000000-0000-0000-0000-000000000000` | ## Propose institutions from Midaz *** Reporter can read the organization table of Midaz and propose one institution for each organization that is not registered yet. **1. Open the proposals panel** On the **Institutions** page, click **Propose from Midaz**. **2. Select the Data Sources** Under **Data sources to read**, select the PostgreSQL Data Sources that read Midaz. The panel lists the PostgreSQL Data Sources that you registered. It does not list Data Sources provided by the platform. **3. Find the proposals** Click **Find proposals**. Each proposal shows the name, the CNPJ, and the proposed scope in each Data Source. **4. Confirm a proposal** Click **Confirm** on a proposal. The **New Institution** form opens with the proposal filled in. Review it, then click **Save**. When you close the form, the proposals panel opens again. > [!WARNING] > > Midaz does not check the CNPJ check digits. A proposal with an incorrect CNPJ shows a warning. Correct the CNPJ in the form before you save. ## Edit an institution *** Click the name of the institution, or select **Edit** in the row menu. Change the fields, then click **Save**. ## Delete an institution *** Select **Delete** in the row menu and confirm. After the deletion, you can register the same CNPJ again at once. ## Where institutions apply *** | Where | What the Console asks | | - | - | | **Reports** > **New Report** | With one institution, the wizard uses it. With several, choose one institution or **All institutions**. With none, the wizard does not ask. | | **Overview** > **Add Reminder**, type **Regulatory** | The same rule as a report. | | **Regulatory Filing** > **Activate document** | With one institution, the dialog uses it. With several, choose the institution that files the document. A filing never offers **All institutions**. Register at least one institution before you activate a document. | When the tenant has several institutions, **Regulatory Filing** also shows these items: * Each subscription shows the institution it files for, or **No institution set**. Set the institution before you generate a draft. * **Activate for another institution** appears on an activated document when an institution does not file it yet. * **Set institution** in the menu of a subscription changes the institution for the next filings. Reports already generated keep their institution. Subscribing to a document and generating, correcting or adopting its filings needs `regulatory:post`. Mapping its fields and setting its institution need `regulatory:put`. Deactivating a subscription needs `regulatory:delete`. Approving a draft needs `regulatory-approval:post`. ## Next steps *** * Use [Generate a Report](/en/products/reporter/console/generating-a-report) to generate a Report for an institution. * Use [Managing Data Sources](/en/products/reporter/console/managing-data-sources) to register the Data Sources that a scope names. > **API equivalent** > > [Create institution endpoint](/en/reference/products/reporter/create-institution) and [Propose institutions endpoint](/en/reference/products/reporter/propose-institutions) # Managing Reports Source: https://docs.lerian.studio/en/products/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. After you upload templates, you can view and manage reports through [Lerian Console](/en/platform/console/about-lerian-console). You can generate reports, filter them, track their status, and download the results. Use the Reports page as the operational queue for generated files. It shows the generated files, whether generation finished, and the next action for the operator. 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 absent, unhealthy, or needs a test. ## Accessing the Reports page *** To open the **Reports** page, click **Reports** from the left-side menu on the **Reporter** page. The page lists reports 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 > **[Generate a Report](/en/products/reporter/console/generating-a-report)** > > Start a new generation from an existing Template. > **[Download a Report](/en/products/reporter/console/downloading-a-report)** > > Save a finished Report for submission or archiving. > **[Correct a report](/en/products/reporter/console/editing-a-report)** > > Learn what to change when a generated Report is wrong. For reports in `Error` or `Partial failure`, the menu () has these actions: * **View Details**: opens a panel with error details. * **Try Again**: opens the generation wizard. ## Filtering reports *** You can filter reports by date using the filter fields above the list. > [!TIP] > > 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: | Status | Definition | | :- | :- | | `Processing` | The system is generating the report. | | `Finished` | The report is ready for viewing and download. | | `Partial failure` | Some data sections failed. You can view the failed sections and try again. | | `Error` | Generation failed. You can view error details and try again. | | `Draft` | A regulatory filing that a person can still correct and approve. | | `Final` | An approved regulatory filing. It is ready for download and cannot change. | If the status is `Error` or `Partial failure`, the Console offers no view or download. 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 `Draft` or `Final` report opens its filing page. Other reports open a preview dialog that shows 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. > [!NOTE] > > For PDF reports, use the embedded viewer controls to navigate pages, zoom, and print. # Managing Templates Source: https://docs.lerian.studio/en/products/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. Manage them from 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, operators often start here. Data Sources for products such as Midaz or CRM may already be configured. The first manual task is to choose, create, or update the Template. ## Accessing the Templates page *** To open the **Templates** page, click **Templates** from the left-side menu on the **Reporter** page. The page lists templates in a table. The columns are **Name**, **Template ID**, **Format** (CSV, XML, HTML, TXT, or PDF), **Last Modified**, and **Actions**. 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 that creates a template from blocks. * **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 absent. ### Available actions menu For each template, the following actions are available in the action menu (): * **Edit**: opens the edit wizard, for every template. The Template Builder creates templates but does not open a saved one. * **Delete**: permanently removes the template and the reminders linked to it. ### Related pages > **[Add a Template](/en/products/reporter/console/adding-template)** > > Create a Template visually or upload a prepared `.tpl` file. > **[Update a Template](/en/products/reporter/console/updating-a-template)** > > Change the Template used for future Reports. > **[Delete a Template](/en/products/reporter/console/deleting-a-template)** > > Remove a Template that you no longer need for operations. > **[Template Builder](/en/products/reporter/console/template-builder)** > > Build a Template with visual blocks instead of editing `.tpl` code. > [!NOTE] > > Updating or deleting a Template does not change Reports already generated. It only changes what Reporter generates next. # Regulatory Filing Source: https://docs.lerian.studio/en/products/reporter/console/regulatory-filing Activate a regulatory document for an institution, map your tables to its fields, then generate, correct, and approve the filing in the Reporter module of the Lerian Console. **Regulatory Filing** is the Reporter page where you file regulatory documents. Reporter keeps the contract of each document: the fields that its file needs. You activate a document, map your tables and columns to its fields, and generate a draft. Then you correct the draft if necessary and approve it. The approved draft becomes the filing. A filing is made for an institution. Register at least one institution on the [Institutions](/en/products/reporter/console/managing-institutions) page before you activate a document. ## Open Regulatory Filing *** In the left-side menu of the **Reporter** page, click **Regulatory Filing**. The page has two panels: * **Document catalog**: the documents that this environment can file. Each card shows the title and the version of the document. The card also tells you if the document includes a reference mapping from Lerian. With a reference mapping, you correct it. Without one, you map the fields from the start. * **Your subscriptions**: the documents that you file. The table shows **Document**, **Adopted version**, **Mapping version**, **Status**, **Readiness**, and **Actions**. **Status** is **Active** or **Inactive**. When the catalog is empty, no regulatory document is mounted in this environment. An administrator mounts the regulatory catalog on the Reporter service. ## Activate a document *** **1. Open the dialog** On a card of the **Document catalog**, click **Activate**. The **Activate document** dialog opens. **2. Choose the institution** With one institution, the dialog uses it. With several, choose the institution that files the document. **3. Choose the version** Leave **Version** empty to pin the newest published version. To pin a different version, type it. **4. Activate** Click **Activate**. The **Document activated** dialog shows what the starting mapping can read now, and what it cannot read. Activation pins the version. To change it later, use **Adopt another version**. See [Manage a subscription](#manage-a-subscription). ## Map the fields *** In the menu () of a subscription, select **Open mapping**. The mapping page has two sections: * **What this mapping reads**: the queries of the mapping. Each query reads one table of one of your Data Sources. To add a query, click **Add query**. * **What the document asks for**: one row for each field that the regulator declares. For each set, choose the query that fills its rows. Then choose where each field is read from. | Where the field is read from | What you enter | | - | - | | **A column of this set query** | The column of the query that fills the set. | | **A column matched onto the published rows** | The column. This choice appears when the document publishes rows for the set. | | **The same value every time** | The value. | | **A value given when the filing is generated** | The parameter of the document that fills the field. | | **Filled by a person on the draft** | A value, or a note that tells what is still missing. Reporter refuses a field with no value and no note. | Click **Save mapping**. If Reporter refuses the mapping, the page shows the reason and keeps your changes. ## Check readiness *** In the **Readiness** column of an active subscription, click **Check now**. Reporter checks the mapping against your databases as they are now. The result opens under the row. | Result | What it means | | - | - | | **Ready** | Each field that the document needs is filled. | | **N required fields are missing** | Nothing fills these fields yet. The result lists them. | | **Will not generate** | The mapping reads a Data Source that nobody registered, a table that your database does not have, or a column that the table does not have. Or the mapping asks for something that Reporter cannot execute. | | **Partly checked** | A table answered, but it did not tell which columns it has. | | **Not checked** | A database did not answer, or no query ran against your databases. | To check again, click **Check again**. ## Generate a draft *** **1. Open the dialog** In the menu () of an active subscription, select **Generate a draft**. **2. Fill in the parameters** The document sets the parameters that the filing needs. Write a competency month as `YYYY-MM` and a date as `YYYY-MM-DD`. Some documents are filed for specific months. The field tells you which months. **3. Generate** Click **Generate**. The **Draft generated** dialog lists the blank fields. A blank field is a field that the mapping could not fill. It is not a generation failure. **4. Open the filing** Click **Open the filing**. The draft is a Report with the status `Draft`. It also shows on the [Reports](/en/products/reporter/console/managing-reports) page. ## Correct and approve the filing *** The filing page shows the status, the blank fields, the **Corrections**, and the **Provenance**. **Provenance** shows the document, the document version, the mapping version, the generation time, the parameters, and the file checksum (SHA-256). While a required field is blank, you cannot approve or download the filing. The page lists each blank field. ### Correct a figure **1. Open the dialog** On the filing page of a `Draft`, click **Correct a figure**. **2. Find the figure** Enter the **Set** and the **Field**. To select a row, click **Add a key** for each key of the row, and enter its **Key** and **Value**. Leave the keys empty for a set with one row. **3. Enter the correction** Enter the **Corrected value**. Under **Why (recorded in the audit trail)**, write why the figure changes. The reason is mandatory. **4. Apply** Click **Apply correction**. Reporter builds the file again from the same rows, so no other figure changes. **Corrections** shows each change, who made it, and the reason. ### Approve the draft **1. Open the dialog** On the filing page of a `Draft`, click **Approve**. The Console hides **Approve** while a required field is blank. **2. Confirm** Click **Approve as the filing**. The draft becomes the filing, with the status `Final`. A `Final` filing refuses corrections, and its provenance does not change. ## Manage a subscription *** The menu () of an active subscription also has these items: * **Set institution**: changes the institution for the next filings. Reports already generated keep their institution. This item appears when the tenant has several institutions. * **Adopt another version**: changes the version that you file. Leave **Version to adopt** empty to take the newest published version. Reporter refuses a version that does not exist. Filings already produced keep their version. After you adopt a version, the dialog shows what the new version does to your mapping. * **Deactivate**: stops the filing of the document. The subscription stays in the list as **Inactive**. Nothing is deleted, and reports already generated from it stay readable. To restore an inactive subscription with its mapping and version, click **Reactivate** on its row. When the document has no active subscription, its catalog card also has **Reactivate**. The card also offers a new subscription. It shows **Start from Lerian's mapping** when the document includes a reference mapping, and **Start from an empty mapping** when it does not. The mapping that you edited stays saved on the deactivated subscription. ## Permissions *** Opening the **Regulatory Filing** page needs `regulatory:get`. Activating or reactivating a document, generating a draft, correcting a figure, and adopting another version need `regulatory:post`. Mapping its fields and setting its institution need `regulatory:put`. Deactivating a subscription needs `regulatory:delete`. Approving a draft needs `regulatory-approval:post`. ## Next steps *** * Use [Managing Institutions](/en/products/reporter/console/managing-institutions) to register the institutions that file documents. * Use [Managing Data Sources](/en/products/reporter/console/managing-data-sources) to register the Data Sources that a mapping reads. * Use [Managing Reports](/en/products/reporter/console/managing-reports) to find a filing among your Reports. # Concepts Map Source: https://docs.lerian.studio/en/products/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 connect to each other, 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. ## 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. For external databases, you add it manually. * The **Report** is the file that Reporter produces when it runs a Template against the connected data. Filters can narrow the result. * 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. | | `Institution` | One regulatory declarant of the tenant, with its CNPJ and the rows of each Data Source that belong to it. | A Report reads for one institution, or for all of them. A regulatory filing is made for one institution. | | `Parameter` | A value that the Template declares and the operator enters at generation time, such as a reference month. | It sets the period or the option of a single Report run. | | `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 failure`, or `Error`, or `Draft` and `Final` for a regulatory filing. | 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 when you click **Generate Report** in the 4-step wizard on the **Reports** page. At that point: 1. Reporter takes the selected Template, the institution, and the parameters. 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. ## When users usually get lost *** > **Template vs Report** > > A Template is a definition. A Report is a generated file. You can generate many Reports from the same Template over time (each with its own filters, run date, and status). > **Data Source vs Filter** > > 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. > **Template Builder vs uploading a `.tpl`** > > 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, and both open in the same edit wizard. > **Why a finished Report is not the same as a delivered Report** > > When a Report reaches `Finished`, Reporter has produced the file and you can download it. Reminders on the Overview page separately track delivery, such as sending the file to a regulator or archiving it. > [!TIP] > > Pair this page with [Setup path](/en/products/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/products/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. This page explains how to operate the Reporter Module from the Console. > [!WARNING] > > Reporter is only available if you've acquired the Reporter product. [Get in touch with our team](https://lerian.studio/contact) to learn more. ## Who uses this page *** Use this page if you produce or manage Reports in the Console. That usually means operations, compliance, finance, regulatory, or support teams. These teams turn stored data into a file for review, submission, or archiving. Developers and integration teams may also use Reporter, but their main reference is the [Reporter API reference](/en/reference/products/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. > **[Setup Path](/en/products/reporter/console/reporter-setup-path)** > > Choose the right path: use existing internal Data Sources, or add and test an external Data Source before generating Reports. > **[Concepts Map](/en/products/reporter/console/reporter-concepts-map)** > > 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. Add external databases from this area. * **Register Institutions**: Record each institution that the tenant reports for, with its CNPJ and the rows that belong to it. * **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. Fill in the parameters that the Template declares, and add optional filters such as date range, status, or transaction type. * **Review generation status**: Track whether a Report is `Processing`, `Finished`, `Partial failure`, or `Error`, and whether a regulatory filing is `Draft` or `Final`. * **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. * **File regulatory documents**: In [Regulatory Filing](/en/products/reporter/console/regulatory-filing), activate a document from the catalog for an institution and map your tables to its fields. Then generate, correct, and approve the filing. ## Operational areas *** The Reporter sidebar follows the common operator workflow: understand the current state, define the output, then generate and download files. Manage Data Sources when you add, test, or change a connection. > **[Overview](/en/products/reporter/console/reporter-module)** > > Monitor the current Reporter workspace: KPIs, delivery reminders, upcoming deadlines, and recent error count. > **[Data Sources](/en/products/reporter/console/managing-data-sources)** > > Review internal connections and configure or test external database connections Reporter uses to fetch data. > **[Templates](/en/products/reporter/console/managing-templates)** > > Create, update, and delete Templates that define the Report layout and output format. > **[Reports](/en/products/reporter/console/managing-reports)** > > Generate Reports, monitor their status, preview finished files, and download the result. > **[Institutions](/en/products/reporter/console/managing-institutions)** > > Register the institutions the tenant reports for, with their CNPJ and data scope. ## 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. ## Main tasks *** > **[Add a Data Source](/en/products/reporter/console/adding-a-data-source)** > > Register an external database connection when the needed Data Source is not already configured. > **[Test a Data Source Connection](/en/products/reporter/console/testing-a-data-source)** > > Confirm that credentials, network access, and SSL settings are correct. > **[Add a Template](/en/products/reporter/console/adding-template)** > > Upload a prepared `.tpl` file or build a Template visually with Template Builder. > **[Generate a Report](/en/products/reporter/console/generating-a-report)** > > Run a Template against your data and produce a downloadable file. > **[Download a Report](/en/products/reporter/console/downloading-a-report)** > > Save the final file from a finished Report generation. > **[Correct a Report](/en/products/reporter/console/editing-a-report)** > > 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. # Setup Path Source: https://docs.lerian.studio/en/products/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. Start with Templates if internal product Data Sources are already configured. If you need an external database, add and test the Data Source first. ## Before you start *** Read the [Reporter concepts map](/en/products/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. * **Institutions** define who a Report is for, and which rows each institution reads. * **Templates** define the structure and output format of the file. * **Reports** are the files that Reporter generates from a Template. You can monitor, view, and download them. 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. It also tells you which Template, output format, and 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 | | Institution details | The CNPJ of each institution that the tenant reports for, and the rows that belong to it. A regulatory filing needs an institution. | Institutions, Reports, Regulatory Filing | | 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 absent, stop there first. Reporter can only generate useful files when you know the Template, the intended output, and the relevant data connection. ## Minimum setup sequence *** **1. Confirm access and choose the first report** Start from the Reporter overview. Confirm that the team knows which report to create first. Use the overview when you need to explain what each Reporter area is for. Go to [Reporter Module](/en/products/reporter/console/reporter-module) and [Reporter concepts map](/en/products/reporter/console/reporter-concepts-map). **2. Confirm the Data Source path** 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 that Data Source. Test it before you build 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. The Console tests the connection when you save it. Use [Adding a Data Source](/en/products/reporter/console/adding-a-data-source) and [Testing a Data Source connection](/en/products/reporter/console/testing-a-data-source). **3. Register the institutions** Register each institution that the tenant reports for, with its CNPJ and its scope. A Report reads for one institution or for all of them. A regulatory filing is made for one institution. Use [Managing Institutions](/en/products/reporter/console/managing-institutions). Use **Propose from Midaz** to start from the organizations that Midaz already holds. **4. Add or build a Template** 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/products/reporter/console/adding-template). If the operator needs to assemble the layout from blocks, use the [Template Builder](/en/products/reporter/console/template-builder). **5. Generate a Report** On the **Reports** page, create a report from the Template. Select the Template. Choose the institution and fill in the parameters when the wizard asks. Apply filters when needed, review the setup, then generate the file. Use [Generating a report](/en/products/reporter/console/generating-a-report). This is the point where a Template becomes a Report. Before generation, it is only a reusable definition. **6. Monitor completion and download the file** Generated reports start in `Processing`. Wait until the status reaches `Finished`, then view or download the output file. If the report ends in `Error` or `Partial failure`, inspect the details. Then run it again after you fix the Data Source, Template, or filter inputs. Use [Downloading a report](/en/products/reporter/console/downloading-a-report) when the file is ready. **7. Track recurring delivery dates when needed** If the report has a recurring deadline, use the **Overview** page to add the delivery reminder. This keeps upcoming and overdue deliveries visible. ## Common sequencing mistakes *** > **Creating a Data Source when one already exists** > > 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. > **Using a Data Source that has never been tested** > > An external connection may exist, but its credentials and network access still need validation when the report depends on that source. Run **Test Connection** before relying on it. > **Uploading a Template without knowing the output format** > > 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. > **Expecting the API reference to replace Console setup** > > Operators can complete the first setup in the Console. Use the API reference when you must automate, integrate, or debug the same workflow with engineering support. ## Related links *** > **[Reporter API reference](/en/reference/products/reporter/reporter-developer-quick-start)** > > Use when you must automate or integrate the Reporter workflow through APIs. # Using the Template Builder Source: https://docs.lerian.studio/en/products/reporter/console/template-builder Build report templates visually by assembling blocks instead of writing .tpl code manually. Use Template Builder to create 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 and a **Loop** for each transaction row. It can also use **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. * 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**. * you tested the Data Source when connection health is uncertain. The **Data Sources** sidebar lists its tables and fields. * you know the expected output format: XML, HTML, CSV, TXT, or PDF. * the operator knows the report structure, such as required header, rows, totals, or conditional sections. > [!NOTE] > > Blocks define the output structure. Filters applied during Report generation define which records enter that structure. ## Step by step *** **1. Open the builder** Go to the **Templates** page and click **Template Builder**. **2. Name the Template** Edit the Template name in the header. Use a name that operators can identify during report generation. Then enter the **Category**, the report family, such as `cadoc`. **3. Choose the output format** Select XML, HTML, CSV, TXT, or PDF. > [!NOTE] > > When you select **PDF**, the generated template code uses HTML format. The code is converted to PDF during report generation. **4. Add blocks to the canvas** Use the blocks toolbar to add structure. Common blocks are **Text**, **Variable**, **Loop**, **Conditional**, and **Aggregation**. **5. Connect fields from Data Sources** Use the Data Sources sidebar to browse schemas, tables, and fields. Click a field to add a **Variable** block for that field. **6. Review the code or the preview when needed** Switch from **Visual** to **Code** to inspect a read-only local approximation of the `.tpl` code. Switch to **Preview** to see the Template rendered by Reporter with placeholder values, not real data. **7. Save the Template** 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` | | **Category** | Required report family in the builder header. | `cadoc` | | **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. | Use while arranging blocks | | **Code** | Read-only local approximation of the `.tpl` code, with **Copy code**. | Review generated `.tpl` | | **Preview** | The Template rendered by Reporter with placeholder values. A PDF Template previews as its HTML stage. | Check where each field lands | | **Download .tpl file** | Downloads the generated `.tpl` without saving a new Template. | `monthly-transaction-csv.tpl` | | **Save** | Validates blocks, generates `.tpl` code, and creates the Template. Requires a name, a category, 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 you save, the Template appears on the **Templates** page. You can select it in the **Generate Report** wizard. You can also download the generated `.tpl` file from the builder. ## Common errors and care points *** > **No Data Sources appear in the sidebar** > > The sidebar only shows configured Data Sources. If the expected internal source does not appear, 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. > **Required block fields are missing** > > The builder validates blocks during save. If a block has no source, field, condition, or expression, fix that block before you save. > **Using filters inside the wrong part of the workflow** > > Template blocks define the file structure. You select report filters later, during report generation. They decide which records enter the output. > **Choosing PDF without understanding the generated code** > > PDF output comes from HTML. Build the Template as HTML-compatible content when the final output format is PDF. ## Next steps *** * Use [Generate a Report](/en/products/reporter/console/generating-a-report) to test the Template with real filters. * Use [Update a Template](/en/products/reporter/console/updating-a-template) to change the name, output format, category, or `.tpl` file later. The builder does not open a saved Template. * Use [Add a Template](/en/products/reporter/console/adding-template) if you need to upload a prepared `.tpl` file instead. > **API equivalent** > > Call [Generate template code endpoint](/en/reference/products/reporter/generate-template-code), then upload the generated `.tpl` file with [Upload template endpoint](/en/reference/products/reporter/upload-template). # Test a Data Source Connection Source: https://docs.lerian.studio/en/products/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. The test does not read the schema. You cannot test a Data Source marked **Provided by the platform**. ## 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: * when a Data Source shows **Not checked** or **Disconnected**. * 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. > [!NOTE] > > 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 *** **1. Open Data Sources** Go to **Data Sources**. **2. Find the connection** Locate the card for the Data Source you need to test. **3. Run the test** Click **Test Connection**. **4. Wait for the result** The status changes to **Connecting** while Reporter tests the connection. **5. Read the result** On success, the card shows **Connected** and the latency. On failure, the card shows **Disconnected** and the Console shows the reason that Reporter returns. **6. Check the schema** Expand the card to see the tables that Reporter can read. When no tables appear, click **Refresh** to read the schema again. ## Field guide *** ### Test action and result fields | Field or status | What it means | Example | | - | - | - | | **Test Connection** | Action on the Data Source card. The test endpoint identifies the selected connection by `dataSourceId`. | 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** (Console display label) | Successful result mapped from the wire value `connected`; the Data Source is usable at that moment. | **Connected** with `Latency: 45ms`. | | **Disconnected** (Console display label) | Failed result. A failed probe maps the wire value `failed` to this label. Check host, port, credentials, network access, and SSL settings. | **Disconnected** after `authentication failed`. | | **Not checked** (Console display label) | Reporter has no test result for the Data Source. | **Not checked** with `Never checked`. | | Latency | `latencyMs`, returned in milliseconds for the probe. 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 available separately | Schema item | What it shows | Example | | - | - | - | | Tables | `tables[].name` returned by the separate schema operation. 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 card shows **Connected**. Expand the card to confirm the tables before using its fields in Template Builder and report filters. ## Common errors and care points *** > **Connection succeeds but no tables appear** > > Check schema names and database permissions. The database user may connect but not have access to the tables needed for reporting. > **Connection fails after editing credentials** > > Reopen the Data Source and confirm username, password, host, port, and SSL settings. > **Testing once and assuming it stays valid** > > Network rules, rotated passwords, and database permissions can change. Re-test when generation errors appear. ## Next steps *** * Use [Template Builder](/en/products/reporter/console/template-builder) after schemas and fields are visible. * Use [Generate a Report](/en/products/reporter/console/generating-a-report) after the Data Source is healthy. * Use [Edit a Data Source](/en/products/reporter/console/editing-a-data-source) if the test fails because settings are wrong. # Update a Template Source: https://docs.lerian.studio/en/products/reporter/console/updating-a-template Update a Template's name, output format, or replace the uploaded .tpl file directly from the Lerian Console, without deleting and recreating 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. Update it when the output format must be XML instead of TXT. Update it when the Template name is too generic for operators to identify safely. ## When to use *** Use this task when: * you still need the Template. * the name or output format is wrong. * an uploaded `.tpl` file needs a replacement. * future Reports should use the corrected Template. Do not use this task to change a Report already generated. Generated Reports are historical files. Correct the source setup and generate a new Report. ## Before you start *** Every Template opens in the same edit wizard. You can update the name, output format, and category, or replace the `.tpl` file. The Template Builder does not open a saved Template. To change the blocks of a Template that you built, build it again and save it as a new Template, or replace its `.tpl` file. Confirm that the change applies to future Reports. Reports already generated from the old Template stay unchanged. > [!WARNING] > > 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 *** **1. Open the Templates page** Go to **Templates** and locate the Template you need to update. **2. Open the edit action** Click the three dots () in the **Actions** column and select **Edit**. **3. Edit the Template fields** In **Edit template**, update the fields that changed: **Template Name**, **Output Format**, **Category**, and **Template File (.tpl)**. If you do not upload a new file, the current file stays. **4. Save the changes** Click **Update Template**. **5. Generate a test Report** Generate a new Report with the updated Template and confirm that the output matches the expected structure. ## 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` | | **Category** | Required report family. | `cadoc` | ## 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 *** > **Expecting old Reports to change** > > Updating a Template does not rewrite Reports already generated. Generate a new Report after correcting the Template. > **Replacing the file without checking output format** > > If the `.tpl` file structure and **Output Format** do not match the expected delivery format, operators can generate the wrong file type. > **Renaming without operational context** > > 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/products/reporter/console/generating-a-report) to test the updated Template. * Use [Correct a Report](/en/products/reporter/console/editing-a-report) if you found the issue in a generated Report. * Use [Delete a Template](/en/products/reporter/console/deleting-a-template) only if you no longer need the Template. > **API equivalent** > > [Update templates endpoint](/en/reference/products/reporter/update-templates) # Prepare trial-balance reporting with Midaz and Reporter Source: https://docs.lerian.studio/en/products/reporter/creating-trial-balance-report Use Midaz accounting entries and Reporter templates to build a reporting workflow suited to your institution. ## Scope of this guide This guide describes a reporting integration pattern. It is not a COSIF mapping specification and does not certify an output for BACEN submission. Define, validate, and approve any regulatory mapping in the controls and processes that own it. ## Use the current Midaz accounting model For new accounting classifications, use the rubric codes in an OperationRoute's `accountingEntries`. Midaz keeps the legacy `OperationRoute.code` field for backward compatibility and marks it deprecated. It is an optional external reference. Midaz does not reserve it for COSIF, does not assign it regulatory meaning, and does not write it to operations. When `accounting.validateRoutes` is enabled on the ledger and a matching rubric resolves, Midaz records the resolved rubric code from `accountingEntries` as `routeCode` on the operation. In other conditions, `routeCode` stays empty. Use `routeCode` when you join report data to classifications. Do not use `code` as the authoritative source for a regulatory mapping. An OperationRoute can hold accounting entries for the transaction-lifecycle actions `direct`, `hold`, `commit`, `cancel`, and `revert`, plus the supplementary keys `overdraft`, `block`, and `unblock`. Each entry can define debit and credit rubrics with a code and description. ## Build the reporting workflow 1. Define your institution's chart of accounts and the classifications required by the report. 2. Configure OperationRoutes and their accounting entries according to those classifications. 3. Configure a Reporter data source and template for the data available in your environment. 4. Generate the report and validate its content through your accounting and regulatory controls. Reporter renders template-driven reports from configured data sources. Its supported output formats are HTML, PDF, CSV, XML, and TXT. The chosen format does not, by itself, make a report compliant with a regulatory submission requirement. ## Related documentation * [OperationRoutes API Reference](/en/reference/products/midaz/v2/create-operation-route) * [Reporter overview](/en/products/reporter/what-is-reporter) * [Reporter template reference](/en/products/reporter/template-reference) # How report generation works Source: https://docs.lerian.studio/en/products/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. ## The path end to end *** **1. Request** You call [Create a report](/en/reference/products/reporter/create-report) with a template identifier and the row filters you want. Reporter validates the request and stores the report as `Processing`. **2. Extraction** 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. **3. Render** The extracted rows become the context for the template. Reporter renders the document in the template's output format. **4. Storage** Reporter writes the rendered file to the configured object storage bucket and records the terminal status of the report. **5. Download** You call [Download a report](/en/reference/products/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 these 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. Reporter rejects a repeat of an in-flight request 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 resolves the parameters and the institution.** Reporter applies the defaults that the template declares for its parameters. It refuses a missing required parameter. It then resolves the institution that the report reads for. A tenant with several institutions must name one, or `all`. **It records the report and queues the work.** The report is stored as `Processing`. The report keeps its own copy of the output format and the template description. That snapshot 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. If the report already reached a terminal status, the worker leaves it as it is and does not generate it 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 that you create through `POST /v1/reports` 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, the request never reached the worker, or the worker stopped sending heartbeats | None | A regulatory filing never ends in `Partial`. It ends in `Draft` or `Error`, and approval turns a `Draft` into `Final`. Poll [Check report status](/en/reference/products/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/products/reporter/download-report) serves the artifact of a `Finished` or `Final` report, and of a `Draft` with no required field blank. ## 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, 20 tables per data source, 50 fields per table, and 256 MiB of extracted result. Each table returns at most 150,000 rows, and each query times out after 300 seconds. See [Environment variables](/en/products/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 starts a report only when an operator turns on the deadline trigger and names the template's category in `DEADLINE_TRIGGER_GENERATE_CATEGORIES`. Otherwise, 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. 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/products/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. > [!NOTE] > > Reporter keeps rendered files in the bucket. Set retention with a lifecycle policy on the bucket itself. ## Next steps *** > **[Reporter template engine](/en/products/reporter/reporter-template-engine)** > > The field map, the data context, and the two kinds of filter. > **[Using Reporter](/en/products/reporter/using-reporter)** > > Upload a template, generate a report, and download the result. > **[Managing deadlines](/en/products/reporter/managing-deadlines)** > > Track a regulatory obligation and mark it delivered. > **[Reporter architecture](/en/products/reporter/reporter-architecture)** > > One binary, two surfaces, and the stores they share. # Managing deadlines Source: https://docs.lerian.studio/en/products/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. Templates define *how* a report looks, and report generation produces the *output*. A deadline records *when* a report is due and its delivery state. Each deadline represents a delivery obligation, typically a regulatory filing or a recurring business report. You can optionally link a deadline to the template that fulfills it. Deadlines sit alongside the [reporting lifecycle](/en/products/reporter/reporter-quick-start#the-reporting-lifecycle) as an operational tracker, so teams can see 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. 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**: each deadline records `deliveredAt` and moves through `pending` → `overdue` → `delivered`. It leaves an auditable history of *when* each obligation was satisfied. ## 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. It uses deadlines to track and meet each filing window. * 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, deadlines show 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 optionally links to the template that fulfills the obligation, watches the due date, and exposes a single status. The status tells the business whether the obligation has been met. Deadlines never submit a report to a regulator. When an operator turns on the deadline trigger, Reporter publishes `deadline.due` on the day a deadline falls due. It also generates the report, but only for the template categories that the operator lists in `DEADLINE_TRIGGER_GENERATE_CATEGORIES`. In every other case, you generate reports through the normal lifecycle, and the deadline records the delivery status. ## 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 a `semiannual` or `annual` deadline falls. Required for those two frequencies and refused for the others. | | `templateId` | Optional UUID of the [template](/en/products/reporter/using-reporter) used to fulfill the deadline. The template must have a category. | | `filters` | Optional row filters, by data source, table, and field, for the report generated for this deadline. A condition can use `period`, such as `previous_month`, which Reporter resolves into a date range when the deadline fires. | | `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`. > [!NOTE] > > Include an `Authorization: Bearer ` header on every deadline request if your environment enables [Access Manager](/en/platform/access-manager). ## Creating a deadline *** Create a deadline with the [Create a Deadline](/en/reference/products/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, "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`. > [!TIP] > > API reference: [Create a Deadline](/en/reference/products/reporter/create-deadline) ## Listing deadlines *** Retrieve deadlines with the [Retrieve Deadlines](/en/reference/products/reporter/retrieve-deadlines) endpoint (`GET /v1/deadlines`). The endpoint paginates results, and you can filter them by status, activity, type, and due-date range. | Query parameter | Description | Default | | - | - | - | | `status` | Filter by `pending`, `overdue`, or `delivered`. | — | | `active` | Filter by active or inactive deadlines. | — | | `type` | Filter by deadline type. | — | | `start_date`, `end_date` | Earliest and latest due date, in `YYYY-MM-DD`, inclusive. | — | | `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. > [!TIP] > > API reference: [Retrieve Deadlines](/en/reference/products/reporter/retrieve-deadlines) ## Updating a deadline *** Update an existing deadline with the [Update a Deadline](/en/reference/products/reporter/update-deadline) endpoint (`PATCH /v1/deadlines/{id}`). The endpoint changes only the fields in the request body, so you can send a partial payload. For example, 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. > [!TIP] > > API reference: [Update a Deadline](/en/reference/products/reporter/update-deadline) ## Deleting a deadline *** Remove a deadline you no longer need to track with the [Delete a Deadline](/en/reference/products/reporter/delete-deadline) endpoint (`DELETE /v1/deadlines/{id}`). A successful request returns `204 No Content`. > [!TIP] > > API reference: [Delete a Deadline](/en/reference/products/reporter/delete-deadline) ## Marking a deadline as delivered *** After you file or send the report behind a deadline, mark the deadline as delivered. Use the [Deliver a Deadline](/en/reference/products/reporter/deliver-deadline) endpoint (`PATCH /v1/deadlines/{id}/deliver`). 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 you marked delivered by mistake. A successful request returns `200 OK` with the updated deadline. > [!NOTE] > > Delivering a deadline only tracks the obligation. It records that the obligation was met. It does not generate or submit the underlying report. Generate the report through the [reporting lifecycle](/en/products/reporter/reporter-quick-start), then mark the deadline delivered to keep your tracker accurate. > [!TIP] > > API reference: [Deliver a Deadline](/en/reference/products/reporter/deliver-deadline) ## How deadlines fit the workflow *** A typical obligation moves through these states: **1. Create the deadline** Register the obligation with its due date, frequency, and optional template. **2. Track its status** It shows as `pending` until the due date, then `overdue` if you do not deliver the report in time. **3. Generate the report** Produce the report through the normal [reporting lifecycle](/en/products/reporter/reporter-quick-start), using the linked template. **4. Mark it delivered** Call the deliver endpoint to set `status` to `delivered` and record `deliveredAt`. ## Next steps *** > **[Getting started with Reporter](/en/products/reporter/reporter-quick-start)** > > Walk through the reporting lifecycle that deadlines track. > **[Using Reporter](/en/products/reporter/using-reporter)** > > Build the templates that fulfill your deadlines. > **[BACEN templates](/en/products/reporter/reporter-bacen-templates)** > > Ready-to-use templates for Brazilian regulatory reporting. > **[Deadlines API](/en/reference/products/reporter/create-deadline)** > > Full request and response reference for every deadline endpoint. # Reporter architecture Source: https://docs.lerian.studio/en/products/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. They share one build, one version, and one release. You choose a topology at deploy time by setting an environment variable. ## 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. Separate deployments let you size each side to its own load. ## The path a report takes *** **1. The API accepts the request** A `POST` to `/v1/reports` names a template and its filters. Reporter takes an idempotency lock first. The lock key is the `X-Idempotency` header when you send one, and a hash of the request body when you do not. Reporter rejects a duplicate that is still in flight. A duplicate of a completed request replays the original report and marks the response as a replay. **2. The API validates and persists** Reporter loads the template's field map and output format. It then checks the filter fields against the live schema of each data source, and writes the report with status `Processing`. **3. The API queues the work** It publishes a command message to the internal RabbitMQ queue and answers `201 Created` with the `Processing` report. The caller has nothing more to do. Rendering has not started. **4. The worker renders** The worker consumes the command. It skips the command if the report already reached a terminal status. It loads the template from object storage and extracts the data for each data source. It renders the document and converts it to PDF when the format calls for it. It uploads the artifact. **5. The worker settles the state** It writes `Finished`, `Partial`, or `Error`, or `Draft` for a regulatory filing, and emits the matching event. The download operation serves the artifact of a `Finished` or `Final` report, and of a `Draft` with no required field blank. ## 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 works as 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 reaches your databases through its embedded extraction engine, with no hand-written queries and no network hop to a separate service. The engine bounds every run. The defaults allow 10 data sources per report, 20 tables per data source, and 50 fields per table. They also allow four data sources extracted in parallel, and a five-minute deadline for the whole extraction. Failures do not end the run. A report whose sections partly succeed renders from what it has. It settles as `Partial` and records which sections failed. ## Stores and artifacts *** | Dependency | Role | | - | - | | MongoDB | Templates, reports, deadlines, institutions, regulatory subscriptions, 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. You register a Midaz database the same way as any other PostgreSQL database. 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. > [!NOTE] > > See [Connecting Reporter to Midaz](/en/products/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. The `/health` probe answers 503 until the startup self-probe succeeds. This lets an orchestrator restart a pod that could not reach its dependencies at boot. The `/readyz` probe reports the dependencies that surface actually uses, and its response states which deployment mode the process runs 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 *** > **[Core concepts](/en/products/reporter/reporter-core-concepts)** > > Templates, data sources, reports, deadlines, and the report states. > **[Environment variables](/en/products/reporter/reporter-environment-variables)** > > Every setting that shapes a Reporter deployment. # BACEN regulatory formats Source: https://docs.lerian.studio/en/products/reporter/reporter-bacen-templates Use Reporter templates as a rendering tool and validate every regulatory mapping with the applicable authoritative contract. Reporter renders templates from configured data sources. Subscription-driven regulatory reports validate extracted values against the pinned contract and produce a `Draft`. Approval makes it `Final`. These checks do not certify a filing or establish a submission schedule. Use the guides below only after you have verified the relevant layout, field semantics, aggregation, identifiers, and validation rules against the applicable authoritative source. ## Available guidance *** > **[CADOC 4010 and 4016](/en/products/reporter/cadoc-4010-and-4016)** > > Validation status and next steps. > **[CADOC 4111](/en/products/reporter/cadoc-4111)** > > Validation status and next steps. > **[CCS Report](/en/products/reporter/ccs-reports)** > > Review the applicable source contract before use. > [!WARNING] > > Do not treat a Reporter template as regulatory evidence. Validate the generated file with the responsible compliance team and the authoritative validator before operational use. # Reporter core concepts Source: https://docs.lerian.studio/en/products/reporter/reporter-core-concepts The Reporter model in one place: templates, data sources, reports, deadlines, institutions, regulatory filings, the 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 persisted, read-only database connection that an operator manages through the API or seeds at startup. | | 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. | | Institution | One regulated entity with one CNPJ, and the rows of each data source that belong to it. | | Regulatory filing | A report of a curated regulatory document that a person corrects and approves. | | 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, a short description, and its category. The category is the report family that a consumer routes by. 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/products/reporter/template-reference) for the tags, filters, and expressions the language offers. ## Data sources *** A data source is a PostgreSQL or MongoDB connection stored in Reporter's registry. The API creates and manages registry entries. A self-hosted deployment can also seed them from `DATASOURCE_*` variables at startup. Data extraction remains read-only by construction: Reporter issues `SELECT` statements and MongoDB finds, and it has no write path into the source database. Each entry has two identifiers. The `dataSourceId` is the UUID that registry operations use. The `configName` is the stable name that templates and filters use, so keep it stable while hosts and credentials change. Reporter encrypts passwords at rest with `DATASOURCE_CRED_ENC_KEY` and never returns them. The registry exposes seven operations: list, create, get, partially update, soft-delete, inspect the live schema, and test the connection. Reporter refuses deletion while a live template still references the source. Read [Connecting Reporter to Midaz](/en/products/reporter/connecting-reporter-to-midaz) for a worked configuration. ## Reports *** A report request names a template and the filters that narrow the data behind it. Send an empty `filters` object when you need none. Filters nest three levels (data source, then table, then field): ```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 at creation time. That snapshot keeps an artifact 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. ### Report states Creation is synchronous only until the work reaches the queue. The `POST` operation 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, the request never reached the worker, or the worker stopped sending heartbeats. | | `Draft` | A regulatory filing is generated and waits for review. | | `Final` | A person approved the regulatory draft. | `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. The worker skips a report that already reached a terminal status, and does not generate it again. The download operation serves the artifact of a `Finished` or `Final` report, and of a `Draft` with no required field blank. 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. Reporter derives status and never stores it. 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. It rolls forward only when it carries the delivery mark. 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/products/reporter/managing-deadlines) for the full lifecycle. ## Institutions and regulatory filings *** An institution is one regulated entity of a tenant, identified by its CNPJ. Its scope names the rows that belong to it in each data source. A report reads for one institution, or for `all`. A regulatory filing starts from a curated document that the deployment carries. The tenant subscribes to the document and maps its fields to tables in a YAML mapping. Each generation produces a `Draft`. A person corrects the draft field by field, and a person with the approval permission turns it `Final`. The filing keeps a provenance record of the document version, the mapping, the parameters, and the SHA-256 of the file. See [Reporter REST API](/en/products/reporter/reporter-rest-api#regulatory-filing) for the operations. ## 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. Reporter rejects a request when it cannot resolve the tenant, and nothing falls back to a shared pool. A self-hosted deployment needs no tenant configuration at all. ## Next steps *** > **[Architecture](/en/products/reporter/reporter-architecture)** > > One binary, two surfaces, and the queue between them. > **[Quick start](/en/products/reporter/reporter-quick-start)** > > Upload a template and generate your first report. > **[Template reference](/en/products/reporter/template-reference)** > > The tags, filters, and expressions available to a template. > **[Managing deadlines](/en/products/reporter/managing-deadlines)** > > Create, track, and deliver a reporting obligation. # Reporter deployment Source: https://docs.lerian.studio/en/products/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, and `RUN_MODE` selects which surfaces a process serves. 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, data sources, institutions, and regulatory filing. 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. `MONGO_HOST` and `MONGO_NAME` are required at startup when `MULTI_TENANT_ENABLED` is `false`, the default. ## 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 plus its broker connection: `RABBITMQ_HOST`, `RABBITMQ_PORT_AMQP`, `RABBITMQ_DEFAULT_USER`, and `RABBITMQ_DEFAULT_PASS`. The worker consumes from the queue and needs that same connection plus `RABBITMQ_GENERATE_REPORT_QUEUE`. The broker objects must exist before either service starts. 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.*`, `regulatory.*`) go to an exchange you name in `RABBITMQ_REPORT_EVENTS_EXCHANGE`. The value is operator-configured. The reference value is `reporter.events`. 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//.` 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 report files and regulatory filings forever. It has no delete endpoint and no retention setting, so the bucket grows with your report volume. The only way a file expires is an object-storage lifecycle rule that you configure. **Such a rule also deletes approved regulatory filings, which are the institution's regulatory record.** The saved rows that a regulatory correction re-renders the draft from are also stored under `reports/`, so an expiry rule can make an older draft impossible to correct. > [!WARNING] > > Keep `templates/` out of scope: 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. Redis backs the idempotency lock on report creation and the datasource schema cache. Idempotency state lives in Redis, not in process memory, so API replicas share it. ## Data sources *** Reporter reads reporting data from PostgreSQL and MongoDB data sources in its persisted registry. Create and manage them through the [data-source API](/en/reference/products/reporter/list-data-sources). An optional `DATASOURCE_{NAME}_*` block seeds a user-managed registry entry at Manager startup. It seeds the entry only when that `configName` is absent. Later API edits and deletions take precedence over the environment. Both Manager and worker require the same persistent `DATASOURCE_CRED_ENC_KEY` to protect the registry credentials. Keep it unchanged across restarts and deployments so they can continue decrypting stored credentials. 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 extraction limits. They are 10 data sources, 20 tables per data source, and 50 fields per table. They also cover 4 data sources read at a time, a 300-second deadline, and a 256 MiB cap on extracted data. The `ENGINE_*` variables change them. Each table also returns at most 150,000 rows and each query times out after 300 seconds, unless the data source sets its own values. ## 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. > [!WARNING] > > **SaaS mode requires TLS on every dependency.** Set `DEPLOYMENT_MODE=saas` and a plaintext MongoDB, RabbitMQ, Redis, or object storage 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`, any required RabbitMQ connection or command-queue variable, or `DATASOURCE_CRED_ENC_KEY` missing on the API surface | Config validation fails and lists each missing variable. | | Any of `RABBITMQ_HOST`, `RABBITMQ_PORT_AMQP`, `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS`, `RABBITMQ_GENERATE_REPORT_QUEUE`, or `DATASOURCE_CRED_ENC_KEY` missing on the worker surface | Config validation fails and lists each missing variable. | | `MONGO_HOST` or `MONGO_NAME` missing while `MULTI_TENANT_ENABLED` is `false` | Config validation names them. | | `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. | | `WORKER_HEARTBEAT_STALE_SEC` below three times `WORKER_HEARTBEAT_INTERVAL_SEC`, an unknown `REPORT_TIMEZONE`, or a `REGULATORY_CATALOG_DIR` whose catalog does not load | Startup aborts. | > [!NOTE] > > **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. The `/readyz` probe answers 503 from the moment the signal arrives, before the servers begin shutdown. 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 *** > **[Environment variables](/en/products/reporter/reporter-environment-variables)** > > Every Reporter variable, by category. > **[BYOC configuration](/en/reference/byoc-configuration)** > > The configuration blocks every Lerian product shares. > **[Health and readiness](/en/reference/health-and-readiness)** > > The probe contract and what each response means. > **[Connect Reporter to Midaz](/en/products/reporter/connecting-reporter-to-midaz)** > > Point Reporter at a Midaz database and render your first report. # Environment variables Source: https://docs.lerian.studio/en/products/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, and `RUN_MODE` selects its active surfaces: 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, 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 | | `ENV_NAME` | Environment name. With `production`, the Manager requires telemetry, Access Manager authentication, explicit HTTPS CORS origins, and real passwords at startup. It also requires TLS to Redis, RabbitMQ, and object storage unless `ALLOW_INSECURE_TLS` is `true`. With `local`, Reporter reads a `.env` file. | `local` | 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 | ## Template uploads | Variable | Description | Default | Required | | - | - | - | - | | `MAX_TEMPLATE_FILE_BYTES` | Largest template file, in bytes, that an upload accepts. A larger file is rejected with `RPT-0078`. The server refuses any request body over 4 MiB, whatever this value. | `1048576` (1 MiB) | No | ## Template previews | Variable | Description | Default | Required | | - | - | - | - | | `PREVIEW_MAX_CONCURRENT` | Maximum synchronous template previews the API manager renders at once. When all slots are in use, Reporter rejects a new preview immediately with HTTP `429` and `RPT-0108`; it does not queue it. The value caps concurrent renders and does not cap the request rate. Values below `1` use the default. | `4` | No | ## Report calendar | Variable | Description | Default | Required | | - | - | - | - | | `REPORT_TIMEZONE` | IANA time zone that measures the start and end of every day in report date filters. Set the zone your books close in, such as `America/Sao_Paulo`. Manager and worker must carry the same value. An unknown name stops the startup. The value applies to every tenant of the deployment. | `UTC` | No | ## Database (MongoDB) Stores report metadata, templates, and run history. | Variable | Description | Default | Required | | - | - | - | - | | `MONGO_URI` | Connection scheme: `mongodb` or `mongodb+srv` | — | Yes | | `MONGO_HOST` | MongoDB host | — | Yes (`MULTI_TENANT_ENABLED=false`) | | `MONGO_PORT` | MongoDB port | — | Yes | | `MONGO_USER` | Database user | — | Yes | | `MONGO_PASSWORD` | Database password. Sensitive. | — | Yes | | `MONGO_NAME` | Database name | — | Yes (`MULTI_TENANT_ENABLED=false`) | | `MONGO_PARAMETERS` | Extra MongoDB URI options, such as `authSource=admin` | — | No | | `MONGO_MAX_POOL_SIZE` | Maximum connection pool size | `100` | 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. | — | Yes | | `RABBITMQ_DEFAULT_USER` | Broker user | — | Yes | | `RABBITMQ_DEFAULT_PASS` | Broker password. Sensitive. | — | Yes | | `RABBITMQ_EXCHANGE` | Exchange where the API publishes report-generation commands | — | Yes (`api`/`all`) | | `RABBITMQ_GENERATE_REPORT_QUEUE` | Queue that carries report-generation commands from the API to the worker | — | Yes (`api`/`worker`/`all`) | | `RABBITMQ_GENERATE_REPORT_KEY` | Routing key the API uses for report-generation commands | — | Yes (`api`/`all`) | | `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. Leave empty for AWS S3. | — | No | | `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. Leave it and the secret key empty to use the AWS default credential chain. Sensitive. | — | No | | `OBJECT_STORAGE_SECRET_KEY` | Secret access key. Sensitive. | — | No | | `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 | `3` | 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 | | `PDF_PAPER_SIZE` | Page size: `A4` or `Letter` | `A4` | No | | `PDF_DISPLAY_HEADER_FOOTER` | Print a header and a footer on each PDF page | `true` | No | ## Extraction limits (worker) | Variable | Description | Default | Required | | - | - | - | - | | `ENGINE_MAX_DATASOURCES` | Data sources one report can read | `10` | No | | `ENGINE_MAX_TABLES_PER_DATASOURCE` | Tables or collections per data source | `20` | No | | `ENGINE_MAX_FIELDS_PER_TABLE` | Fields per table | `50` | No | | `ENGINE_MAX_CONCURRENCY` | Data sources extracted in parallel | `4` | No | | `ENGINE_TIMEOUT_SEC` | Timeout of one extraction (seconds) | `300` | No | | `ENGINE_MAX_RESULT_BYTES` | Largest extraction result. A larger result fails the extraction. | 256 MiB | No | | `DATASOURCE_DEFAULT_ROW_CEILING` | Rows one table can return, for a data source without its own ceiling. A table that exceeds it fails with `RPT-0161`. | `150000` | No | | `DATASOURCE_DEFAULT_QUERY_TIMEOUT_SEC` | Timeout of one query (seconds), for a data source without its own value. A query that exceeds it fails with `RPT-0162`. | `300` | No | | `ENGINE_MAX_DYNAMIC_POOLS` | Data-source connection pools the worker keeps open, across every tenant | `500` | No | | `ENGINE_POOL_IDLE_TIMEOUT_SEC` | Idle time before a data-source pool closes (seconds) | `7200` | No | ## Stale-report reaper A worker stamps a heartbeat on each report it generates. The Manager moves a `Processing` report whose heartbeat is stale to `Error` with `RPT-0160`. Manager and worker must carry the same heartbeat values. | Variable | Description | Default | Required | | - | - | - | - | | `REPORT_REAPER_ENABLED` | Run the reaper on the Manager | `true` | No | | `REPORT_REAPER_INTERVAL_SEC` | Seconds between reaper passes | `60` | No | | `REPORT_REAPER_BATCH` | Reports one tenant pass ends | `200` | No | | `WORKER_HEARTBEAT_INTERVAL_SEC` | Seconds between worker heartbeats | `30` | No | | `WORKER_HEARTBEAT_STALE_SEC` | Age after which a heartbeat is stale (seconds) | `180` | No | ## Deadline trigger (Manager) | Variable | Description | Default | Required | | - | - | - | - | | `DEADLINE_TRIGGER_ENABLED` | Run the sweep that publishes `deadline.due` when a deadline's day arrives | `false` | No | | `DEADLINE_TRIGGER_INTERVAL_SEC` | Seconds between sweeps | `300` | No | | `DEADLINE_TRIGGER_GENERATE_CATEGORIES` | Comma-separated template categories for which the sweep also generates the report. Empty generates none. | — | No | | `DEADLINE_TRIGGER_GENERATION_TIMEOUT_SEC` | Timeout of one scheduled generation (seconds) | `20` | No | | `DEADLINE_TRIGGER_MAX_CATCH_UP_OCCURRENCES` | Missed occurrences of one deadline that one sweep generates | `7` | No | ## Regulatory catalog Without a catalog source, the deployment carries no regulatory documents. | Variable | Description | Default | Required | | - | - | - | - | | `REGULATORY_CATALOG_DIR` | Directory where the curated catalog is mounted. Reporter reads it at startup, and a catalog that does not load stops the startup. | — | No | | `REGULATORY_CATALOG_GITHUB_REPO` | `owner/repo` that publishes the catalog releases. When set, subscriptions fetch from it instead of the directory. | — | No | | `REGULATORY_CATALOG_GITHUB_TOKEN` | Token for a private catalog repository. Sensitive. | — | No | | `REGULATORY_CATALOG_GITHUB_URL` | Alternative API root, such as a proxy | — | No | ## Report datasources Reports read from PostgreSQL and MongoDB datasources in the persisted registry. Create and manage ordinary entries through the [data-source API](/en/reference/products/reporter/list-data-sources). The environment block below is an optional bootstrap path. `DATASOURCE_{NAME}_CONFIG_NAME` makes an environment seed block exist. Reporter scans 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. At Manager startup, a complete block seeds its `configName` only when no registry entry, including a soft-deleted one, already uses that name. 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_CRED_ENC_KEY` | Persistent hex-encoded AES key shared by Manager and worker to encrypt and decrypt registry credentials. Keep it unchanged across restarts and deployments; changing it prevents them from decrypting credentials already stored in the registry. Generate it with `openssl rand -hex 32`. | — | Yes | | `DATASOURCE_{CONFIG_NAME}_SCHEMAS` | Schemas to expose from the datasource (CSV). This key uses the uppercased `CONFIG_NAME` value, unlike the other keys in the block. | `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}_SSLCA_BASE64` | Standard Base64 of a PEM CA bundle, on one line. An alternative to the certificate file path. For PostgreSQL, set `SSLMODE` to `verify-ca` or `verify-full`, or leave it empty for `verify-full`. | — | No | | `DATASOURCE_{NAME}_OPTIONS` | Extra MongoDB URI options | — | No | | `DATASOURCE_{NAME}_ROW_CEILING` | Rows one table of this data source can return | `DATASOURCE_DEFAULT_ROW_CEILING` | No | | `DATASOURCE_{NAME}_QUERY_TIMEOUT_SEC` | Timeout of one query on this data source (seconds) | `DATASOURCE_DEFAULT_QUERY_TIMEOUT_SEC` | No | | `DATASOURCE_CRM_MIDAZ_ORGANIZATION_ID` | Midaz organization ID that scopes the reserved CRM datasource. Required when `DATASOURCE_CRM_CONFIG_NAME=plugin_crm`. | — | No | | `CRYPTO_HASH_SECRET_KEY_CRM` | Existing CRM hashing key used to read CRM datasource PII. Sensitive; required when CRM fields are read. | — | No | | `CRYPTO_ENCRYPT_SECRET_KEY_CRM` | Existing CRM encryption key used to read CRM datasource PII. Sensitive; required when CRM fields are read. | — | No | A complete per-datasource seed block follows. Set the global `DATASOURCE_CRED_ENC_KEY` separately as described above. We recommend using the same name for `CONFIG_NAME` and `{NAME}`, with the environment-variable segment uppercased (for example, `ONBOARDING` for `CONFIG_NAME=onboarding`). That keeps the schema key intuitive, because `SCHEMAS` uses the `CONFIG_NAME` value as its key while every other field uses `{NAME}`: ```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 }}`. Use the API to add or update an ordinary datasource. An environment block only seeds a previously absent entry. It never overwrites an API-managed entry or restores a soft-deleted one. ### Reserved CRM datasource `plugin_crm` is the reserved, environment-managed CRM datasource. Do not create it or PATCH it through the ordinary data-source API: Reporter rejects that path with `RPT-0073`. A CRM template must use the exact config name `plugin_crm` and a complete MongoDB block: ```bash theme={null} DATASOURCE_CRM_CONFIG_NAME=plugin_crm DATASOURCE_CRM_TYPE=mongodb DATASOURCE_CRM_HOST= DATASOURCE_CRM_PORT= DATASOURCE_CRM_DATABASE= DATASOURCE_CRM_USER= DATASOURCE_CRM_PASSWORD= DATASOURCE_CRM_MIDAZ_ORGANIZATION_ID= ``` Put `DATASOURCE_CRM_PASSWORD`, `CRYPTO_HASH_SECRET_KEY_CRM`, and `CRYPTO_ENCRYPT_SECRET_KEY_CRM` in your secret store. The CRM crypto values must be the same keys already used by CRM. Do not generate replacement keys for Reporter. Keep `DATASOURCE_CRED_ENC_KEY` stable as well: changing it prevents Reporter from decrypting stored datasource credentials. Manager uses this block at startup to seed `plugin_crm` and to fill an absent or empty `metadata.midazOrganizationId`. A non-empty persisted organization ID wins and is not overwritten by the environment. If it is wrong, stop and use the supported engineering recovery path. Do not use the ordinary API to change it. To set these variables with the Reporter chart, see the [Reporter chart README](https://github.com/LerianStudio/helm/tree/main/charts/reporter). For templates that use `plugin_crm`, validate the full path: import or save the template, generate a report, confirm CRM fields decrypt, and confirm its records are scoped to the intended Midaz organization. ## Configuration shared across products The following blocks are identical across Lerian products. The [BYOC configuration reference](/en/reference/byoc-configuration) documents them in full. They default off. * **Access Manager authentication**: `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_ADDRESS`. Enable in production. * **Service discovery**: `SD_*` (Consul, and 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 and `STREAMING_KAFKA_BROKERS` for the optional Kafka copy of `report.finished` and `report.partial`. Off by default. * **OpenTelemetry**: `ENABLE_TELEMETRY`, `OTEL_*`, `OTEL_INSECURE_EXPORTER`. Telemetry is OTLP push. # Reporter events Source: https://docs.lerian.studio/en/products/reporter/reporter-events Subscribe to Reporter's report, deadline, and regulatory filing events: the streaming manifest operation, the CloudEvents envelope, delivery policies, and what each payload carries. Reporter publishes a business event whenever a template, a report, a deadline, or a regulatory filing 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 *** [Streaming events](/en/reference/products/reporter/get-streaming-events) returns the static event catalog. It requires authentication like every other operation. It answers `Cache-Control: no-store`, and it responds whether or not this deployment publishes events. Read it at startup to check that your consumer and Reporter agree on the contract. The response exposes the application identity and catalog: | Field | What it carries | | - | - | | `version` | Semantic version of the manifest wire format. | | `publisher` | Who publishes: `serviceName` and `source` are `reporter`, `routePath` is this operation, and `outboxSupported` is `true`, plus the application and library versions. | | `events` | One entry per event definition, with its key, `eventKey`, resource type, event type, `class`, schema version, description, and default delivery policy. Reporter emits only `fact` events. | | `routes` | Omitted. Reporter does not publish broker topology through the API. | ## Broker routing *** The manifest does not disclose a topic or broker topology. Bind your queue to the exchange named by `RABBITMQ_REPORT_EVENTS_EXCHANGE`. Each message uses the event definition key verbatim as its AMQP routing key: `report.finished`, `deadline.delivery_reverted`, underscores included. When `STREAMING_KAFKA_BROKERS` is set, `report.finished` and `report.partial` also publish to the Kafka topic `lerian.streaming.reporter`, in addition to RabbitMQ. ## The event catalog *** Seventeen event definitions exist across the two run modes. | Event | Emitted when | Delivery profile | | - | - | - | | `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 created through `POST /v1/reports` or by the deadline trigger is stored as `Processing`, before Reporter queues its generation. | 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 | | `deadline.due` | The deadline trigger announces an occurrence whose day arrived. | Critical | | `deadline.generation_failed` | The trigger announced an occurrence, and Reporter refused the report it had to generate. | Critical | | `regulatory.draft_ready` | A regulatory draft and its file are stored and ready for review. | Critical | | `regulatory.filing_approved` | A person approved a regulatory draft, and it is `Final`. | Critical | | `regulatory.version_adopted` | A subscription moved to another version of its regulatory document. | Critical | This channel is publish-only. Reporter emits these events and consumes none of them. ## Delivery *** Every manifest event has class `fact`. The delivery profile in the table above selects its delivery policy. | Delivery profile | Direct publish | Outbox | Route DLQ | | - | - | - | - | | Important | Yes | Falls back to the outbox when the circuit opens | Not configured | | Critical | No | Always | Not configured | An event with the `Critical` delivery profile therefore never publishes straight to the broker. Reporter writes it to the durable Mongo streaming outbox after the business-state commit, and a dispatcher replays it after a broker outage. The state write and outbox insert are separate operations, not one atomic application transaction. That gap is a failure window: a crash or a failed outbox insert after the state commit loses the event. No reconciliation recovers it. The loss leaves only an error log and a metric. Once the row is in the outbox, the dispatcher retries until delivery. The policy requests dead-letter handling for routable failures, but the current RabbitMQ routes do not provide an explicit DLQ destination. Such a failure is surfaced and logged, with no forensic dead-letter copy. Emission happens after the commit and never fails the work. A publication problem does not turn a stored report into an errored one. > [!WARNING] > > Delivery is at-least-once. Deduplicate on `(ce-source, ce-id)`. Template events, `report.requested`, and deadline create, update, and delete events carry a generated identifier. The other events use deterministic identifiers. ## 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` | Opaque producer-defined id; deterministic only for the event classes described above | | `ce-source` | `reporter` | | `ce-type` | `studio.lerian.reporter..`, for example `studio.lerian.reporter.report.finished` | | `ce-time` | RFC 3339 emission timestamp | | `ce-subject` | The identifier of the resource that the event is about | | `ce-resourcetype` | `report`, `template`, `deadline`, or `regulatory` | | `ce-eventtype` | `finished`, `created`, `delivery_reverted`, and so on | | `ce-schemaversion` | `2.0.0` on `report.finished`, `report.partial`, and `report.errored`. `1.0.0` on the other events. | | `ce-tenantid` | The tenant that owns the change | Reporter marks messages persistent and always stamps a tenant value. ## 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": "org-01abc/reports/8f2c1a9e-4b30-4c02-9a1b-2d5e6f7a1c33/019826f4-6a9c-7b31-9d40-2f1e8c5a4b77.pdf", "artifact_content_type": "application/pdf", "document_type": "cadoc", "completed_at": "2026-07-29T14:22:08Z", "duration_ms": 8421, "section_count": 3 } ``` `artifact_object_key` is the exact object key reported by storage after the write, not a path that consumers should reconstruct. Use the value verbatim. If you validate its tenant segment, compare it with the tenant bound to your authenticated subscription, not with another field from the same message. An empty key on `report.finished` or `report.partial` is a contract violation. [Download a report](/en/reference/products/reporter/download-report) remains the supported way to fetch a report in the `Finished` state. `report.partial` adds `section_failures` and `failed_section_count` alongside the same artifact fields. A resolvable key does not mean that the report is complete. Deliver a template report only from `report.finished`, and a regulatory filing only from `regulatory.filing_approved`. Strictly decode the `status` field instead of using key presence as the discriminator. `regulatory.filing_approved` names the approved file in `artifact_object_key` and carries `artifact_sha256`, `artifact_size_bytes`, `delivery_attempt`, and `approved_by`. `document_type` carries the category of the template. `report.errored` replaces the artifact fields with `error_code` and `error_summary`, and adds `document_id` and `document_version`, which name the regulatory document when a subscription generated the report. Its lack of an artifact field does not prove that no object was written: storage can succeed before terminal status persistence fails, leaving an object that no event names. Both error fields come from a fixed vocabulary: `report_generation_failed`, `report_generation_timeout`, `report_generation_canceled`, or `report_worker_lost`. Each code has 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` | Must be exactly `reporter`; startup rejects any other value. | When publication is off, `STREAMING_CLOUDEVENTS_SOURCE` may be unset. If it is set, it must still be exactly `reporter`. Reporter rejects another non-empty value at startup even while publication is off. When publication is on, Reporter also refuses to start if any of the three settings is blank, so a misconfigured deployment fails at startup instead of dropping events quietly. ## Next steps *** > **[Reporter REST API](/en/products/reporter/reporter-rest-api)** > > The 56 operations, authentication, pagination, and errors. > **[API Reference](/en/reference/introduction)** > > The streaming manifest operation, with its full response shape. > **[Environment variables](/en/products/reporter/reporter-environment-variables)** > > Every setting behind the streaming, exchange, and run-mode surfaces. > **[What is Reporter?](/en/products/reporter/what-is-reporter)** > > Templates, reports, deadlines, and where they fit. # HTML template Source: https://docs.lerian.studio/en/products/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.

Generation Date: {% date_time "dd/MM/YYYY HH:mm" %}

Organization: {{ midaz_onboarding.organization.0.legal_name }}

Ledger: {{ midaz_onboarding.ledger.0.name }}

{% for account in midaz_onboarding.account %} {% with balance = filter(midaz_transaction.balance, "account_id", account.id)[0] %}
Account: {{ account.alias }}

ID: {{ account.id }}

Currency: {{ balance.asset_code }}

Current Balance: {{ balance.available }}

{% 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 %} {% endif %} {% endfor %}
Operation ID Type Original Amount Discount (3%) Final Amount Description
{{ operation.id }} {{ operation.type }} {{ original_amount|decimal:2 }} {{ discount_amount|decimal:2 }} {{ final_amount|decimal:2 }} {{ operation.description }}
{% endwith %} {% endfor %}
``` ## Code breakdown *** ### Organization and Ledger information ```html HTML theme={null}

Generation Date: {% date_time "dd/MM/YYYY HH:mm" %}

Organization: {{ midaz_onboarding.organization.0.legal_name }}

Ledger: {{ midaz_onboarding.ledger.0.name }}

``` **`

Generation Date: {% date_time "dd/MM/YYYY HH:mm" %}

`** This function applies the date and time at render time, in the `dd/MM/YYYY HH:mm` format. **`

Organization: {{ midaz_onboarding.organization.0.legal_name }}

`** 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_onboarding.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]` takes the first matching balance (assuming there's only one per account). * The result goes into 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 }}
`** Shows 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 }}

`** Shows 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 relates to the current account in the outer loop (from your earlier block). It proceeds only 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 line saves the result as `discount_amount`. **`{% set final_amount = original_amount - discount_amount %}`** This line subtracts the discount from the original amount. * The line stores the final result in `final_amount`, which you can render later. ### Table columns ```html HTML theme={null} {{ operation.id }} {{ operation.type }} {{ original_amount|decimal:2 }} {{ discount_amount|decimal:2 }} {{ final_amount|decimal:2 }} {{ operation.description }} ``` This block outputs a 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|decimal:2 }}`** Shows the original (unadjusted) amount, formatted with 2 decimal places. **`{{ discount_amount|decimal:2 }}`** Shows the calculated discount, also with 2 decimal places. **`{{ final_amount|decimal:2 }}`** Shows the final amount after applying the 3% discount. **`{{ 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. The footer states that Reporter by Lerian generated the content automatically. * 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

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 *** ![Example financial report rendered from the HTML template code shown above](https://mintcdn.com/lerian-49cb71fc/uUQdWFdbYtzFLYzd/images/en/docs/html_template.jpg?fit=max\&auto=format\&n=uUQdWFdbYtzFLYzd\&q=85\&s=da9d3540b0a8b7b1122b38f24c2715cc) Figure 1. An example of a financial report rendered from the HTML code in the example. # Reporter quick start Source: https://docs.lerian.studio/en/products/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/products/reporter/using-reporter). For complete request and response schemas, use the [Reporter API quick start](/en/reference/products/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/products/reporter/template-examples). ## Upload the template *** Upload the file with the four required multipart fields: `template`, `outputFormat`, `description`, and `category`. The category names the report family, in lowercase letters, digits, underscores, and hyphens. ```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" \ -F "category=quick_start" ``` Save the returned template `id` as `TEMPLATE_ID`: ```bash theme={null} export TEMPLATE_ID="returned-template-id" ``` See [Upload template](/en/reference/products/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/products/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/products/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/products/reporter/download-report) for response headers and error details. ## Troubleshooting *** | Problem | Check | | - | - | | Template upload is rejected | Include the `.tpl` file, `outputFormat`, `description`, and `category`; confirm that the file content matches the format. | | Report creation is rejected | Send both `templateId` and `filters`; use `{}` when no filter applies. When the tenant has several institutions, also send `institution`. | | 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/products/reporter/using-reporter) to design a reusable, recurring workflow. * Learn how to reference configured data in [Template formats](/en/products/reporter/template-examples). * Review the specialized guidance in [BACEN templates](/en/products/reporter/reporter-bacen-templates) before working with regulatory report structures. * Use the [Reporter API quick start](/en/reference/products/reporter/reporter-developer-quick-start) for complete API examples. # Reporter REST API Source: https://docs.lerian.studio/en/products/reporter/reporter-rest-api Orient yourself in the Reporter API: the /v1 base path, bearer authentication, the 56 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 56 operations across nine areas: templates, the template builder, reports, data sources, deadlines, institutions, regulatory filing, metrics, and the streaming manifest. All 56 render under the **Reporter** anchor in the [API Reference](/en/reference/introduction). This page covers what the operations share and points you at the reference page for each one. > [!NOTE] > > 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`, `institutions`, `regulatory`, `metrics`, or `streaming`. Approving a regulatory draft authorizes against its own resource, `regulatory-approval`. 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. ## The operations by job *** ### Templates Nine operations own the template lifecycle: [list](/en/reference/products/reporter/list-templates), [upload](/en/reference/products/reporter/upload-template), [get](/en/reference/products/reporter/retrieve-template-details), [get content](/en/reference/products/reporter/get-template-content), [update](/en/reference/products/reporter/update-templates), [delete](/en/reference/products/reporter/delete-template), and [preview](/en/reference/products/reporter/preview-template), plus [list report families](/en/reference/products/reporter/list-report-families) and [assign a report family](/en/reference/products/reporter/assign-report-family). A template is a plain-text `.tpl` file plus its output format, description, and category. Deleting a template also removes the deadlines that point at it. ### Template builder Four operations back a visual editor without persisting anything. [List block definitions](/en/reference/products/reporter/list-block-definitions) and [list filter definitions](/en/reference/products/reporter/list-filter-definitions) return the catalog an editor draws from. [Validate blocks](/en/reference/products/reporter/validate-template-blocks) checks a block tree and reports per-block errors. [Generate code](/en/reference/products/reporter/generate-template-code) turns that tree into template source you can then upload. ### Reports Four operations: [create](/en/reference/products/reporter/create-report), [get](/en/reference/products/reporter/check-report-status), [list](/en/reference/products/reporter/retrieve-reports), and [download](/en/reference/products/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`, and a regulatory filing reaches `Draft` instead of `Finished`. Poll the get operation, or subscribe to the events described in [Events](/en/products/reporter/reporter-events). Download serves a report in `Finished` or `Final`, and a `Draft` only when no required field is blank. The `X-Report-Status` response header names the status you received. Every report you create stays addressable by its identifier for as long as your object-storage retention policy keeps the artifact. ### Data sources Seven operations manage the persisted registry under `/v1/data-sources` (note the hyphen). You can list or create entries, get, partially update, or soft-delete one by `dataSourceId`, inspect its live schema, and test its connection. Passwords are write-only, encrypted at rest, and absent from every response. A delete is refused while a live template still references the data source. A self-hosted deployment may also seed registry entries from `DATASOURCE_*` variables at startup. ### Institutions Six operations register the financial institutions a tenant reports for: [list](/en/reference/products/reporter/list-institutions), [create](/en/reference/products/reporter/create-institution), [get](/en/reference/products/reporter/get-institution), [replace](/en/reference/products/reporter/replace-institution), [delete](/en/reference/products/reporter/delete-institution), and [propose from Midaz](/en/reference/products/reporter/propose-institutions). An institution has one CNPJ and a scope that names its rows in each data source. A report request names the institution it reads for. When the tenant has several institutions, a request that names none is refused. Replace requires `If-Match` with the version you read. ### Regulatory filing Eighteen operations run the filing cycle for the curated regulatory documents that a deployment carries: * **Catalog**: [list documents](/en/reference/products/reporter/list-regulatory-documents), [get a document](/en/reference/products/reporter/get-regulatory-document), and [get its contract](/en/reference/products/reporter/get-document-contract), which lists every field the file needs. * **Subscriptions**: [list](/en/reference/products/reporter/list-subscriptions), [create](/en/reference/products/reporter/create-subscription), [get](/en/reference/products/reporter/get-subscription), [deactivate](/en/reference/products/reporter/deactivate-subscription), [reactivate](/en/reference/products/reporter/reactivate-subscription), [adopt another document version](/en/reference/products/reporter/adopt-document-version), and [set the filing institution](/en/reference/products/reporter/set-subscription-institution). * **Mapping**: [get](/en/reference/products/reporter/get-subscription-mapping) and [replace](/en/reference/products/reporter/replace-subscription-mapping) the YAML mapping from the document's fields to your tables, and [check it against the live databases](/en/reference/products/reporter/check-mapping-readiness). * **Filings**: [generate a draft](/en/reference/products/reporter/create-regulatory-report), [correct it](/en/reference/products/reporter/correct-regulatory-report), [read the corrections](/en/reference/products/reporter/list-report-corrections), [approve it](/en/reference/products/reporter/approve-regulatory-report), and [read its provenance](/en/reference/products/reporter/get-report-provenance). Approval is refused while a required field is blank. ### Deadlines Six operations model a filing obligation with a due date and a recurrence rule: [list](/en/reference/products/reporter/retrieve-deadlines), [create](/en/reference/products/reporter/create-deadline), [update](/en/reference/products/reporter/update-deadline), [delete](/en/reference/products/reporter/delete-deadline), [deliver](/en/reference/products/reporter/deliver-deadline), and [notifications](/en/reference/products/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/products/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/products/reporter/get-streaming-events) returns the event manifest covered in [Events](/en/products/reporter/reporter-events). ## Content types *** Template create and update are `multipart/form-data`: a `template` file part, plus `outputFormat`, `description`, and `category`. The regulatory mapping is `application/yaml`. 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 *** Template, deadline, data-source, and institution lists 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 uses keyset pagination instead: send `limit` without `page`, then follow `nextCursor` or `prevCursor` from the response. A cursor is absent when there is no page in that direction, and the response does not include `page` or `total`. The report list accepts `status`, `template_id`, `created_at`, and `sort_order`. The cursor carries the sort order for the walk. The template list filters on `output_format`, `category`, `has_category`, `description`, `data_source_name`, and `created_at`. The deadline list filters on `status`, `active`, `type`, `start_date`, and `end_date`. ## 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/products/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 *** > **[API Reference](/en/reference/introduction)** > > The 56 operations, with full request and response shapes. > **[API quick start](/en/reference/products/reporter/reporter-developer-quick-start)** > > Upload a template, generate a report, and download it with cURL. > **[Events](/en/products/reporter/reporter-events)** > > Subscribe to report and deadline events instead of polling. > **[What is Reporter?](/en/products/reporter/what-is-reporter)** > > Templates, reports, data sources, and where they fit. # Reporter SDK and embedding Source: https://docs.lerian.studio/en/products/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, institutions, regulatory filing, the template builder, and metrics are all HTTP calls. Nothing exists only inside a client library, and nothing exists only inside the Console. ## 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 and the action. The resource is one of templates, reports, data sources, deadlines, institutions, regulatory, regulatory approval, metrics, or streaming, and the action 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. Most list operations take `limit` and `page`, 10 and 1 by default. The report list takes `limit` and a `cursor` from the previous page instead. 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 *** **1. Confirm the token and the base URL** ```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. **2. Request a report** ```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. **3. Wait for a terminal state, then download** ```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 ``` Send `X-Idempotency` on every report request, and on template upload for the same reason. Repeat a request that is still in flight 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`. 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. ## Managing data sources *** The API manages the persisted data-source registry. Use it to list or create connections, get or partially update one by `dataSourceId`, inspect its live schema, test it, or soft-delete it: ```bash theme={null} curl -s "https://reporter.example.com/v1/data-sources" \ -H "Authorization: Bearer $TOKEN" ``` Each entry carries the stable `configName` that templates address. Responses never contain passwords. Fetch one by identifier with `/v1/data-sources/{dataSourceId}` and inspect its live tables or collections with `/v1/data-sources/{dataSourceId}/schema`. ## The Go SDK *** [`lerian-sdk-golang`](https://github.com/LerianStudio/lerian-sdk-golang) ships a `reporter` package alongside the other Lerian products. It acquires an OAuth2 client-credentials token, refreshes it, and hands back typed results. Configure it with the same `https:///v1` base URL you use with cURL. Add the client ID, the client secret, 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, data, 0o600); err != nil { return err } } ``` `Download` returns the rendered bytes in the template's output format. Write them to disk, as above, or stream them to your caller. Use the package to get a report and download its file. Create templates and reports, and call every other operation, over REST. Mixing both in one integration is normal. ## 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 and decides whether that user may run this report. It 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/products/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. > [!NOTE] > > 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 *** > **[Reporter REST API](/en/products/reporter/reporter-rest-api)** > > Every operation, grouped by the job it does. > **[Reporter events](/en/products/reporter/reporter-events)** > > The event contract, and how to subscribe instead of polling. > **[API quick start](/en/reference/products/reporter/reporter-developer-quick-start)** > > Upload a template and generate a report with cURL. > **[Error list](/en/reference/products/reporter/reporter-error-list)** > > Reporter's error codes and what resolves them. # Reporter template engine Source: https://docs.lerian.studio/en/products/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/products/reporter/template-reference) lists every tag and filter. [Template formats](/en/products/reporter/template-examples) shows a worked file per output format. ## 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|decimal: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 *** A datasource normally lives in Reporter's persisted registry and is managed through the [data-source API](/en/reference/products/reporter/list-data-sources). An environment block can seed a registry entry at Manager startup only when no active or soft-deleted entry already uses that config name. `CONFIG_NAME` sets the name your templates use, and `SCHEMAS` lists the schemas Reporter discovers on it. Use the same name for the environment-name segment and `CONFIG_NAME`, uppercasing that name in environment-variable keys, 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. The schema key is `DATASOURCE_{CONFIG_NAME}_SCHEMAS`, whereas the other environment keys use the `{NAME}` segment. See [Environment variables](/en/products/reporter/reporter-environment-variables) for the full bootstrap 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, 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 what the block catalogue returns as the category string. A client that matches 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. The template you upload is always the text. Three operations support that flow: * [Validate template blocks](/en/reference/products/reporter/validate-template-blocks) checks the structure and reports each problem against the block that caused it. * [Generate template code](/en/reference/products/reporter/generate-template-code) returns the finished source for an output format. * [List block definitions](/en/reference/products/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/products/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 the `decimal` filter, as in `{{ total|decimal:2 }}`, 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 a byte order mark. PDF output ignores the marker. ## Generation parameters *** A template can declare the parameters that a report request answers. Each declaration is a template comment: ```django theme={null} {# reporter:parameter name=dataBase type=month required=true #} ``` A declaration takes the keys `name`, `type`, `required`, `default`, `allowedValues`, `description`, and `appliesTo`. The type is `string`, `integer`, `decimal`, `boolean`, `date` (`YYYY-MM-DD`), or `month` (`YYYY-MM`). A malformed declaration fails the upload. The report request sends the values in `parameters`, and the template prints one as `{{ system.dataBase }}`. When the template declares parameters, Reporter refuses a request that sends an undeclared one. It also refuses a request that omits a required parameter that has no default. `appliesTo` binds a `date` or `month` parameter to a date field, written as `..`. Reporter then adds the end of that period as an `lte` filter on the field. It refuses a request that also sets an upper bound on that field. ## Limits the engine applies *** > [!NOTE] > > Reporter blocks the Pongo2 tags that load or extend another file: `include`, `extends`, `import`, `block`, and `ssi`. A template is a self-contained document. Reporter also blocks `filter`, which applies a filter to a whole block of output. 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 *** > **[How report generation works](/en/products/reporter/how-report-generation-works)** > > The path from a report request to a file you can download. > **[Template reference](/en/products/reporter/template-reference)** > > Every tag, filter, and operator the syntax accepts. > **[Template formats](/en/products/reporter/template-examples)** > > A worked template per output format. > **[Reporter core concepts](/en/products/reporter/reporter-core-concepts)** > > Templates, data sources, reports, and deadlines in one place. # TXT template Source: https://docs.lerian.studio/en/products/reporter/reporter-txt-template Build a transaction receipt template in plain text with Reporter, in a simple .tpl file that renders into a printable TXT proof of payment. This page shows how to create a transaction receipt template with 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 during template processing. * 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`: Creation timestamp of the transaction. * `transaction.amount`: The raw amount of the transaction. * `transaction.status`: Current status, such as `APPROVED`, `PENDING`, or `CANCELED`. ### 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 automatic generation of the file, 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 Transaction Status: APPROVED ------------------------------------------ Source Accounts: - Alias: @external|USD - Debit Amount: 1500 Target Accounts: - Alias: @wallet_12345 - Credit Value: 1500 ------------------------------------------ Automatically generated document. ``` # XML template Source: https://docs.lerian.studio/en/products/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 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.organization.0.legal_name }} - Tax ID: {{ midaz_onboarding.organization.0.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 %} {%- endwith %} ``` ## Code breakdown *** ### Organization and Ledger information ```xml XML theme={null} {{ midaz_onboarding.organization.0.legal_name }} - Tax ID: {{ midaz_onboarding.organization.0.legal_document }} {% date_time "dd/MM/YYYY HH:mm" %} {%- with ledger = midaz_onboarding.ledger[0] %} {{ ledger.name }} ``` **`{{ midaz_onboarding.organization.0.legal_name }} - Tax ID: {{ midaz_onboarding.organization.0.legal_document }}`** Displays the organization’s legal name and corresponding *CNPJ* (Brazilian Tax ID). * `{{ midaz_onboarding.organization.0.legal_name }}`: retrieves the organization's name. * `{{ midaz_onboarding.organization.0.legal_document }}`: retrieves the *CNPJ*. **`{% date_time "dd/MM/YYYY HH:mm" %}`** This function applies the date and time of the render, in 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. * `balance` holds the result. **` {{ 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. * If they match, the balance appears. ### Currency information ```xml XML theme={null} {{ account.asset_code }} ``` Shows the account’s currency (e.g., BRL, USD). * `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 that lists all operations for an account. * 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 %}`** Includes only the relevant operations for the current account. * If the `account_id` of the operation is equal to the current `account.id`, the rest of the block runs. **`{%- 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. * 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. * `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 - Tax ID: 78425230000190 28/04/2025 10:30 O'Connell, Dietrich and Bernhard 01965efe-5afd-733d-9a64-3d53f73b13c8 @external/BRL 1000 1000 BRL 01965f04-7087-73f1-a802-f8fceedb34ec Initial transfer DEBIT 1000 30 970 BRL 1 1000 1000 01965eff-0b2d-73cf-bbfe-0dc1ac9032d3 @wallet_46040127 0 0 BRL 0 0 0 01965f01-aa72-7254-bb4a-fa9c8c9d4009 @account1 300 300 BRL 01965f04-7087-73d2-a167-cb5146bee801 External to accounts 1 and 2 CREDIT 300 9 291 BRL 1 300 300 01965f01-ff74-712a-9e2c-d987f15a1d14 @account2 700 700 BRL 01965f04-7087-73b1-aff9-69f59d623d31 External to accounts 1 and 2 CREDIT 700 21 679 BRL 1 700 700 01965f02-55fc-7a06-9f95-07e9a9ff4941 @account3 0 0 BRL 0 0 0 ``` # Template formats Source: https://docs.lerian.studio/en/products/reporter/template-examples Design Reporter templates for HTML, XML, CSV, PDF, or TXT outputs, with practical examples to start building reports that fit your use case. Reporter supports multiple output formats: HTML, XML, CSV, PDF, and plain text. The examples show how to structure your `.tpl` files for each format. They follow the formatting rules and parse correctly during generation. > [!CAUTION] > > The file content **must** follow the output format you need (HTML, XML, CSV or TXT), but make sure to save it with a `.tpl` extension. For PDF output, author the template as HTML. Reporter converts it during generation. ## Available examples *** > **[HTML template](/en/products/reporter/reporter-html-template)** > > Analytical financial report with account-level transaction analysis. > **[XML template](/en/products/reporter/reporter-xml-template)** > > Analytical financial report in XML with custom logic and summaries. > **[TXT template](/en/products/reporter/reporter-txt-template)** > > 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/products/reporter/create-report) endpoint: ```json JSON theme={null} { "templateId": "0196f99c-4d3e-73bc-b5d3-2a5bb1efbee4", "filters": { "midaz_transaction": { "transaction": { "id": { "eq": ["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`. The receipt includes that transaction and the operations linked to it. ### What happens next? After you send the request: * The API returns the report `id`. * You can use this ID to check the status of the report. * When it's ready, the report will be available for download. ### Each receipt generated in isolation Reporter generates each receipt in isolation, using the filters and templates you define. # Template reference Source: https://docs.lerian.studio/en/products/reporter/template-reference Reference every template tag, filter, and operator available in Reporter: loops, conditionals, placeholders, and built-in formatting helpers. This page is the reference for the template tags, filters, and operators in Reporter. For an introduction to templates and placeholders, see [What is Reporter](/en/products/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 as %} ... {% endwith %} ``` * **Value formatting** ``` {{ field_name | decimal:2 }} --> renders 123.4 as 123.40 ``` ### Conditional blocks | Block | Description | Example | | - | - | - | | If | Runs block if condition is true | `{% if condition %}...{% endif %}` | | If-else | Runs one block if true, another if false | `{% if condition %}...{% else %}...{% endif %}` | | If-else-if | Allows multiple checks | `{% if a %}...{% elif b %}...{% else %}...{% endif %}` | | Equal | Checks if two values are equal | `{% if a == b %}` | | Not equal | Checks if two values are different | `{% if a != b %}` | | Greater than | Checks if a is greater than b | `{% if a > b %}` | | Less than | Checks if a is less than b | `{% if a < b %}` | | Greater than or equal | Checks if a is greater than or equal to b | `{% if a >= b %}` | | Less than or equal | Checks if a is less than or equal to b | `{% if a <= b %}` | | And | Returns true if both conditions true | `{% if a and b %}` | | Or | Returns true if at least one true | `{% if a or b %}` | | Not | Inverts Boolean result | `{% if not a %}` | ## Tags reference *** ### Aggregation tags **sum\_by** -- Sums numeric values from a field across all items in a collection. ``` {% sum_by by %} {% sum_by by if %} ``` **Example:** ```xml theme={null} {% sum_by transaction.operation by "amount" if accountAlias != "@external/BRL" %} ``` **count\_by** -- Counts the number of items in a collection. ``` {% count_by %} {% count_by if %} ``` **Example:** ```xml theme={null} {% count_by transaction.operation if accountAlias != "@external/BRL" %} ``` **avg\_by** -- Calculates the average of numeric values in a field. ``` {% avg_by by %} {% avg_by by if %} ``` **min\_by** -- Finds the minimum numeric value in a field. ``` {% min_by by %} {% min_by by if %} ``` **max\_by** -- Finds the maximum numeric value in a field. ``` {% max_by by %} {% max_by by if %} ``` > [!NOTE] > > All aggregation tags use decimal precision to avoid floating-point rounding errors. Missing or non-numeric fields are skipped. Returns `0` if no items match. ### Date and time tag **date\_time** -- Outputs the current date and time in the Reporter runtime's local time zone, formatted according to the provided format string. ``` {% date_time "" %} ``` **Format codes:** | Code | Meaning | Example | | - | - | - | | `YYYY` | 4-digit year | 2025 | | `MM` | 2-digit month | 01-12 | | `dd` | 2-digit day | 01-31 | | `HH` | 2-digit hour (24h) | 00-23 | | `mm` | 2-digit minute | 00-59 | | `ss` | 2-digit second | 00-59 | **Examples:** ``` {% date_time "YYYY-MM-dd" %} --> 2025-02-06 {% date_time "dd/MM/YYYY HH:mm:ss" %} --> 06/02/2025 14:30:45 ``` ### Arithmetic tag **calc** -- Evaluates mathematical expressions with support for variables from the template context. ``` {% calc %} ``` **Supported operators:** | Operator | Description | Precedence | | - | - | - | | `**` | Exponentiation | Highest (right-to-left) | | `*` `/` | Multiplication, division | Middle | | `+` `-` | Addition, subtraction | Lowest | | `( )` | Parentheses | Override precedence | **Examples:** ``` {% calc 100 + 50 %} --> 150 {% calc balance.available * 0.5 %} --> calculated value {% calc (balance.available + 1.2) * balance.on_hold - balance.available / 2 %} ``` > [!NOTE] > > Variables that cannot be resolved default to `0`. Division by zero produces an error. ### Grouped selection tag **last\_item\_by\_group** -- Groups items by a field and selects the most recent item per group, ordered by a date field (descending). Optionally filters items first. Useful for regulatory reports that require the latest record per account grouped by category. ``` {% last_item_by_group group_by "" order_by "" [if ] as %} ``` The result is a list of items -- the latest one per group -- stored in a variable you can iterate over. Each element is the original collection record, so you access its own fields: ``` {% last_item_by_group accounts group_by "cosif_code" order_by "created_at" as latest %} {% for account in latest %} {{ account.cosif_code }}: {{ account.balance }} {% endfor %} ``` > [!NOTE] > > `group_by` accepts a comma-separated list of fields for composite grouping. Maximum collection size: 100,000 items. Results are sorted by the `group_by` value for deterministic output. ### Counter tags **counter** -- Increments a named counter by 1. Produces no output. Counters are scoped per render. ``` {% counter "" %} ``` **counter\_show** -- Displays the sum of one or more named counters. ``` {% counter_show "" %} {% counter_show "" "" "" %} ``` **Example:** ``` {% for tx in ledger.transactions %} {% counter tx.type %} {% endfor %} Total credits: {% counter_show "credit" %} Total debits: {% counter_show "debit" %} Combined: {% counter_show "credit" "debit" %} ``` ## Filters reference *** ### percent\_of Calculates the percentage of a value relative to a total. Returns a formatted string with 2 decimal places. ``` {{ value | percent_of: total }} ``` Example: if `category.amount = "6.00"` and `total.expenses = "20.00"`: ``` {{ category.amount | percent_of: total.expenses }} --> 30.00% ``` ### strip\_zeros Removes trailing zeros from a numeric value without rounding. ``` {{ number | strip_zeros }} ``` **Examples:** ``` {{ "100.50000" | strip_zeros }} --> 100.5 {{ "100.00" | strip_zeros }} --> 100 {{ "99.990" | strip_zeros }} --> 99.99 ``` ### decimal Prints a number with a fixed number of decimal places, from 0 to 18. ``` {{ number | decimal:places }} ``` **Examples:** ``` {{ "100.5" | decimal:2 }} --> 100.50 {{ 7 | decimal:0 }} --> 7 ``` Only `rjust`, `ljust`, or `center` can follow `decimal` in the same expression. Reporter refuses a template that uses `decimal` inside a `{% %}` tag. ### slice Extracts a substring using start and end indices (0-based). ``` {{ string | slice:"start:end" }} ``` **Examples:** ``` {{ "hello" | slice:"0:3" }} --> hel {{ "12345" | slice:"1:4" }} --> 234 ``` ### replace Replaces all occurrences of a search string with a replacement string. Format: `"search:replacement"`. ``` {{ string | replace:"search:replacement" }} ``` **Examples:** ``` {{ "01310-100" | replace:"-:" }} --> 01310100 (removes hyphens) {{ "1234.56" | replace:".:," }} --> 1234,56 (dot to comma) {{ "12.345.678/0001-99" | replace:".:" }} --> 12345678/0001-99 ``` ### where Filters an array of objects by field equality. Supports nested fields via dot notation. ``` {{ array | where:"field:value" }} ``` **Examples:** ``` {{ holders | where:"state:SP" }} {{ holders | where:"address.state:SP" }} ``` Use inside loops: ``` {% for holder in holders|where:"state:SP" %} {{ holder.name }} {% endfor %} ``` ### sum (filter) Sums numeric values from a field across all items in an array. Uses decimal precision. ``` {{ array | sum:"field" }} ``` **Examples:** ``` {{ operations | sum:"amount" }} {{ items | sum:"price.value" }} ``` ### count (filter) Counts elements in an array where a field matches a value. Supports nested fields. ``` {{ array | count:"field:value" }} ``` **Examples:** ``` {{ operations | count:"nat_oper:6" }} {{ holders | count:"address.state:SP" }} ``` ### add\_day Adds a `dia` field to each row of a list. The value is the day of the named date field, as a number in `YYYYMMDD` form. Use it to group or compare rows by day. ``` {{ list | add_day:"date_field" }} ``` ### add\_signed Adds a `signed` field to each row of a list. The value is the named amount, negative when the row's `direction` is `debit`. Use it to sum a net balance. ``` {% with ops = midaz_transaction.operation|add_signed:"amount" %} {% sum_by ops by "signed" %} {% endwith %} ``` ### filter (function) Returns the rows of a list whose field equals a value. ``` {% for operation in filter(midaz_transaction.operation, "transaction_id", transaction.id) %} ``` ### contains Checks if one value is partially included in another, ignoring case. Useful when data includes dynamic prefixes or suffixes. ``` {% if contains(source_field, target_field) %} ``` **Example:** * Source: `0#@external/BRL` * Target: `@external/BRL` Returns `true` because `@external/BRL` exists within the source value. ## Operators and filters summary *** | Name | Type | Description | | - | - | - | | `sum_by` | Tag | Sum values by field with optional filter | | `count_by` | Tag | Count items with optional filter | | `avg_by` | Tag | Calculate average by field | | `min_by` | Tag | Find minimum value | | `max_by` | Tag | Find maximum value | | `date_time` | Tag | Format current date/time | | `calc` | Tag | Evaluate arithmetic expressions | | `last_item_by_group` | Tag | Latest item per group by date | | `counter` | Tag | Increment a named counter | | `counter_show` | Tag | Display counter value(s) | | `percent_of` | Filter | Calculate percentage | | `strip_zeros` | Filter | Remove trailing zeros | | `slice` | Filter | Extract substring | | `replace` | Filter | String replacement | | `where` | Filter | Filter array by field value | | `sum` | Filter | Sum array field values | | `count` | Filter | Count matching items | | `decimal` | Filter | Print fixed decimal places | | `add_day` | Filter | Add a numeric day field to each row | | `add_signed` | Filter | Add a signed amount field to each row | | `filter` | Function | Rows whose field equals a value | | `contains` | Function | Partial string match, ignoring case | | `floatformat` | Filter | Round to decimal places | ## Advanced filtering *** When generating a report, you can pass filters in the request body to narrow the data. Filters follow a structure of datasource > table > field: **Single schema (default):** ```json theme={null} { "templateId": "00000000-0000-0000-0000-000000000000", "filters": { "midaz_onboarding": { "account": { "id": { "eq": ["123", "456"] }, "createdAt": { "between": ["2023-01-01", "2023-01-31"] }, "status": { "in": ["active", "pending"] } } } } } ``` **Multi-schema (explicit schema.table key):** ```json theme={null} { "templateId": "00000000-0000-0000-0000-000000000000", "filters": { "external_db": { "sales.orders": { "total": { "gt": [100] }, "created_at": { "gte": ["2025-01-01"] } }, "finance.invoices": { "status": { "eq": ["paid"] } } } } } ``` **Supported operators:** | Operator | Description | Example | | - | - | - | | `eq` | Equal to | `{ "eq": ["active", "pending"] }` | | `gt` | Greater than | `{ "gt": [100] }` | | `gte` | Greater than or equal to | `{ "gte": ["2025-06-01"] }` | | `lt` | Less than | `{ "lt": [1000] }` | | `lte` | Less than or equal to | `{ "lte": ["2025-06-30"] }` | | `between` | Value falls within a range | `{ "between": [100, 1000] }` | | `in` | Value is within a list | `{ "in": ["active", "pending"] }` | | `nin` | Value is not within a list | `{ "nin": ["deleted", "archived"] }` | # Using Reporter Source: https://docs.lerian.studio/en/products/reporter/using-reporter Manage Reporter templates, generate regulatory reports on demand, track their status, and download completed files. Use this guide for the recurring Reporter workflow: manage a template, generate a report, verify the result, and download the finished file. ## Prerequisites Before you begin, make sure that: * Reporter runs and you can authenticate with its API. * An operator has configured at least one data source for the data your template queries. * You have a `.tpl` file that matches the intended output format. For PDF output, author the template as HTML. See [Template examples](/en/products/reporter/template-examples) and the [template reference](/en/products/reporter/template-reference). *** ## Manage templates Reporter uses uploaded `.tpl` files to define report content and layout. ### Upload a template Call [Upload a template](/en/reference/products/reporter/upload-template) as a multipart request with all four required fields: * `template`: the `.tpl` file. * `outputFormat`: the generated file format, such as `HTML`, `PDF`, `XML`, `CSV`, or `TXT`. * `description`: a human-readable description of the template. * `category`: the report family, such as `cadoc`. Use lowercase letters, digits, underscores, and hyphens, up to 100 characters. Reporter returns the template identifier that you use when generating reports. ### Maintain existing templates Use the template endpoints to: * [List templates](/en/reference/products/reporter/list-templates). * [Retrieve template details](/en/reference/products/reporter/retrieve-template-details). * [Update a template](/en/reference/products/reporter/update-templates). * [Delete a template](/en/reference/products/reporter/delete-template). Deleting a template is a soft delete. Reporter excludes it from standard queries but preserves reports already created from it. *** ## Generate a report with filters Call [Create a report](/en/reference/products/reporter/create-report) with both required fields: * `templateId`: the identifier returned when you uploaded the template. * `filters`: the conditions grouped by data source, table, and field. The following request limits the report to one transaction: ```json theme={null} { "templateId": "0196159b-4f26-7300-b3d9-f4f68a7c85f3", "filters": { "midaz_transaction": { "transaction": { "id": { "eq": ["0196d983-a2c2-7d5a-a5b7-029fe0dcb710"] } } } } } ``` To generate a report without filtering rows, send an empty object. Don't omit the field: ```json theme={null} { "templateId": "0196159b-4f26-7300-b3d9-f4f68a7c85f3", "filters": {} } ``` Reporter returns the report identifier in the `id` field. Store this value as `REPORT_ID` to check the generation status and retrieve the output. See [Advanced filtering](/en/products/reporter/template-reference#advanced-filtering) for the supported operators and filter structure. *** ## Discover data source schemas Inspect the configured data sources before building templates or dynamic filter interfaces: * [List data sources](/en/reference/products/reporter/list-data-sources) returns a page of registered connections without credentials. * [Retrieve a data source](/en/reference/products/reporter/retrieve-data-source) returns one connection configuration by `dataSourceId`. Credentials remain hidden. * [Get a data source schema](/en/reference/products/reporter/get-data-source-schema) introspects the live tables or collections and their typed fields. The API owns the registry lifecycle: create a data source, partially update it, test the connection, inspect its schema, or soft-delete it. Reporter refuses deletion while a live Template still references the source. A self-hosted deployment can also seed entries from `DATASOURCE_*` variables at startup. *** ## Interpret report statuses and errors Call [Check report status](/en/reference/products/reporter/check-report-status) with `REPORT_ID`. | Status | Meaning | What to do | | - | - | - | | `Processing` | Reporter is generating the file. | Keep polling with a reasonable interval. | | `Finished` | Generation completed successfully. | Download the report. | | `Partial` | Reporter generated only part of the requested output. | Inspect the response details and correct the failed data sections before generating again. | | `Error` | Generation failed. | Inspect the error details, template, filters, and data source availability. | Treat `Finished` and `Final` reports, and a `Draft` with no required field blank, as downloadable. A `Partial` result requires investigation even when Reporter produced some data. *** ## Verify and download the report When the status is `Finished`: 1. Call [Download a report](/en/reference/products/reporter/download-report) with `REPORT_ID`. 2. Confirm that the response has the expected content type and `Content-Disposition` header. 3. Open the file and verify that its data and layout match the template and filters. *** ## Troubleshooting | Symptom | Check | | - | - | | Template upload is rejected | Send `template`, `outputFormat`, `description`, and `category`, and confirm that the file uses the `.tpl` extension. | | Report creation is rejected | Send both `templateId` and `filters`. Use `"filters": {}` when you don't need row filters. | | Report remains in `Processing` | Check Worker health, RabbitMQ connectivity, and the referenced data sources. | | Report ends as `Partial` | Inspect which data sections failed and verify their source, table, field, and filter names. | | Report ends as `Error` | Check the returned error, template syntax, filter values, data source connectivity, and object storage. | | Download is rejected | Check the latest status. Downloads are available for `Finished` and `Final` reports, and for a `Draft` with no required field blank. | *** ## Operator configuration The following deployment settings are for operators. Application users don't need them for the report generation workflow. ### Configure object storage Reporter stores templates and generated reports in one S3-compatible bucket. It uses the `templates/` and `reports/` prefixes. Reporter supports AWS S3, MinIO, and SeaweedFS. | Variable | Description | Default | | - | - | - | | `OBJECT_STORAGE_ENDPOINT` | S3-compatible endpoint. Leave empty for AWS S3. | -- | | `OBJECT_STORAGE_REGION` | AWS region. | `us-east-1` | | `OBJECT_STORAGE_ACCESS_KEY_ID` | Access key. | -- | | `OBJECT_STORAGE_SECRET_KEY` | Secret key. | -- | | `OBJECT_STORAGE_USE_PATH_STYLE` | Uses path-style URLs. Commonly required by MinIO and SeaweedFS. | `false` | | `OBJECT_STORAGE_DISABLE_SSL` | Uses HTTP instead of HTTPS when `OBJECT_STORAGE_ENDPOINT` has no scheme. An explicit `http://` or `https://` scheme takes precedence. | `false` | | `OBJECT_STORAGE_BUCKET` | Bucket name. | `reporter-storage` | > **AWS S3** > > ```env theme={null} > OBJECT_STORAGE_ENDPOINT= > OBJECT_STORAGE_REGION=us-west-2 > OBJECT_STORAGE_ACCESS_KEY_ID=AKIA... > OBJECT_STORAGE_SECRET_KEY=your-secret-key > OBJECT_STORAGE_USE_PATH_STYLE=false > OBJECT_STORAGE_DISABLE_SSL=false > OBJECT_STORAGE_BUCKET=reporter-prod-bucket > ``` > [!WARNING] > > The MinIO and SeaweedFS examples below use HTTP only for local development. Production deployments require HTTPS and TLS. > **MinIO (local development)** > > ```env theme={null} > OBJECT_STORAGE_ENDPOINT=http://minio:9000 > OBJECT_STORAGE_REGION=us-east-1 > OBJECT_STORAGE_ACCESS_KEY_ID=minioadmin > OBJECT_STORAGE_SECRET_KEY=minioadmin > OBJECT_STORAGE_USE_PATH_STYLE=true > OBJECT_STORAGE_DISABLE_SSL=true > OBJECT_STORAGE_BUCKET=reporter-storage > ``` > **SeaweedFS (local development)** > > ```env theme={null} > OBJECT_STORAGE_ENDPOINT=http://reporter-seaweedfs:8333 > OBJECT_STORAGE_REGION=us-east-1 > OBJECT_STORAGE_ACCESS_KEY_ID=any > OBJECT_STORAGE_SECRET_KEY=any > OBJECT_STORAGE_USE_PATH_STYLE=true > OBJECT_STORAGE_DISABLE_SSL=true > OBJECT_STORAGE_BUCKET=reporter-storage > ``` > [!NOTE] > > S3 doesn't support per-object TTL. Configure [S3 bucket lifecycle policies](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) if generated reports must expire automatically. ### Configure external data sources Set `DATASOURCE_CRED_ENC_KEY` to a persistent hexadecimal AES key before Reporter starts. Generate a 32-byte key with `openssl rand -hex 32`. Manager and worker fail startup when the key is absent or malformed. Keep the same key available to every Reporter runtime that reads the registry, because stored passwords are encrypted with it. Use the API for the normal data-source lifecycle. In a self-hosted deployment, you may also seed PostgreSQL or MongoDB entries at startup with `DATASOURCE__*` variables: | Variable | Description | Required | | - | - | - | | `DATASOURCE__CONFIG_NAME` | Identifier used in templates, such as `midaz_onboarding`. | Yes | | `DATASOURCE__HOST` | Database host. | Yes | | `DATASOURCE__PORT` | Database port. | Yes | | `DATASOURCE__USER` | Database user. | Only when the database requires user authentication | | `DATASOURCE__PASSWORD` | Database password. | Only when the database requires password authentication | | `DATASOURCE__DATABASE` | Database name. | Yes | | `DATASOURCE__TYPE` | `postgresql` or `mongodb`, in lowercase. | Yes | | `DATASOURCE__SSLMODE` | PostgreSQL SSL mode, such as `disable` or `require`. | PostgreSQL only | | `DATASOURCE__SSLROOTCERT` | PostgreSQL root certificate path. | PostgreSQL only | | `DATASOURCE__SSL` | Enables MongoDB SSL. | MongoDB only | | `DATASOURCE__SSLCA` | MongoDB CA certificate path. | MongoDB only | | `DATASOURCE__SSLCA_BASE64` | Standard Base64 of a PEM CA bundle, as an alternative to a certificate file path. For PostgreSQL, set `SSLMODE` to `verify-ca` or `verify-full`, or leave it empty for `verify-full`. | No | | `DATASOURCE__OPTIONS` | Additional MongoDB URI options. | MongoDB only | | `DATASOURCE__ROW_CEILING` | Rows one table can return. Defaults to `DATASOURCE_DEFAULT_ROW_CEILING`. | No | | `DATASOURCE__QUERY_TIMEOUT_SEC` | Query timeout in seconds. Defaults to `DATASOURCE_DEFAULT_QUERY_TIMEOUT_SEC`. | No | | `DATASOURCE__SCHEMAS` | Comma-separated PostgreSQL schemas to expose. The variable prefix derives from `CONFIG_NAME`. | PostgreSQL only | For a source whose `CONFIG_NAME` is `midaz_onboarding`: ```env theme={null} DATASOURCE_ONBOARDING_CONFIG_NAME=midaz_onboarding DATASOURCE_ONBOARDING_HOST=midaz-postgres-replica DATASOURCE_ONBOARDING_PORT=5702 DATASOURCE_ONBOARDING_USER=midaz DATASOURCE_ONBOARDING_PASSWORD=CHANGE_ME DATASOURCE_ONBOARDING_DATABASE=onboarding DATASOURCE_ONBOARDING_TYPE=postgresql DATASOURCE_ONBOARDING_SSLMODE=require ``` Reference it in a template by its `CONFIG_NAME`: ```django theme={null} {% for account in midaz_onboarding.account %} {{ account.id }} - {{ account.name }} {% endfor %} ``` For multiple PostgreSQL schemas, derive the schema variable from `CONFIG_NAME`. For example, `external_db` maps to `DATASOURCE_EXTERNAL_DB_SCHEMAS`: ```env theme={null} DATASOURCE_EXTERNAL_CONFIG_NAME=external_db DATASOURCE_EXTERNAL_HOST=external-postgres DATASOURCE_EXTERNAL_PORT=5432 DATASOURCE_EXTERNAL_USER=db_user DATASOURCE_EXTERNAL_PASSWORD=CHANGE_ME DATASOURCE_EXTERNAL_DATABASE=external_database DATASOURCE_EXTERNAL_TYPE=postgresql DATASOURCE_EXTERNAL_SSLMODE=require DATASOURCE_EXTERNAL_DB_SCHEMAS=sales,inventory,reporting ``` Use `database:schema.table` in templates and `schema.table` as the filter table key: ```django theme={null} {% for order in external_db:sales.orders %} {{ order.id }} - {{ order.total }} {% endfor %} ``` ```json theme={null} { "templateId": "00000000-0000-0000-0000-000000000000", "filters": { "external_db": { "sales.orders": { "created_at": { "gte": ["2025-01-01"] } } } } } ``` When the schema variable isn't set, Reporter uses the `public` schema. Manager and worker connect to a data source on demand, when a request needs it. *** ## Related tasks * [Get started with Reporter](/en/products/reporter/reporter-quick-start) * [Connect Reporter to Midaz](/en/products/reporter/connecting-reporter-to-midaz) * [Build templates](/en/products/reporter/template-reference) * [Review template examples](/en/products/reporter/template-examples) # Reporter in Lerian Console Source: https://docs.lerian.studio/en/products/reporter/using-reporter-in-lerian-console Manage templates, generate reports, and download results in Reporter through the Lerian Console. No API calls or local tooling required. Reporter is available as a product module within the [Lerian Console](/en/platform/console/about-lerian-console), the unified web interface for managing all Lerian products. Through the Console, you can manage templates, generate reports, and download results from a single interface. > **[Reporter in Lerian Console](/en/products/reporter/console/reporter-module)** > > Access the full documentation for using Reporter in the Lerian Console. ## Available actions *** From the Lerian Console, you can: * [Manage templates](/en/products/reporter/console/managing-templates): Upload, update, and delete `.tpl` template files * [Manage reports](/en/products/reporter/console/managing-reports): Generate, track, and download reports # What is Reporter? Source: https://docs.lerian.studio/en/products/reporter/what-is-reporter Turn financial data into repeatable business and operational reports with Reporter: reusable templates, automated rendering, and BACEN-ready output. Reporter is Lerian's licensed reporting engine. It turns data from your financial systems into repeatable reports based on plain-text `.tpl` templates. You define the document structure once, then generate updated outputs whenever you need them. Reporter can render HTML, CSV, XML, PDF, and TXT files. Lerian licenses Reporter to institutions and maintains its source code in a private repository. ## What you can use Reporter for Reporter gives product, operations, and engineering teams a shared way to produce consistent documents from changing data. You can use it to: * Replace recurring manual report assembly with a repeatable process * Keep document structure separate from the data used to populate it * Reuse a template across reporting periods and data sets * Produce several output formats through one reporting workflow * Store templates and generated files for later retrieval ## Problems Reporter solves Reporter separates data extraction, document formatting, and delivery. Data sources provide the values, templates describe the output, and the rendering workflow produces the final file. This separation helps you update a layout without rebuilding the data integration, or connect new data without redesigning every report. ## How Reporter works Each report follows the same flow: 1. You create or select a `.tpl` template. 2. You request a report with the relevant parameters and filters. 3. Reporter retrieves data from configured PostgreSQL or MongoDB sources. 4. The template engine applies the document logic and renders the output. 5. Reporter stores the generated file and makes it available to the requesting system. ![Reporter workflow from a template and configured data sources to a generated report](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/reporter-workflow.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=c1e8c0d4649fd75eecdb139e3ba465b9) ## Core capabilities * **Template-driven reports:** Define layouts and document logic in reusable `.tpl` files. * **Multiple data sources:** Read report data from PostgreSQL and MongoDB. * **Flexible document logic:** Build conditional sections, repeated content, calculations, and aggregations into templates. * **Multiple output formats:** Generate HTML, CSV, XML, PDF, or TXT files. * **Asynchronous processing:** Run report generation outside the request path when a workload requires background processing. * **Durable artifacts:** Keep templates and generated reports in S3-compatible object storage. * **Regulatory filing cycle:** When the deployment carries a curated regulatory catalog, subscribe to a document, map its fields to your tables, and turn each generated draft into an approved filing with a provenance record. * **API-based operation:** Create templates, request reports, and retrieve results through the Reporter API. ## Architecture and deployment model Reporter keeps data access, template processing, storage, and API delivery as separate responsibilities: * The data layer connects to configured PostgreSQL and MongoDB sources. * The template layer resolves data and renders the requested format. * The storage layer uses an S3-compatible service for templates and outputs. * The API layer exposes reporting resources to applications and operators. ![Reporter architecture with data, template processing, storage, and API layers](https://mintcdn.com/lerian-49cb71fc/L6O4e7zHOgclCLRh/images/en/d2/reporter-layers.svg?fit=max\&auto=format\&n=L6O4e7zHOgclCLRh\&q=85\&s=d75f7a4dba14864f5c374e3f43011a3b) You deploy Reporter alongside the databases and object storage it needs to reach. The exact topology depends on your environment and operational model. Reporter can integrate with [Access Manager](/en/platform/access-manager) to control access to reporting resources. Whether that integration is enabled, and which policies apply, depends on your deployment and licensed offering. ## Next steps > **[Start with Reporter](/en/products/reporter/reporter-quick-start)** > > Complete the shortest path from access to a generated report. > **[Connect Reporter to Midaz](/en/products/reporter/connecting-reporter-to-midaz)** > > Understand how Reporter reads data from a Midaz deployment. > **[Use Reporter](/en/products/reporter/using-reporter)** > > Follow task-oriented guidance for templates, data sources, and reports. > **[Explore template capabilities](/en/products/reporter/template-reference)** > > Review the available template tags, filters, and expressions. # Audit and compliance Source: https://docs.lerian.studio/en/products/tracer/audit-compliance Understand how Tracer's immutable, hash-chained audit trail works, and query validation history for compliance reporting. Auditors, compliance officers, and disputes teams use this layer to answer one question: *"Why did this transaction get this decision?"* **What changes in your operation:** evidence of a control goes from "let me pull logs from N systems and reconcile timestamps" to "here is the immutable record, cryptographically chained, showing that this transaction got this decision because this specific rule fired at this moment." **Trade-off:** there is no "delete" or "edit" on audit records, by design. A `TRUNCATE` trigger at the database level blocks bulk deletion. Each record's SHA-256 hash includes the previous record's hash, so removing or re-dating a record breaks the chain everywhere downstream. If you need to remove a record for legal reasons (such as GDPR right-to-be-forgotten on PII), the answer is data minimization upfront, not retroactive editing. > [!TIP] > > **Who is this guide for?** Compliance officers and auditors checking what Tracer guarantees, disputes teams looking up validation history, and developers building reports against the audit endpoints. The retention section assumes zero API knowledge. The query and verify sections assume basic REST. Tracer maintains an immutable audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) of validation decisions. This guide explains how the audit system works and how to query validation history for compliance reporting. ## Audit trail architecture *** Tracer records `POST /v1/validations` decisions with full context for compliance and investigation. ### What gets recorded The audit record of a validation contains: | Data | Description | | - | - | | **Request snapshot** | Complete input payload as received | | **Response snapshot** | Full response including decision and details | | **Decision** | ALLOW, DENY, or REVIEW | | **Reason** | Why the decision was made | | **Evaluated rules** | All rules that were evaluated | | **Matched rules** | Rules that triggered (if any) | | **Limit details** | Usage information for checked limits | | **Processing time** | How long validation took | | **Timestamp** | When the validation occurred | > [!NOTE] > > Audit events are deduplicated for transaction validations. If you retry a validation request with the same `requestId`, Tracer stores only the first audit event. This ensures the audit trail reflects unique business events, not API retry patterns. ### Immutability and hash chain Audit records are write-once and cryptographically chained: * You cannot modify a record after creation. * A `TRUNCATE` trigger protects the audit table at the database level and blocks bulk deletion. * Each record stores a SHA-256 hash computed over the record's identity, timestamp, actor, and the previous record's hash, forming an append-only chain. Removing, re-ordering, or re-dating a record makes every later record fail verification. * A `pg_advisory_xact_lock` serializes hash-chain writes to keep the order stable under concurrent inserts. You can verify the chain at any time using `GET /v1/audit-events/{id}/verify`, which returns: ```json theme={null} { "isValid": true, "totalChecked": 12345, "message": "Hash chain integrity verified successfully" } ``` The check covers the chain from its first record up to and including the record you name. When every hash still matches, `isValid` is `true` and `message` confirms it. When a record no longer matches its stored hash, `isValid` is `false`. The `message` field reports tampering. On a failed check, `firstInvalidId` carries an internal sequence number for the diverging record. It is not an audit-event id, so it is not a value you can pass to `GET /v1/audit-events/{id}`. *** ## Data retention *** ### Retention periods | Data type | Retention period | Reason | | - | - | - | | **Rules (active/inactive)** | Indefinite | Operational continuity | | **Rules / Limits (deleted)** | Soft-deleted: row kept indefinitely for audit, excluded from API listings | Compliance trail must outlive operational visibility | | **Limits** | Indefinite | Operational continuity | *** ## Querying validation history *** Use the `GET /v1/validations` endpoint to query historical validations. ### Basic query ```http theme={null} GET /v1/validations X-API-Key: {api_key} ``` Returns validations in cursor-paginated reverse chronological order. > [!WARNING] > > **A query with no dates covers the last 90 days, not the full retention period**. Tracer applies that default window only when both `start_date` and `end_date` are absent. Send either one, or both, to query an older range. With only `start_date`, the range runs forward from that date with no end. With only `end_date`, the range runs backward from that date with no start. ### Filtered query ```http theme={null} GET /v1/validations?start_date=2026-01-01T00:00:00Z&end_date=2026-01-31T23:59:59Z&decision=DENY X-API-Key: {api_key} ``` ### Available filters | Parameter | Type | Description | | - | - | - | | `start_date` | RFC3339 | Start of date range (inclusive). Defaults to 90 days ago when neither date is supplied | | `end_date` | RFC3339 | End of date range (inclusive). Defaults to the end of today when neither date is supplied | | `decision` | enum | Filter by ALLOW, DENY, or REVIEW | | `account_id` | UUID | Filter by account | | `segment_id` | UUID | Filter by segment | | `portfolio_id` | UUID | Filter by portfolio | | `transaction_type` | enum | Filter by CARD, WIRE, PIX, CRYPTO | | `matched_rule_id` | UUID | Filter by rule that matched | | `exceeded_limit_id` | UUID | Filter by limit that was exceeded | ### Date format requirement > [!WARNING] > > Date parameters must use RFC3339 format with mandatory timezone. Tracer rejects date-only formats. **Valid:** ``` start_date=2026-01-01T00:00:00Z start_date=2026-01-01T00:00:00-03:00 ``` **Invalid:** ``` start_date=2026-01-01 (rejected - missing time and timezone) ``` ### Pagination Results use cursor-based pagination. The response includes `nextCursor` and `hasMore` fields to navigate through results. | Parameter | Default | Maximum | Description | | - | - | - | - | | `limit` | 100 | 1000 | Results per page | | `cursor` | - | - | Pagination cursor from previous response | > [!NOTE] > > Cursor pagination keeps `sort_by` and `sort_order` from the original query. ### Sorting ```http theme={null} GET /v1/validations?sort_by=created_at&sort_order=DESC ``` | Parameter | Options | Default | | - | - | - | | `sort_by` | `created_at`, `processing_time_ms` | `created_at` | | `sort_order` | ASC, DESC | DESC | *** ## Getting validation details *** Retrieve complete details for a specific validation using `GET /v1/validations/{id}`. The response contains everything needed to understand a validation decision: * **Request snapshot**: The complete input payload as received * **Response snapshot**: Full response including decision and reason * **Evaluated rules**: All rules that Tracer checked * **Matched rules**: Rules that triggered (if any) * **Limit details**: Usage information for checked limits * **Timestamps**: When the validation occurred and processing time *** ## Querying audit events *** Beyond validation records, Tracer also exposes a generic audit event log via `GET /v1/audit-events`. This is the only way to see lifecycle changes for rules and limits: who created, updated, activated, deactivated, drafted, or deleted them. ### Event types | `eventType` | When emitted | | - | - | | `TRANSACTION_VALIDATED` | A validation request was processed (also available via `GET /v1/validations`) | | `RULE_CREATED` / `RULE_UPDATED` | Rule created or modified | | `RULE_ACTIVATED` / `RULE_DEACTIVATED` | Rule moved into ACTIVE / INACTIVE | | `RULE_DRAFTED` | Rule moved from INACTIVE back to DRAFT for re-editing | | `RULE_DELETED` | Rule soft-deleted | | `LIMIT_CREATED` / `LIMIT_UPDATED` | Limit created or modified | | `LIMIT_ACTIVATED` / `LIMIT_DEACTIVATED` | Limit moved into ACTIVE / INACTIVE | | `LIMIT_DRAFTED` | Limit moved from INACTIVE back to DRAFT for re-editing | | `LIMIT_DELETED` | Limit soft-deleted | | `RESERVATION_RESERVED` / `RESERVATION_CONFIRMED` / `RESERVATION_RELEASED` / `RESERVATION_EXPIRED` | Two-phase reservation lifecycle on the ledger seam: capacity held, committed, returned on abort, or expired | ### Filters `GET /v1/audit-events` accepts the same date-range and scope filters as `GET /v1/validations`, plus filters tailored to lifecycle events: | Parameter | Type | Description | | - | - | - | | `start_date` / `end_date` | RFC3339 | Date range (both inclusive). With neither supplied, the query covers the last 90 days | | `event_type` | enum | `TRANSACTION_VALIDATED`, or a rule, limit, or reservation event type | | `action` | enum | `VALIDATE`, `CREATE`, `UPDATE`, `DELETE`, `ACTIVATE`, `DEACTIVATE`, `DRAFT`, `RESERVE`, `CONFIRM`, `RELEASE`, `EXPIRE`, `SKIP` | | `result` | enum | `SUCCESS`, `FAILED` (for CRUD) or `ALLOW`, `DENY`, `REVIEW` (for validations) | | `resource_type` | enum | `transaction`, `rule`, `limit`, `reservation` | | `resource_id` | UUID | The affected resource ID | | `actor_type` / `actor_id` | string | `user`, `api_key`, or `system`, plus the actor ID | | `account_id` / `segment_id` / `portfolio_id` / `transaction_type` / `matched_rule_id` | UUID/enum | Same scope filters as validations | | `limit`, `cursor`, `sort_by`, `sort_order` | — | Cursor pagination (default `limit` 100, max 1000; `sort_by` accepts `created_at` or `event_type`) | ### Use cases * **Who activated this rule?** `GET /v1/audit-events?resource_type=rule&resource_id={ruleId}&action=ACTIVATE` * **All rule changes last week:** `GET /v1/audit-events?resource_type=rule&start_date=...&end_date=...` * **All limit deletions in 2026:** `GET /v1/audit-events?resource_type=limit&action=DELETE&start_date=2026-01-01T00:00:00Z` ### Single event detail Use `GET /v1/audit-events/{id}` to retrieve a specific audit record, including the snapshot of state at the time of the event. *** ## Compliance reporting scenarios *** Common queries for audit and compliance reporting. ### Scenario 1: Audit investigation "Why was this transaction denied on January 15th?" ```http theme={null} GET /v1/validations/{id} ``` The response shows the exact request received, all rules evaluated, which rule or limit caused the denial, and the timestamp. ### Scenario 2: Monthly compliance report "Show all denied transactions for corporate accounts in January" ```http theme={null} GET /v1/validations?start_date=2026-01-01T00:00:00Z&end_date=2026-02-01T00:00:00Z&decision=DENY&segment_id=corporate-segment-uuid&limit=1000 ``` ### Scenario 3: Rule effectiveness analysis "Which transactions were denied by a specific fraud rule?" ```http theme={null} GET /v1/validations?matched_rule_id=fraud-rule-uuid&decision=DENY&limit=1000 ``` ### Scenario 4: Limit usage review "Which transactions exceeded spending limits this month?" ```http theme={null} GET /v1/validations?start_date=2026-01-01T00:00:00Z&end_date=2026-02-01T00:00:00Z&exceeded_limit_id=daily-limit-uuid ``` *** ## Best practices for compliance *** Recommendations for maintaining audit readiness. ### Record keeping * **Store validation IDs** in your transaction records for easy cross-reference * **Log the requestId** you send to Tracer for correlation * **Export regularly** if you need records in external audit systems > [!WARNING] > > **Common pitfalls when reading the audit trail:** > > * **"I can see the validation but the rule that fired was already deleted."** Deleted rules are soft-deleted. The row stays in the database, but it doesn't appear in `GET /v1/rules`. To investigate, query `GET /v1/audit-events?resource_type=rule&resource_id={ruleId}` for the lifecycle of that rule, including its activations and the eventual delete. > * **"Two retries of the same `requestId` only produced one audit event."** This is by design (deduplication via `idx_audit_events_validation_dedup`). The audit trail reflects unique business events, not API retry patterns. If your retry produced a different decision, that's worth investigating. Tracer should return the original cached response. > * **"`/verify` says the chain is broken on a record I haven't touched."** The chain links every record to the previous one, so a tamper or DB corruption anywhere makes everything downstream report invalid. Run `/verify` against progressively earlier records to bracket where the chain first breaks. > * **"My audit query came back empty for last year."** A query with no `start_date` and no `end_date` covers the last 90 days. Supply the range you want. ### Audit preparation * **Test queries** before audit season to ensure you can retrieve needed data * **Verify date ranges** work correctly with your timezone requirements ### Investigation workflow When investigating a specific transaction: 1. **Find the validation ID** from your transaction logs or Tracer history 2. **Retrieve full details** using GET /v1/validations/ 3. **Review the request snapshot** to see the data in the request 4. **Check matched rules** to understand the reason for the decision 5. **Verify limit status** if limits apply *** ## No-match behavior and audit *** When a validation runs and no rule matches, Tracer returns a configured default decision instead of treating the empty match as an error. This is a **per-request fallback**, not an infrastructure resilience strategy. The `DEFAULT_DECISION_WHEN_NO_MATCH` environment variable governs the decision (default: `ALLOW`). | Scenario | Behavior | Audit record | | - | - | - | | No rules match | Returns `DEFAULT_DECISION_WHEN_NO_MATCH` (default `ALLOW`) | Recorded with `reason: "No matching rules found"` | | Evaluation timeout | Returns HTTP 504 with error code `0422` | No validation record; an audit failure event may be emitted | | Database error during limit check | Returns HTTP 500; the entire validation transaction is rolled back | No validation record persisted | > [!NOTE] > > Set `DEFAULT_DECISION_WHEN_NO_MATCH=DENY`. The service logs a warning at startup if this remains at the default `ALLOW`. Infrastructure failures (database down, cache stale, timeout) do **not** fall back to ALLOW. They surface as HTTP errors to the client, and the original transaction has no audit record. Operators should monitor `tracer_audit_persist_failures_total` and the `/readyz` endpoint to detect these cases. *** ## Quick reference *** Key endpoints and retention information. ### Endpoints | Operation | Method | Endpoint | | - | - | - | | List validations | GET | `/v1/validations` | | Get validation | GET | `/v1/validations/{id}` | | List audit events | GET | `/v1/audit-events` | | Get audit event | GET | `/v1/audit-events/{id}` | | Verify hash chain | GET | `/v1/audit-events/{id}/verify` | ### Retention summary | Data | Retention | | - | - | | Active rules/limits | Indefinite | # Managing limits Source: https://docs.lerian.studio/en/products/tracer/console/managing-limits Create, filter, activate, and edit Tracer spending limits in Lerian Console with period-based caps, scope conditions, and DRAFT to ACTIVE lifecycle controls. Use the **Limits** page to define spending caps by period and transaction context. ## Accessing the Limits page *** In the Tracer sidebar, select **Limits**. The page opens as **Transaction Limits** with the helper **What is a Limit?**. ## Available filters *** | Filter | Options | | - | - | | **Search** | Search by name and account ID | | **Status** | All, Active, Inactive, Draft | | **Period** | All Periods, Daily, Weekly, Monthly, Custom, Per Transaction | | **Type** | All Types, PIX, Card, Wire, Crypto | Items per page is also configurable. ## Limit cards *** Each card shows: * Limit name and limit type. * Max amount and asset. * Limit status (`ACTIVE`, `INACTIVE`, `DRAFT`). * Scope badges (when configured). * Action menu. ## Limit actions *** | Action | Behavior | | - | - | | **Details** | Opens the limit side panel | | **Activate** | Moves limit to `ACTIVE` | | **Deactivate** | Moves limit to `INACTIVE` | | **Move to Draft** | Available for `INACTIVE` limits; returns to `DRAFT` | | **Delete** | Permanently deletes limit with confirmation | > [!WARNING] > > Deactivate an `ACTIVE` limit before you move it back to `DRAFT`. ## Creating a new limit *** **Step 1** Click **New Limit**. **Step 2** Fill the side panel: | Field | Required | Notes | | - | - | - | | **Name** | Yes | Limit name | | **Description** | No | Limit context | | **Limit Type** | Yes | `DAILY`, `WEEKLY`, `MONTHLY`, `CUSTOM`, `PER_TRANSACTION`. `CUSTOM` reveals **Start Date** and **End Date** fields. | | **Asset** | Yes | `BRL`, `USD`, `EUR` | | **Max Amount** | Yes | Numeric max amount | | **Active Time Window** | No | Reveals **Start Time (UTC)** and **End Time (UTC)**. Tracer skips the limit for transactions outside the window. | | **Scope** | Yes | Groups of conditions. See [Scope](#scope). | **Step 3** Click **Create**. New limits start in `DRAFT` status. ## Editing a limit *** Open **Details** to edit. The Console only allows saving changes for `DRAFT` limits and shows **Only draft limits can be edited** otherwise. You cannot change `Limit Type` and `Asset` on an existing limit. The Console has no reset time field. To start periods at another time of day, create the limit through the API with `resetTime`. See [Reset time](/en/products/tracer/spending-limits#reset-time). ## Scope *** A limit needs at least one group. **Add Group** adds a group of conditions. Inside a group, **Add Condition** adds a row with a scope type and a value. Each scope type appears once per group. | Scope type | Input | | - | - | | Account | UUID | | Merchant | UUID | | Segment | UUID | | Portfolio | UUID | | Transaction type | Selector (`CARD`, `PIX`, `WIRE`, `CRYPTO`) | | Sub type | Text | All conditions in a group must match. One matching group is enough, and the Console shows **OR** between groups. Each group saves as one scope object. For every limit type except `PER_TRANSACTION`, the Console notes that usage counts separately for the accounts, portfolios, segments, and merchants each group names. Transaction type does not split the count. ## Related *** * [Spending limits](/en/products/tracer/spending-limits) - Conceptual guide * [Create limit](/en/reference/products/tracer/create-limit) - API reference * [Update limit](/en/reference/products/tracer/update-limit) - API reference * [Retrieve limit usage](/en/reference/products/tracer/retrieve-limit-usage) - API reference # Managing rules Source: https://docs.lerian.studio/en/products/tracer/console/managing-rules Create, filter, activate, and edit Tracer decision rules in Lerian Console with CEL expression validation, ALLOW/DENY/REVIEW actions, and scope conditions. Use the **Rules** page to create and operate Tracer decision rules. A rule combines expression logic, decision action, and optional scope. ## Accessing the Rules page *** In the Tracer sidebar, select **Rules**. The page opens as **Fraud Detection Rules** with the inline helper **What is a Rule?**. ## Available filters *** Use the filter bar at the top: | Filter | Options | | - | - | | **Search** | Search by rule name | | **Status** | All, Active, Inactive, Draft | | **Decision** | All, Allow, Deny, Review | | **Transaction Type** | All Types, Card, PIX, Wire, Crypto | You can also choose items per page. ## Rule cards *** Each card shows: * Rule name. * Rule status (`ACTIVE`, `INACTIVE`, `DRAFT`). * Rule action (`ALLOW`, `DENY`, `REVIEW`). * Description (when provided). * Rule expression. * Last update date. ## Rule actions *** The action menu on each card supports: | Action | Behavior | | - | - | | **Details** | Opens the rule side panel | | **Activate** | Moves rule to `ACTIVE` | | **Deactivate** | Moves rule to `INACTIVE` | | **Move to Draft** | Available for `INACTIVE` rules; returns to `DRAFT` | | **Delete** | Permanently deletes rule with confirmation | > [!WARNING] > > Deactivate an `ACTIVE` rule before you move it back to `DRAFT`. ## Creating a new rule *** **Step 1** Click **New Rule**. **Step 2** Complete the side panel fields: | Field | Required | Notes | | - | - | - | | **Name** | Yes | Rule name | | **Description** | No | Rule context | | **Expression** | Yes | Evaluated with real-time validation | | **Action** | Yes | `ALLOW`, `DENY`, or `REVIEW` | | **Scope** | No | Groups of conditions. See [Scope](#scope). | **Step 3** Click **Create**. New rules start in `DRAFT` status. ## Editing a rule *** Open **Details** to edit. The Console only allows saving changes for `DRAFT` rules. Other statuses show **Only draft rules can be edited**. ## Scope *** **Add Group** adds a group of conditions. Inside a group, **Add Condition** adds a row with a scope type and a value. Each scope type appears once per group. | Scope type | Input | | - | - | | Account | UUID | | Merchant | UUID | | Segment | UUID | | Portfolio | UUID | | Transaction type | Selector (`CARD`, `PIX`, `WIRE`, `CRYPTO`) | | Sub type | Text | All conditions in a group must match. One matching group is enough, and the Console shows **OR** between groups. Each group saves as one scope object. A rule with no group applies to all transactions. To save it, select **This rule applies to all transactions**. ## Related *** * [Rule engine](/en/products/tracer/rule-engine) - Conceptual behavior * [Create rule](/en/reference/products/tracer/create-rule) - API reference * [Update rule](/en/reference/products/tracer/update-rule) - API reference # Managing validations Source: https://docs.lerian.studio/en/products/tracer/console/managing-validations Inspect Tracer validation history in Lerian Console with search, date and decision filters, evaluated rules, and detailed transaction decision panels. The **Validations** page is read-only and focused on investigation. It lists validation decisions produced by Tracer for each evaluated transaction. > [!NOTE] > > You cannot create, edit, or delete validation records from this page. ## Accessing the Validations page *** In the Tracer sidebar, select **Validations**. The page opens as **Transaction Validations** with the helper **What is a Validation?**. ## Available filters *** | Filter | Options | | - | - | | **Search** | Search by ID, reason, or rule name | | **Start date** | Start datetime | | **End date** | End datetime | | **Decision** | All, Approved, Blocked, Review | | **Type** | All Types, Card, PIX, Wire, Crypto | Use **Refresh** to reload the data. ## Main table *** Each row displays: * Validation ID. * Decision label. * Transaction type. * Amount and asset. * Decision reason. * Validation timestamp. * View action. Decision mapping: | Internal value | Console label | | - | - | | `ALLOW` | Approved | | `DENY` | Blocked | | `REVIEW` | Review | ## Validation details panel *** Open a row to see **Transaction Details**: * Hero header with amount and decision. * ID and date. * Transaction type and reason. * Account ID (when present). * Evaluated rules with `Matched` and `Passed` markers. * Processing time in milliseconds. ## Related *** * [Audit and compliance](/en/products/tracer/audit-compliance) - Conceptual guide * [Integration guide](/en/products/tracer/integration-guide) - Validation generation flow * [Retrieve validation](/en/reference/products/tracer/retrieve-validation) - API reference * [List validations](/en/reference/products/tracer/list-validations) - API reference # Tracer module overview Source: https://docs.lerian.studio/en/products/tracer/console/tracer-module Open the Tracer module in Lerian Console: the dashboard shows what Tracer decided over a period, and the module pages manage rules, limits, and validations. **Tracer** is available as a product module in Lerian Console for real-time transaction control. In this module, you manage decision rules, spending limits, and validation history from one operational surface. > [!WARNING] > > Tracer is available only if your organization has purchased the product. Need access? [Contact our team](https://lerian.studio/contact). ## Accessing Tracer *** Open the product launcher in Lerian Console and select **Tracer**. ## Dashboard *** With Tracer 4.1.0 or later, the module opens on the **Dashboard**. The dashboard shows what Tracer decided over a period. With an earlier Tracer version, the module opens on a cover page with links to **Rules**, **Limits**, and **Validations**. Select the period at the top: **Last 7 days**, **Last 30 days**, or **Last 90 days**. The default is the last 30 days. The dashboard shows: * **Transactions Processed**, **Fraud Rate**, and **Avg Decision Time**. * **My Operation**: **Approval Rate**, **Blocked Frauds** with the blocked amount of each asset, **Manual Review Rate**, and **Manual Reviews**. * **Active Rules** and **Active Limits**. * The **Validations per Day** and **Flagged Traffic by Type** charts. * The **Busiest Rules** table. If a dashboard read fails, a notice with a **Retry** button replaces the figures. The period buttons stay available. With Access Manager, the dashboard needs the read permission on the Tracer dashboard resource. ## Available pages *** ### Rules and limits > **[Rules](/en/products/tracer/console/managing-rules)** > > Manage rule logic, decision action, lifecycle status, and optional scope conditions. > **[Limits](/en/products/tracer/console/managing-limits)** > > Configure period-based spending caps with controlled activation and scope filtering. ### Audit and investigation > **[Validations](/en/products/tracer/console/managing-validations)** > > Inspect validation decisions, reason, transaction type, and evaluated rules in detail. > **[Tracer Documentation](/en/products/tracer/what-is-tracer)** > > Access conceptual guides and API references for Tracer. ## Related *** * [What is Tracer](/en/products/tracer/what-is-tracer) - Product overview * [Getting started](/en/products/tracer/getting-started) - Initial setup * [Integration guide](/en/products/tracer/integration-guide) - Connect Tracer to your systems # Creating a rule from a policy Source: https://docs.lerian.studio/en/products/tracer/creating-a-rule-from-a-policy Turn a written policy sentence into a live Tracer rule: map it to fields, create it as a draft, rehearse it on a test account, activate it, and change or retire it later. A compliance officer hands over a sentence: *"decline card-not-present purchases above BRL 5,000 from a device we have not seen before."* This guide turns that sentence into a rule that evaluates the way the policy reads. You rehearse it where it reaches nobody, then put it live. **What changes in your operation:** the policy stops living in a ticket and starts living in an endpoint. The person who wrote the sentence can read the rule back. The rehearsal runs through real evaluation instead of a spreadsheet. Each change to the rule leaves an audit event behind it. > [!TIP] > > **Who is this guide for?** Risk and fraud analysts authoring rules, and the developers who wire the policy fields into the validation request. Steps 1 and 2 are about the policy. Steps 3 to 8 are API calls. ## Before you start *** * [ ] Tracer running and reachable, with an API key. See [Getting started](./getting-started.mdx) * [ ] The CEL (A lightweight expression language for writing business rules, for example, 'if transaction amount > 10000 then REVIEW'. Tracer uses CEL for validation rules.) variables and the scope model. See [Rules engine](./rule-engine.mdx) * [ ] A test account id you can send validations for, that no customer traffic uses * [ ] The policy sentence, written down, with whoever wrote it available for one question All calls below send the API key as `X-API-Key`. *** ## Step 1: Map the sentence onto fields *** A rule reads what the validation request carries. Take the sentence apart clause by clause and put each one in the column it belongs to. | Clause in the policy | Where the value comes from | Reads as | | - | - | - | | "card ... purchases" | `transactionType`, a Tracer enum | `CARD` | | "above BRL 5,000" | `amount` and `asset` on the request | `amount`, `asset` | | "card-not-present" | `subType`, free-form text your integration sets | `subType` | | "a device we have not seen before" | metadata (Additional key-value information attached to entities like accounts or transactions, such as external IDs, reference numbers, or department codes.), which your integration sets | `metadata.deviceFirstSeen` | The first two are fields Tracer defines. The last two are fields your integration has to send. Tracer has no opinion about what "card-not-present" or "new device" mean. That is the question to take back to whoever wrote the policy: *which flag in our payload says the device is new?* > [!NOTE] > > `subType` reaches expressions in lower case, so `"card_not_present"` is the form to compare against. If your integration already uses `subType` for something else, carry the entry mode in `metadata` instead and match on that. Both work the same way in an expression. For the full variable list and the fields each context map carries, see [Rules engine](./rule-engine.mdx#expressions). *** ## Step 2: Split the rule between scope and expression *** Two things in that table (the transaction type and the account) are things Tracer can filter on before an expression runs. Those belong in the rule's `scopes`. The value comparisons belong in the `expression`. **Scope**, which decides *whether the rule is considered at all*: ```json theme={null} "scopes": [{ "transactionType": "CARD" }] ``` **Expression**, which decides *whether the rule fires*: ```cel theme={null} subType == "card_not_present" && amount > 5000 && metadata.deviceFirstSeen == true ``` Read the expression against the sentence: entry mode, threshold, and the device flag. `amount > 5000` is strictly above, so a transaction of exactly `5000.00` does not fire it. Check that against the policy before you go further, because "above" and "from" are different rules. A rule that reads a metadata key the request does not carry does not match, and the other rules still run. So the expression above needs no presence test for `deviceFirstSeen`. See [Rules engine](./rule-engine.mdx#expressions). > [!WARNING] > > Do not repeat scope conditions inside the expression. `transactionType == "CARD"` in both places is not wrong. It leaves two places to edit when the policy changes, and the expression is the one that needs a lifecycle round-trip to edit. *** ## Step 3: Create the rule as a draft *** `POST /v1/rules` creates the rule in `DRAFT`. A draft never reaches evaluation, so nothing you do here reaches traffic. ```bash theme={null} curl -X POST http://localhost:4020/v1/rules \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "Deny CNP card purchases above BRL 5,000 from a new device", "description": "Compliance policy 2026-14, approved 2026-07-20", "expression": "subType == \"card_not_present\" && amount > 5000 && metadata.deviceFirstSeen == true", "action": "DENY", "scopes": [ { "transactionType": "CARD", "accountId": "8a1f2c34-5d6e-4a7b-8c9d-0e1f2a3b4c5d" } ] }' ``` The `accountId` in that scope is your test account. It is what keeps Step 4 off customer traffic. Step 5 takes it out. A `201` answers with the stored rule: ```json theme={null} { "ruleId": "4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68", "name": "Deny CNP card purchases above BRL 5,000 from a new device", "description": "Compliance policy 2026-14, approved 2026-07-20", "expression": "subType == \"card_not_present\" && amount > 5000 && metadata.deviceFirstSeen == true", "action": "DENY", "scopes": [ { "transactionType": "CARD", "accountId": "8a1f2c34-5d6e-4a7b-8c9d-0e1f2a3b4c5d" } ], "status": "DRAFT", "createdAt": "2026-07-31T11:04:12.318Z", "updatedAt": "2026-07-31T11:04:12.318Z" } ``` Tracer preserves the name's case and internal whitespace. It trims leading and trailing whitespace before storage. Rule-name uniqueness is case-sensitive within the context derived from the rule's scopes. So `FraudRule` and `fraudrule` are different names, and the same name can coexist in different contexts. Take the `ruleId` from the response. That is the handle every call below uses. See [Create a rule](/en/reference/products/tracer/create-rule). The expression compiles on this call, so an expression that cannot run never becomes a draft. A syntax error answers `0340`, an expression that does not return a boolean answers `0341`, and one whose estimated cost is above `CEL_COST_LIMIT` answers `0342`. *** ## Step 4: Rehearse it on an account nobody else uses *** The scope you set is what keeps the rehearsal contained. The rule reaches evaluation only for transactions on that one test account. Activation puts it in front of exactly the traffic you send it. **1. Activate the scoped rule** ```bash theme={null} curl -X POST http://localhost:4020/v1/rules/4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68/activate \ -H "X-API-Key: your-secure-api-key" ``` The response comes back with `status: "ACTIVE"` and an `activatedAt`. See [Activate a rule](/en/reference/products/tracer/activate-rule). **2. Send a transaction the policy should decline** ```bash theme={null} TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) curl -X POST http://localhost:4020/v1/validations \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "requestId": "c1a7f402-6b93-4d5e-8f10-2a4c6e8b0d31", "transactionType": "CARD", "subType": "card_not_present", "amount": "7500.00", "asset": "BRL", "transactionTimestamp": "'"$TS"'", "account": { "accountId": "8a1f2c34-5d6e-4a7b-8c9d-0e1f2a3b4c5d", "type": "checking", "status": "active" }, "metadata": { "deviceFirstSeen": true } }' ``` See [Validate a transaction](/en/reference/products/tracer/validate-transaction). **3. Read the decision** ```json theme={null} { "validationId": "b7e3d190-4c25-4e8f-9a16-3d5f7b0c2e41", "requestId": "c1a7f402-6b93-4d5e-8f10-2a4c6e8b0d31", "decision": "DENY", "matchedRuleIds": ["4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68"], "evaluatedRuleIds": ["4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68"], "reason": "Rule matched with DENY action", "totalRulesLoaded": 1, "truncated": false, "limitUsageDetails": [], "processingTimeMs": 6.1, "evaluatedAt": "2026-07-31T11:09:44.207Z" } ``` Your `ruleId` in `matchedRuleIds` means the rehearsal passed. **4. Send the cases that should not fire** Change one value at a time and repeat the call with a fresh `requestId`: `"amount": "5000.00"` for the boundary, `"deviceFirstSeen": false` for a known device, `"subType": "purchase"` for a card-present sale. Each should come back without your `ruleId` in `matchedRuleIds`. > [!WARNING] > > Send a new `requestId` for every attempt. The `requestId` is the idempotency key. Repeat one and Tracer answers `200` with the decision it already recorded for that key. The change you just made then looks like it did nothing. > [!NOTE] > > A rehearsal is a real validation. It stores a decision record and writes an audit event, and an `ALLOW` decision consumes the spending limits that cover that account. That is why the test account matters. If your `ruleId` is not in `matchedRuleIds`, work through it in this order. Is the rule `ACTIVE` (`GET /v1/rules/{id}`)? Does the transaction match the scope you set? Do the values you sent satisfy the expression? *** ## Step 5: Put it live *** Going live means one edit: drop the test account from the scope so the rule applies to the population the policy names. **1. Stop evaluating the rehearsal version** A scope edit does not require `INACTIVE`. Deactivating first makes the switch take effect at a moment you control and records a visible gap in the audit trail. Each instance serves rules from a cache it refreshes on a poll (every 10 seconds by default, `RULE_SYNC_POLL_INTERVAL_SECONDS`). Allow that window for the deactivation to reach every instance. `GET /v1/rules` confirms the stored status, not that every instance has caught up. ```bash theme={null} curl -X POST http://localhost:4020/v1/rules/4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68/deactivate \ -H "X-API-Key: your-secure-api-key" ``` Status goes to `INACTIVE`. See [Deactivate a rule](/en/reference/products/tracer/deactivate-rule). **2. Replace the scope** ```bash theme={null} curl -X PATCH http://localhost:4020/v1/rules/4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68 \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "scopes": [{ "transactionType": "CARD" }] }' ``` `scopes` replaces the whole array. Send every scope object you want the rule to keep. See [Update a rule](/en/reference/products/tracer/update-rule). **3. Activate** ```bash theme={null} curl -X POST http://localhost:4020/v1/rules/4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68/activate \ -H "X-API-Key: your-secure-api-key" ``` Activation reaches the instance that served this call as soon as it commits. When you run several instances behind a load balancer, the others pick the change up on their next rule sync (`RULE_SYNC_POLL_INTERVAL_SECONDS`, default `10`). Deactivation travels the same way. Allow that same window after activation before you treat the rule as enforcing on every instance. **4. Confirm what is live** ```http theme={null} GET /v1/rules?status=ACTIVE&transaction_type=CARD&sort_by=updated_at X-API-Key: {api_key} ``` The listing answers "which rules enforce on card traffic right now". See [List rules](/en/reference/products/tracer/list-rules). For one rule, `GET /v1/rules/{id}` returns the expression and scopes as stored ([Retrieve a rule](/en/reference/products/tracer/retrieve-rule)). Both report the stored state, not what each instance's cache holds. > [!NOTE] > > A validation loads its rule set once, from the cache of the instance that serves it, when the call starts. A rule that activates while a validation is in flight is not part of that decision. Decisions already recorded do not change when rules change afterwards. *** ## Step 6: Know where your rule sits among the others *** Rules carry no priority field and no ordering to configure. Rules whose scope matches a transaction are evaluated together. The decision comes from the strictest action that fired: a `DENY` rule first, then an exceeded spending limit, then `REVIEW`, then `ALLOW`, then the configured no-match default. The `matchedRuleIds` array carries every rule that matched, whatever action each one holds. Two consequences for the rule you just wrote: * **An `ALLOW` rule does not exempt anyone from a `DENY` rule.** If the policy has an exception (VIP customers, a partner merchant), put it inside the `DENY` expression. Add it as one more condition that makes the rule narrower: ```cel theme={null} subType == "card_not_present" && amount > 5000 && metadata.deviceFirstSeen == true && metadata.customerTier != "vip" ``` The narrowed rule now reads `metadata.customerTier`, and a request that does not carry that key does not match it. * **Your rule joins the set every matching transaction evaluates.** `MAX_RULES_PER_REQUEST` caps how many rules one validation evaluates. When the set is larger, the response reports `truncated: true`. See [environment variables](./tracer-environment-variables.mdx). The precedence table and the reasoning behind it are on the [Rules engine](./rule-engine.mdx#evaluation-pattern) page. *** ## Step 7: Change the rule when the policy changes *** What you do depends on the field, not on how the rule performs. | What changed | How | | - | - | | Anything inside the expression (the threshold, the entry mode, the device flag) | Deactivate, then `POST /v1/rules/{id}/draft`, then `PATCH` the expression, then activate | | `DENY` becomes `REVIEW` | `PATCH /v1/rules/{id}` with the new `action` | | Name, description, or scopes | `PATCH /v1/rules/{id}` | > [!WARNING] > > The `expression` accepts an edit only while the rule is `DRAFT`. Sending one to a rule in another status answers `422` with error code `0351`. Deactivating is not enough on its own, because `INACTIVE` is not `DRAFT`. Do not skip `POST /v1/rules/{id}/draft`. See [Draft a rule](/en/reference/products/tracer/draft-rule). A `PATCH` is stored when it answers, and reaches evaluation on the next rule sync. When the change matters to the minute, deactivate first and activate again after. That sequence also puts a visible gap in the audit trail where the rule did not enforce. A reviewer will look for that gap. The lifecycle is a closed set of moves: `DRAFT` activates or is deleted. `ACTIVE` deactivates. `INACTIVE` goes back to `DRAFT`, back to `ACTIVE`, or is deleted. `DELETED` is the end. Anything else answers `422` with error code `0349`, including a request to draft a rule that is still `ACTIVE`. *** ## Step 8: Retire or delete the rule *** **To stop enforcing without losing anything**, deactivate. The rule keeps its expression, its scopes, and its history. It no longer reaches evaluation, and `POST /v1/rules/{id}/activate` brings it back. That is the move for a policy that is suspended, seasonal, or under review. **To remove it**, delete. Deactivate first, because you cannot delete a rule in `ACTIVE`: ```http theme={null} DELETE /v1/rules/4d9c2e70-8b1a-4f36-9c07-5e2a1b3d4f68 X-API-Key: {api_key} ``` A `204` answers on success. See [Delete a rule](/en/reference/products/tracer/delete-rule). What deleting removes: * The rule stops answering on `GET /v1/rules/{id}`, which returns `404` with error code `0347` afterwards. * It no longer appears in `GET /v1/rules`, and `DELETED` is not a value the `status` filter accepts. * `DELETED` is the end of the lifecycle. No endpoint moves a rule out of it. A deleted rule comes back only as a new rule you create again. What deleting leaves behind: * The audit trail keeps the rule's lifecycle, and the `RULE_DELETED` event carries the definition (name, description, expression, action, scopes) as it stood at deletion. See [Audit and compliance](./audit-compliance.mdx). * Decisions the rule produced keep its `ruleId` in `matchedRuleIds`. A denial from six months ago still names it. See [Reviewing a denied transaction](./reviewing-a-denied-transaction.mdx). * The name becomes available again for a new rule in the same context. > [!WARNING] > > Deactivate, then read the audit trail, then delete. Deactivating is reversible in one call and deleting is not reversible at all, so there is no reason to skip the intermediate state. *** ## Common pitfalls *** > [!WARNING] > > **What usually goes wrong turning a policy into a rule:** > > * **"The rule is ACTIVE but matches nothing."** Check the field the policy leans on hardest. A rule reading `metadata.deviceFirstSeen` matches nothing if your integration never sends that key. The rule is correct and the payload is incomplete. > * **"It fired on a transaction the policy exempts."** An `ALLOW` rule does not override a `DENY`. Put the exemption inside the `DENY` expression (Step 6). > * **"My second rehearsal returned the first decision."** `requestId` is the idempotency key. Send a new UUID per attempt. > * **"PATCH rejected my expression with 422."** The rule was not in `DRAFT`. Move it there first (Step 7). > * **"The name I sent is not the name I get back."** Tracer trims only leading and trailing whitespace. It preserves case and internal whitespace. Reference the rule by `ruleId`. ### Error codes | Code | Status | What to change | | - | - | - | | `0340` | 400 | The expression does not parse as CEL | | `0341` | 400 | The expression does not return a boolean (`amount > 5000`, not `amount`) | | `0342` | 422 | The expression's estimated cost is above `CEL_COST_LIMIT` | | `0347` | 404 | No rule has that `ruleId`, or it was deleted | | `0349` | 422 | The lifecycle does not allow that move (for example, deleting an `ACTIVE` rule) | | `0351` | 422 | An expression edit on a rule that is not `DRAFT` | | `0353` / `0355` / `0357` | 400 | `name`, `expression`, or a valid `action` is missing | | `0354` / `0356` / `0359` | 400 | `name` above 255, `expression` above 5000, or `description` above 1000 characters | | `0358` | 400 | A scope object with no field set. Omit `scopes` entirely for a global rule. Never send `{}` | | `0360` | 400 | More than 100 scope objects on one rule | | `0441` | 409 | Another rule in the same context already holds that name | | `0065` | 400 | The id in the path is not a UUID | | `0082` | 400 | A filter on `GET /v1/rules` carries a value the endpoint does not accept | The complete list is in the [Tracer error list](/en/reference/products/tracer/tracer-error-list). *** ## Quick reference *** | Step | Method | Endpoint | | - | - | - | | Create as a draft | POST | `/v1/rules` | | Start evaluating | POST | `/v1/rules/{id}/activate` | | Rehearse or verify | POST | `/v1/validations` | | Stop evaluating | POST | `/v1/rules/{id}/deactivate` | | Reopen for editing | POST | `/v1/rules/{id}/draft` | | Change fields | PATCH | `/v1/rules/{id}` | | Read one rule | GET | `/v1/rules/{id}` | | See what is live | GET | `/v1/rules` | | Remove | DELETE | `/v1/rules/{id}` | # Getting started with Tracer Source: https://docs.lerian.studio/en/products/tracer/getting-started Set up Tracer with Docker Compose, learn its core validation contexts, and run your first ALLOW, DENY, or REVIEW transaction validation call. Tracer is the layer your authorization or onboarding system calls before a transaction goes through. It runs your fraud, risk, and limit policies in milliseconds and returns ALLOW, DENY, or REVIEW. The decision then lives in one place instead of scattered across product code. **What changes in your operation:** decision logic stops living in scattered `if` statements across services. Rule changes ship through an API the same day instead of in the next release. Validation history gives you one place to investigate why a transaction received its decision. **Trade-off:** you add one HTTP call to the critical path of every transaction. The target is p99 under 80ms. In return, you get a single point for policy and decision history, and you remove duplicated logic from product code. > [!TIP] > > **Who is this guide for?** Developers (junior or senior) integrating Tracer for the first time. If you're evaluating Tracer at a product or strategy level, start with [What is Tracer](./what-is-tracer.mdx). If you already have it running and need API mechanics, jump to the [Tracer API quick start](/en/reference/products/tracer/tracer-api-quick-start). This guide walks you through setting up Tracer and running your first validation. In a few steps, you'll have a working environment ready to validate transactions in real time. For step-by-step API instructions with request and response examples, see the [Tracer API quick start](/en/reference/products/tracer/tracer-api-quick-start). ## Why use Tracer *** * **Real-time validation**: Make ALLOW/DENY/REVIEW decisions in under 80ms (p99) * **Flexible rules**: Expression-based rule engine for custom business logic * **Spending control**: Configure limits by account, portfolio, segment, and period * **Validation history**: Stored decisions for investigation and reporting * **Product-agnostic**: Supports any transaction type (Card, Wire, Pix, Crypto) By the end of this guide, you will: * Understand Tracer architecture and core concepts * Have a working development environment * Run your first transaction validation * Configure a spending limit *** ## What is Tracer *** Tracer is a transaction validation platform that evaluates rules and limits and returns instant decisions. Your system calls Tracer before it executes a transaction. It then acts on the decision (ALLOW, DENY, or REVIEW) according to your business logic. ### How it works ![How Tracer processes a validation request across its Validation, Rules, and Limits contexts and returns an ALLOW, DENY, or REVIEW decision](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/how-tracer-works.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=27f03ce51f24a3a9e5b85cd6d6f40882) Figure 1. How Tracer works In this flow: * **Rules** evaluate expressions against the transaction context * **Limits** check spending thresholds for applicable scopes * **Decision** returns ALLOW, DENY, or REVIEW based on evaluation results ### Core contexts Tracer has three bounded contexts: 1. **Validation Context** - Orchestrates requests, coordinates evaluation, and stores validation history 2. **Rules Context** - Manages rule definitions and expression evaluation 3. **Limits Context** - Manages spending limits and usage tracking *** ## Prerequisites *** Before you start, make sure you have: * [ ] **Docker** and **Docker Compose** installed * [ ] **Go 1.27+** for local development (the repository's `go.mod` declares the exact toolchain version) * [ ] **PostgreSQL 17** (the shared Midaz primary, started by the platform infrastructure compose, not by Tracer's own) * [ ] **API Key** for authentication ### Infrastructure dependencies Tracer requires the following components: | Component | Version | Purpose | | - | - | - | | PostgreSQL | 17 | Data persistence. Tracer uses its own `tracer` database on the shared Midaz PostgreSQL primary; it does not ship a dedicated instance. | ### Ports Default ports used by Tracer services: | Service | Port | Description | | - | - | - | | Tracer API | 4020 | Main REST API | | Reservation seam | 4021 | gRPC service the Midaz ledger calls to reserve limit capacity | | PostgreSQL | 5701 | Shared Midaz PostgreSQL primary, as exposed by the shipped infrastructure example (`DB_PORT`) | *** ## Step 1: Set up the environment *** You can run Tracer with Docker Compose or locally for development. ### Option A: Docker Compose (recommended) > [!NOTE] > > Tracer's own Compose file declares only two services: the application and a one-shot migration runner. **PostgreSQL is not one of them**. It comes from the shared platform infrastructure Compose and must be healthy first. The application container starts only after the migration runner applies the schema and exits successfully. The service always boots against an already-migrated database. > [!NOTE] > > In Midaz v4, Tracer ships in the Midaz repository and release. It still runs as its own service. Start from `components/tracer` for local development. Navigate to the Tracer project directory and start the services: ```bash theme={null} cd components/tracer # Setup environment cp .env.example .env # Start all services (brings up the shared infrastructure first, # then the migration runner, then Tracer) make up ``` ### Option B: Local run For development, you can run Tracer locally: ```bash theme={null} # Set environment variables export DB_HOST="localhost" export DB_NAME="tracer" export API_KEY="your-secure-api-key" export API_KEY_ENABLED="true" export SERVER_PORT="4020" export LOG_LEVEL="INFO" # Start the service go run cmd/app/main.go ``` ### Essential environment variables | Variable | Description | Example | | - | - | - | | `DB_HOST` | PostgreSQL host | `localhost` | | `DB_NAME` | PostgreSQL database name | `tracer` | | `API_KEY` | API Key for authentication | `your-secure-api-key` | | `API_KEY_ENABLED` | Enable API Key authentication | `true`, `false` | | `SERVER_PORT` | API port | `4020` | | `LOG_LEVEL` | Log level | `INFO`, `DEBUG`, `ERROR` | *** ## Step 2: Authenticate to the API *** Tracer supports API key and plugin authentication. Plugin authentication takes precedence when you enable both, except on endpoints configured as API-key-only. | Deployment and authentication configuration | Auth header | When to use | | - | - | - | | **Self-hosted, plugin auth enabled** | `Authorization` header with a Bearer token | `PLUGIN_AUTH_ENABLED=true`, unless the endpoint is configured as API-key-only. | | **Self-hosted, API key auth** | `X-API-Key` header | Plugin auth is disabled, or the endpoint is configured as API-key-only. | | **Lerian Cloud** | `Authorization` header with a Bearer token | Always. The JWT is issued by [Access Manager](/en/platform/access-manager) and carries the `tenantId` claim. | The remaining steps use the API key form because most local-development setups run that way. If plugin authentication applies to your request, replace `X-API-Key: your-secure-api-key` with `Authorization: Bearer $JWT` in every example. ### API Key Include the API Key in the `X-API-Key` header: ```http theme={null} GET /v1/rules X-API-Key: your-secure-api-key ``` ### Bearer JWT Include the JWT issued by Access Manager in the `Authorization` header: ```http theme={null} GET /v1/rules Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` Tracer extracts the `tenantId` claim from the JWT and routes the request to the correct tenant database. **You never pass the tenant identifier in a header, path, body, or rule scope**. The token is the only source of truth. ### cURL example ```bash theme={null} # API key: List rules curl -H "X-API-Key: your-secure-api-key" \ http://localhost:4020/v1/rules ``` ```bash theme={null} # Bearer JWT: List rules curl -H "Authorization: Bearer $JWT" \ https://tracer.sandbox.lerian.net/v1/rules ``` > [!WARNING] > > Keep API Keys and JWTs secure. Never expose them in client-side code or public repositories. > [!WARNING] > > API key authentication is **disabled by default** (`API_KEY_ENABLED=false`). The provided `.env.example` keeps it off, so local development works without setup. A production deployment **must** set `API_KEY_ENABLED=true` or `PLUGIN_AUTH_ENABLED=true` before you expose the service. *** ## Step 3: Configure a spending limit *** Spending limits control transaction amounts by scope and period. Create a limit using `POST /v1/limits`. ### Limit types | Type | Description | Period counting | | - | - | - | | `DAILY` | Maximum amount per day | A new count starts each day at the reset time (00:00 UTC by default) | | `WEEKLY` | Maximum amount per week | A new count starts each Monday at the reset time (00:00 UTC by default) | | `MONTHLY` | Maximum amount per month | A new count starts on the 1st of the month at the reset time (00:00 UTC by default) | | `CUSTOM` | Maximum amount for a custom date range | One count for the whole range | | `PER_TRANSACTION` | Maximum per single transaction | No count is kept | For detailed configuration of all limit types including reset times, time windows, and custom periods, see the [Spending limits guide](./spending-limits.mdx). ### Scopes Apply limits to specific contexts: * **Segment**: Apply to all accounts in a segment (e.g., corporate customers) * **Portfolio**: Apply to accounts in a portfolio * **Account**: Apply to a specific account * **Transaction type**: Apply only to CARD, WIRE, PIX, or CRYPTO ### Create a limit ```bash theme={null} curl -X POST http://localhost:4020/v1/limits \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "Daily Corporate Card Limit", "description": "Daily spending limit for corporate card transactions", "limitType": "DAILY", "maxAmount": "50000.00", "asset": "BRL", "scopes": [ { "segmentId": "550e8400-e29b-41d4-a716-446655440000", "transactionType": "CARD" } ] }' ``` ### Activate a limit ```bash theme={null} curl -X POST http://localhost:4020/v1/limits/{id}/activate \ -H "X-API-Key: your-secure-api-key" ``` ### Limit lifecycle Limits start in `DRAFT` status and follow the lifecycle `DRAFT` → `ACTIVE` → `INACTIVE`. Inactive limits can return to `DRAFT` for editing, or you can delete them. Activate a limit to start enforcement. For the full lifecycle and transition rules, see the [Spending limits guide](./spending-limits.mdx). ### Monitor usage Every `POST /v1/validations` response carries `limitUsageDetails`, with one entry per limit Tracer checked. Each entry holds the cap, the amount attempted, and the projected consumption of that cap's current period. That projection includes this transaction. The endpoint `GET /v1/limits/{id}/usage` reports a cumulative total across the limit's counters, for a review of overall consumption. For detailed configuration options, see the [Spending limits guide](./spending-limits.mdx). *** ## Step 4: Validate your first transaction *** With limits configured, you're ready to validate a transaction using `POST /v1/validations`. ### Submit a transaction for validation Send a validation request with the transaction context including: * Transaction details (type, amount, asset, timestamp) * Account information * Optional: segment, portfolio, merchant, and custom metadata (Additional key-value information attached to entities like accounts or transactions, such as external IDs, reference numbers, or department codes.) ```bash theme={null} TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) curl -X POST http://localhost:4020/v1/validations \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "requestId": "550e8400-e29b-41d4-a716-446655440104", "transactionType": "CARD", "subType": "credit", "amount": "1500.00", "asset": "BRL", "transactionTimestamp": "'"$TS"'", "account": { "accountId": "550e8400-e29b-41d4-a716-446655440100" }, "merchant": { "merchantId": "550e8400-e29b-41d4-a716-446655440103", "category": "5411", "name": "Test Merchant" }, "metadata": { "channel": "mobile" } }' ``` > [!NOTE] > > The `transactionTimestamp` must be recent, which is why the example generates it. Tracer rejects a future timestamp with error code `0419` (1-minute clock skew tolerance). Tracer rejects a timestamp older than 24 hours with error code `0421`. > [!NOTE] > > `requestId` is the idempotency key. Send a new UUID for each attempt. If you repeat one, Tracer returns the decision it already recorded for that key. A rule you activated in between will not appear to take effect. Tracer evaluates the rules and limits that apply to the transaction, then returns one of three decisions: | Decision | Meaning | Your system should | | - | - | - | | `ALLOW` | Transaction approved | Proceed with the transaction | | `DENY` | Transaction denied (rule matched or limit exceeded) | Block the transaction | | `REVIEW` | Requires manual review | Queue for human review | The response names the rules Tracer evaluated, the rules that matched, and the current limit usage. That detail helps with debugging and customer support. > [!NOTE] > > **Why Tracer returns a decision instead of blocking directly.** Tracer is a decisioning layer, not an authorization gateway. The calling system holds the customer relationship and knows the channel. It decides what to do with a DENY. For example, your card-issuing system may honor a `DENY` for a stand-in pre-auth. It may still capture the request for analytics. By returning a decision, Tracer fits into any authorization flow without owning the customer-facing UX. For complete payload structure and field details, see the [API reference](/en/reference/products/tracer/validate-transaction). *** ## Step 5: Create a validation rule *** Rules let you define custom business logic that evaluates during validation. Create a rule using the `POST /v1/rules` endpoint with an expression, action, and optional scopes. For example, to block high-value transactions: ```bash theme={null} curl -X POST http://localhost:4020/v1/rules \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "Block high-value card transactions", "description": "Deny card transactions above R$ 10,000", "expression": "amount > 10000", "action": "DENY", "scopes": [ { "transactionType": "CARD" } ] }' ``` > [!NOTE] > > Tracer preserves the name's case and internal whitespace, trimming only leading and trailing whitespace before storage. Take the `ruleId` from the response and use it in the activate call below. ### Activate a rule ```bash theme={null} curl -X POST http://localhost:4020/v1/rules/{id}/activate \ -H "X-API-Key: your-secure-api-key" ``` > [!NOTE] > > Activation takes effect right away on the instance that served the activate call. A single-instance setup evaluates the rule on your next validation. When you run several instances behind a load balancer, the others pick the change up on their next rule sync. The interval is `RULE_SYNC_POLL_INTERVAL_SECONDS`, default `10`. Deactivation propagates the same way. ### Rule lifecycle Rules follow the same lifecycle as limits: `DRAFT` → `ACTIVE` → `INACTIVE`. To start evaluation, activate the rule using `POST /v1/rules/{id}/activate`. You can deactivate and reactivate an active rule as needed. For detailed information about rule expressions and lifecycle management, see the [Rules engine guide](./rule-engine.mdx). *** ## Observability *** Tracer exposes endpoints for monitoring and observability. ### Key metrics Tracer exposes OpenTelemetry-compatible metrics via the OTLP exporter, plus custom application metrics: * `tracer_auth_failures_total{reason}` - Authentication failures by reason (missing\_api\_key, invalid\_api\_key) * `tracer_validation_rollback_failures_total` - Usage rollback failures during REVIEW decisions (eventual consistency gaps that self-correct at period boundaries) Tracer's built-in OpenTelemetry HTTP middleware provides standard HTTP request metrics automatically. *** ## Verification *** Confirm that everything works correctly. ### Checklist * [ ] Docker services started and healthy * [ ] API Key authentication working * [ ] Spending limit configured * [ ] Test transaction validated successfully * [ ] Rule created and activated *** ## Next steps *** From here, you can explore more advanced features: * **[Integration guide](./integration-guide.mdx)** - Learn how to integrate your authorization system with Tracer * **[Rules engine](./rule-engine.mdx)** - Write validation rules in CEL and manage their lifecycle * **[Spending limits](./spending-limits.mdx)** - Configure and manage spending limits by scope and period * **[Validation history and compliance](./audit-compliance.mdx)** - Query validation history and use it in your compliance processes *** ## Quick reference *** The three flows you'll use most: * **Validate a transaction**: `POST /v1/validations`. See the [Tracer API quick start](/en/reference/products/tracer/tracer-api-quick-start) for the request shape. * **Manage rules**: `/v1/rules` (CRUD + lifecycle endpoints `/activate`, `/deactivate`, `/draft`). See the [Rules engine guide](./rule-engine.mdx). * **Manage limits**: `/v1/limits` (CRUD + lifecycle + `/usage`). See the [Spending limits guide](./spending-limits.mdx). For the full endpoint catalog, request/response schemas, and error codes, see the [API reference](/en/openapi/v3-current/tracer.yaml). ### What your system should do with each decision | Decision | Tracer recommends | Your system should | | - | - | - | | `ALLOW` | Approval | Proceed with the transaction | | `DENY` | Denial | Block the transaction and inform the user | | `REVIEW` | Review | Queue for manual review in your system | > [!NOTE] > > Tracer returns decisions as recommendations. Your system must implement the appropriate action for each decision. # Integration guide Source: https://docs.lerian.studio/en/products/tracer/integration-guide Integrate Tracer with your authorization system: send complete payloads, handle ALLOW, DENY, and REVIEW decisions, and stay within an 80ms latency budget. Integrating Tracer means deciding where in your authorization flow to make the validation call, what data to send, and how to handle the three possible decisions. The pattern is short: your system collects the full transaction context, calls `POST /v1/validations`, acts on ALLOW / DENY / REVIEW, and moves on. Tracer never reaches back into your stack. There are no webhooks or callbacks, and the integration ends with the response. **What changes in your operation:** decisioning moves from in-process logic to an external call. The call is synchronous (request/response, no webhooks), so it sits on the critical path of the transaction. Done well, it adds under 80ms p99 and gives you a single point for policy and validation history. Done poorly (no timeout, no retry strategy, no fallback), it becomes a single point of failure. **Trade-off:** you add a network hop. The contract is simple: idempotent by `requestId`, no callbacks, deterministic three-state response. You must think about timeouts, retries, and what to do if Tracer is unreachable. Most of this guide is about that. > [!TIP] > > **Who is this guide for?** Integration engineers writing the request from your system to Tracer, and architects deciding where the call sits in the flow. Risk and fraud analysts who write rules can skip ahead to the [Rules engine guide](./rule-engine.mdx). Compliance can read the [Audit and compliance guide](./audit-compliance.mdx) instead. This guide covers payload requirements, the integration flow, and practices that keep the validation call inside your latency budget. Tracer sits outside your ledger: it never calls Midaz. Your application orchestrates the two. It calls Tracer to validate, and submits the transaction to Midaz only if the decision is `ALLOW`. Tracer evaluates your configured policies and limits against the context you send, not account balances. The ledger stays the source of truth for what an account holds. > [!NOTE] > > Midaz can also drive Tracer through an optional per-ledger reservation seam. It remains one-way, Ledger → Tracer. The rest of this guide covers the application-orchestrated HTTP pattern. The seam contract appears below. ## Integration overview *** Tracer expects calls from authorization systems (payment gateways, workflow orchestrators, or transaction processors) that need real-time validation decisions. The integration follows a simple request-response pattern: ![Request-response integration where an authorization system calls Tracer for a validation decision and only submits the transaction to Midaz when the decision is ALLOW](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/integration-overview-tracer.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=087d44f66ec751efe4bb38be8c964467) Figure 1. Integration overview with Tracer Tracer does not fetch external data during validation. Your system must provide all context needed for rule evaluation. *** ## Midaz Ledger reservation seam *** This opt-in seam belongs to **Ledger HTTP v2**. Ledger HTTP v1 never invokes it. The Tracer public HTTP API is v1-only and has no reservation operations. Ledger calls the gRPC service `lerian.midaz.reservation.v1.ReservationService`. This service is an internal service-to-service transport, not a public API. Tracer always starts this gRPC listener, on port `4021` by default. To use a different port, set `TRACER_GRPC_PORT` to a listen address in the form `:4021`. On Ledger, set `TRACER_BASE_URL` to the listener address as `host:port`. When `TRACER_BASE_URL` is unset, Ledger makes no reservation call. Ledger calls Tracer only when the per-ledger `tracer.mode` is `advisory` or `enforce`. An unset or `off` mode skips reservations even when you set `TRACER_BASE_URL`. An honored per-call skip also bypasses the seam. The seam has five transitions: | Transition | Ledger action | | - | - | | `Reserve` | Hold limit capacity before the transaction commits. | | `ConfirmByTransaction` | Commit every hold for a transaction. | | `ReleaseByTransaction` | Return every hold for a transaction. | | `ConfirmById` | Commit one hold. | | `ReleaseById` | Return one hold. | ### Deployment order Deploy Tracer from Midaz v4.2.0 before you deploy the Ledger. While a Ledger from v4.2.0 runs, do not roll Tracer back to an earlier version. An earlier Tracer drops the account type, the metadata and the revert flag of each reserve request. ### Rules on a reserve On each `Reserve`, Tracer evaluates your active rules first, with the same evaluator as `POST /v1/validations`. Then it reserves limit capacity. On a reserve, the no-match default does not apply. When no rule matches, the reserve continues to the limit check. A reserve for a revert skips rules and still reserves limit capacity. The reserve request carries the amount, the asset, the ID and type of the first internal source account, the transaction timestamp and scalar metadata values, as strings. It carries no transaction type, segment, portfolio or merchant. ### Outcomes on the Ledger A reservation denial is a successful response, not a transport error. The `advisory` mode never blocks the transaction. The `enforce` mode rejects the transaction before any balance movement: | Tracer outcome | Ledger error in `enforce` mode | | - | - | | A spending limit is exceeded | `0177` | | A matched rule returns DENY | `0535` | | A matched rule returns REVIEW, or a rule cannot be evaluated | `0531` | | Tracer refuses the reserve request | `0532`, for every `failPosture` | | Tracer is unavailable | `0178` when `failPosture` is `closed`. With `open`, the transaction proceeds without a reservation. | With the open posture, the Ledger also proceeds without rule or limit checks when Tracer refuses the Ledger's credentials. The Ledger can also return `0536` for a reserve. The [Midaz error list](/en/reference/products/midaz/error-list) describes each code. Confirm and release failures do not block the transaction. ## Payload-Complete Pattern *** Tracer uses the **Payload-Complete Pattern**. Every request must carry all context required for validation. This design ensures: | Benefit | Description | | - | - | | **Simplicity** | Single request contains everything needed for decision | | **Reliability** | No dependency on external services during validation | | **Flexibility** | Your system controls data freshness and enrichment logic | ### Your responsibilities As the integrating system, you are responsible for: 1. **Enriching the payload** with account, segment, portfolio, and merchant data before calling Tracer 2. **Providing accurate context** for rule and limit evaluation. Tracer cannot fetch missing data 3. **Handling the decision** (ALLOW, DENY, or REVIEW) appropriately in your workflow 4. **Implementing retry logic** if Tracer is temporarily unavailable 5. **Managing review workflows** when Tracer returns `REVIEW`. Tracer does not include case management > [!WARNING] > > Tracer validates what you send. If your payload lacks context (e.g., account status, segment membership), rules that depend on that data cannot evaluate correctly. Always ensure payloads are complete before submission. ### Tracer's responsibilities Tracer is responsible for: 1. **Evaluating rules** against the provided context 2. **Checking limits** against current usage 3. **Storing validation history** for investigation and reporting 4. **Returning decision** with detailed information *** ## Integration flow *** Follow these steps to integrate your system with Tracer. ### Step 1: Prepare the transaction context Before calling Tracer, gather all relevant data from your systems: ![Steps for preparing the transaction context and calling Tracer, from gathering data in your systems to acting on the returned decision](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/integration-flow-tracer.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=d988019ee7d8f486fce76b423b55d941) Figure 2. Preparing transaction context ### Step 2: Call Tracer API Send a POST request to `/v1/validations` with the complete transaction context including: * Transaction details (type, subType, amount, asset, timestamp) * Account information (required) * Optional: segment, portfolio, merchant, and custom metadata (Additional key-value information attached to entities like accounts or transactions, such as external IDs, reference numbers, or department codes.) For complete payload structure and field details, see the [API reference](/en/reference/products/tracer/validate-transaction). ### Step 3: Handle the response Process the decision returned by Tracer: | Decision | Action | | - | - | | `ALLOW` | Proceed with the transaction | | `DENY` | Reject the transaction; show reason to user if appropriate | | `REVIEW` | Queue for manual review in your review system | The response includes the `validationId` for correlation with validation history, details about which rules matched, and current limit usage information. ### Using metadata Metadata allows you to pass custom fields that your rules can evaluate. Use this for context like channel, device information, customer tier, or any business-specific attributes. > [!NOTE] > > Metadata keys must be alphanumeric with underscores only, maximum 64 characters. Maximum 50 entries per request. *** ## Request idempotency *** Validation requests are **idempotent** based on the `requestId` field. If you send the same `requestId` twice, Tracer returns the cached result from the first request instead of reprocessing. | Response code | Meaning | | - | - | | `201 Created` | New validation processed | | `200 OK` | Duplicate request detected; cached result returned | The response body is identical in both cases. Your client should handle both status codes as success. Network timeouts and retries can cause duplicate requests. Without idempotency, a retried request could double-count against limits or create duplicate validation records. The `requestId` ensures exactly-once processing semantics. **Idempotency contract:** * Same `requestId` → Same response (guaranteed) * Different `requestId` → Independent processing (even if transaction data is identical) > [!WARNING] > > Always generate a unique `requestId` (UUID) for each new transaction. Reusing a `requestId` from a previous transaction will return the old result, not process the new transaction. *** ## Authentication *** Tracer supports two authentication modes. You can use them independently or together. ### API key authentication The simplest option. Send your API key in the `X-API-Key` header with every request. | Environment variable | Description | | - | - | | `API_KEY_ENABLED` | Enable API key authentication (default: `false`) | | `API_KEY` | The secret key value | | `API_KEY_ENABLED_ONLY_VALIDATION` | Use API key only for the `/v1/validations` endpoint (default: `false`) | ### Plugin authentication (Access Manager) For enterprise deployments, Tracer can delegate authentication to the [Lerian Access Manager](/en/platform/access-manager/auth-plugin). This enables centralized authentication across all Lerian services. | Environment variable | Description | | - | - | | `PLUGIN_AUTH_ENABLED` | Enable plugin authentication (default: `false`) | | `PLUGIN_AUTH_ADDRESS` | URL of the auth service (default: `http://localhost:4000`) | ### Authentication priority When you enable both modes, Tracer uses this priority: 1. If `PLUGIN_AUTH_ENABLED=true` and the endpoint has no API-key-only flag → Plugin auth 2. If `API_KEY_ENABLED=true` or the endpoint carries the API-key-only flag → API key auth Infrastructure endpoints (health checks, version probe, OpenAPI spec) bypass authentication and are not part of the public `/v1/*` API surface documented in this reference. > [!NOTE] > > You can configure the `/v1/validations` endpoint for API-key-only authentication via `API_KEY_ENABLED_ONLY_VALIDATION=true`. This is useful in high-throughput scenarios where plugin auth adds unacceptable latency. ### Authentication on Lerian Cloud On Lerian Cloud, authentication works like this: * **Every `/v1/*` request must carry a JWT bearer token** issued by [Access Manager](/en/platform/access-manager): `Authorization: Bearer `. * **`tenantId` comes from the JWT claim**, not from a header, path, body, metadata, or rule scope. There is no `X-Tenant-ID` header. The tenant identifier has no effect anywhere other than the token claim. A token that **parses but carries no `sub` claim** returns HTTP 401 and error code `0474` up front. The `sub` claim is what the audit writer uses to attribute the action to a principal. Tracer fails loudly rather than recording the change against a generic system actor. Make sure your Access Manager tokens always carry it. If the service is temporarily at capacity for your tenant, requests return HTTP 503 with error code `0466` and a `Retry-After` header. Back off and retry. *** ## Performance considerations *** Optimize your integration for low latency and high reliability. ### Timeout budget Tracer targets a response in under **80ms (p99)**. Configure your client timeout accordingly: | Configuration | Recommended value | | - | - | | Client timeout | 100ms | | Connection timeout | 50ms | | Read timeout | 100ms | ### Retry strategy Implement retry logic for transient failures: ``` On 5xx error or timeout: - Wait 10ms - Retry once - If still failing, apply fallback policy ``` > [!WARNING] > > Do not retry on 4xx errors. These indicate invalid requests that will fail again. For retries on 5xx/timeout, reuse the same requestId to take advantage of idempotency. ### Fallback behavior Decide what happens when Tracer is unavailable: | Strategy | When to use | | - | - | | **Fail-open** | Allow transaction if Tracer is down (prioritize availability) | | **Fail-closed** | Deny transaction if Tracer is down (prioritize security) | | **Queue for review** | Queue transaction for manual review | Your choice depends on your risk tolerance and business requirements. > [!WARNING] > > **Common integration pitfalls:** > > * **"My retry created a duplicate validation."** Reuse the same `requestId` across retries. Tracer deduplicates by that field. The second call returns the cached result (HTTP 200) without creating another validation. If you generate a fresh UUID on every retry, you defeat idempotency. > * **"My client times out after 30 seconds but Tracer keeps processing."** Tracer respects its own deadlines (\~80ms p99 target). If your client gives up on the call, Tracer still spends that work on a response. Set client timeout aggressively (100ms) and trust the retry path. > * **"Tracer rejects my validation with error code `0421` (timestamp too old) on legitimate transactions."** Default tolerance is 24 hours. Check your server clock and the `transactionTimestamp` you're sending. If you batch-process late, set the timestamp to the actual transaction moment, not the moment you're calling Tracer. > * **"Test transactions show up in production validation history."** Tracer also records validations from test/staging environments calling the same Tracer instance. Use `metadata.environment` (or similar) to tag and filter test traffic if you share Tracer between environments. *** ## Data freshness *** Since you control the payload enrichment, data freshness is your responsibility. Tracer trusts the data you provide and cannot detect stale information. | Data type | Freshness recommendation | Risk if stale | | - | - | - | | Account status | Real-time or near real-time | Transactions on suspended accounts may be allowed | | Segment membership | Can be cached (changes infrequently) | Wrong limits or rules may apply | | Portfolio assignment | Can be cached (changes infrequently) | Incorrect scope matching | | Merchant data | Can be cached with periodic refresh | Risk rules may not trigger correctly | > [!WARNING] > > Stale data leads to incorrect decisions. If you suspended an account but your cache shows it as active, Tracer will allow transactions that it should deny. Your enrichment layer is the source of truth for Tracer. *** ## Date and time format *** All datetime fields must use **RFC3339 format** with mandatory timezone: **Valid formats:** ``` 2026-01-30T10:30:00Z (UTC) 2026-01-30T10:30:00-03:00 (São Paulo timezone) 2026-01-30T00:00:00+00:00 (UTC explicit) ``` **Invalid formats:** ``` 2026-01-30 (date only - rejected) 2026-01-30T10:30:00 (missing timezone - rejected) ``` *** ## Integration checklist *** Before going to production, verify: * [ ] Your API Key is in place and secure * [ ] Each request includes a unique requestId (UUID) * [ ] Client handles both 201 and 200 responses as success * [ ] Your client timeout is 100ms * [ ] Your retry logic covers 5xx errors * [ ] You chose a fallback behavior * [ ] Your payload carries all required fields * [ ] Timestamps use RFC3339 format with timezone * [ ] Asset codes are 1 to 100 uppercase letters * [ ] Your system handles each decision (ALLOW/DENY/REVIEW) * [ ] Your system logs validation IDs for validation-history correlation *** ## Example integration (pseudocode) *** ```python theme={null} def validate_transaction(transaction): # Step 1: Enrich payload payload = { "requestId": generate_uuid(), "transactionType": transaction.type, "amount": transaction.amount, "asset": transaction.asset.upper(), "transactionTimestamp": now_rfc3339(), "account": get_account_context(transaction.account_id), "segment": get_segment_context(transaction.segment_id), "merchant": get_merchant_context(transaction.merchant_id), "metadata": transaction.custom_fields } # Step 2: Call Tracer try: response = http_post( url="https://tracer.example.com/v1/validations", headers={"X-API-Key": API_KEY}, json=payload, timeout_ms=100 ) except Timeout: return apply_fallback_policy() except ServerError: return retry_once_or_fallback() # Step 3: Handle decision if response.decision == "ALLOW": return proceed_with_transaction() elif response.decision == "DENY": return reject_transaction(response.reason) elif response.decision == "REVIEW": return queue_for_manual_review(response.validationId) ``` *** ## Next steps *** * **[Rules engine](./rule-engine.mdx)** - Create rules that evaluate against the context you provide * **[Spending limits](./spending-limits.mdx)** - Configure limits that apply to your transaction scopes * **[Validation history and compliance](./audit-compliance.mdx)** - Query validation history and use it in your compliance processes # Reviewing a denied transaction Source: https://docs.lerian.studio/en/products/tracer/reviewing-a-denied-transaction Read a Tracer DENY or REVIEW decision to find the rule or the limit behind it, retrieve the stored record later, and verify its audit event during an audit. A validation came back `DENY` or `REVIEW` and someone asks why. This guide takes you from the decision Tracer returned to a named rule or a named spending limit. From there it reaches the stored record and the audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) entry you can hand an auditor months later. **What changes in your operation:** the answer to "why was this blocked?" stops being a log search. Each decision arrives with the identifiers of what produced it. The same record answers by id years later, and you can check its audit event against the hash chain. > [!TIP] > > **Who is this guide for?** Developers integrating the validation call, support and disputes teams answering customer questions, and compliance officers preparing evidence. Steps 1 to 4 need only the response you already have. Steps 5 to 8 use the query endpoints. ## Before you start *** * [ ] Tracer running and reachable, with an API key. See [Getting started](./getting-started.mdx) * [ ] A `DENY` or `REVIEW` response to work from, or the `validationId` of one * [ ] Familiarity with what rules and limits do. See the [Rules engine](./rule-engine.mdx) and [Spending limits](./spending-limits.mdx) All calls below send the API key as `X-API-Key`. *** ## Step 1: Read the decision Tracer returned *** `POST /v1/validations` answers with the full decision. A new request answers `201`. A repeated `requestId` answers `200` with the decision Tracer already recorded for that key. ```bash theme={null} TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) curl -X POST http://localhost:4020/v1/validations \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "requestId": "7c3f1b8e-5a2d-4c6b-9e1f-0a4d8b2c6e30", "transactionType": "CARD", "subType": "purchase", "amount": "1500.00", "asset": "BRL", "transactionTimestamp": "'"$TS"'", "account": { "accountId": "550e8400-e29b-41d4-a716-446655440100", "type": "checking", "status": "active" }, "merchant": { "merchantId": "550e8400-e29b-41d4-a716-446655440103", "category": "5411", "country": "BR", "name": "Acme Store" }, "metadata": { "channel": "mobile" } }' ``` A denial produced by a rule looks like this: ```json theme={null} { "validationId": "8f14e45f-ea3b-4c6e-9a1c-2b7d5e0a91c4", "requestId": "7c3f1b8e-5a2d-4c6b-9e1f-0a4d8b2c6e30", "decision": "DENY", "matchedRuleIds": [ "3c8a5b21-9f4d-4e17-8b6a-1d2e3f405162" ], "evaluatedRuleIds": [ "3c8a5b21-9f4d-4e17-8b6a-1d2e3f405162", "a6b7c8d9-0e1f-4a2b-9c3d-4e5f60718293" ], "reason": "Rule matched with DENY action", "totalRulesLoaded": 2, "truncated": false, "limitUsageDetails": [], "processingTimeMs": 9.4, "evaluatedAt": "2026-07-31T14:05:09.481Z" } ``` | Field | What it tells you | | - | - | | `validationId` | The key to this decision. Everything later in this guide starts from it | | `decision` | `ALLOW`, `DENY`, or `REVIEW` | | `reason` | What produced the decision. See [Step 2](#step-2-tell-a-rule-denial-from-a-limit-denial) | | `matchedRuleIds` | Every rule that matched this transaction, whatever action the rule carries | | `evaluatedRuleIds` | The rules Tracer evaluated for this transaction | | `limitUsageDetails` | One entry per spending limit Tracer checked | | `totalRulesLoaded` / `truncated` | How many rules were loaded, and whether `MAX_RULES_PER_REQUEST` cut the set | | `processingTimeMs` / `evaluatedAt` | How long the evaluation took, and when it ran | For the full request and response schema, see [Validate a transaction](/en/reference/products/tracer/validate-transaction). > [!NOTE] > > Store the `validationId` next to your own transaction record. It is the key [Step 5](#step-5-retrieve-the-record-later) takes, and it is also the `resourceId` of the audit event in [Step 7](#step-7-pull-the-audit-event-behind-the-decision). *** ## Step 2: Tell a rule denial from a limit denial *** Read `reason` first. It names what produced the decision, and it tells you which of the next two steps to take. | `reason` | What happened | Go to | | - | - | - | | `Rule matched with DENY action` | A rule whose action is `DENY` matched | [Step 3](#step-3-name-the-rule) | | `limit_exceeded` | The amount would push a spending limit past its ceiling | [Step 4](#step-4-name-the-spending-limit) | | `rule_evaluation_error` | A rule expression failed for this request, so Tracer returned `REVIEW`. `matchedRuleIds` lists the matching `REVIEW` rules, then the rules that failed. A failed rule is absent from `evaluatedRuleIds` | [Step 3](#step-3-name-the-rule) | | `No matching rules found` | No rule matched, so Tracer applied `DEFAULT_DECISION_WHEN_NO_MATCH` | Review that setting on the [environment variables](./tracer-environment-variables.mdx) page | On a `REVIEW` from a matched rule, `reason` carries the same text (`Rule matched with REVIEW action`), and Step 3 reads a review the same way it reads a denial. > [!NOTE] > > On a rule denial, `limitUsageDetails` comes back empty because Tracer stops before the limit check, not because no limit applies to the account. *** ## Step 3: Name the rule *** `matchedRuleIds` lists every rule that matched, whatever action each one carries. A `DENY` rule and an `ALLOW` rule can both appear on the same denial. Retrieve each one and read its `action` to find the rule behind the decision: ```http theme={null} GET /v1/rules/3c8a5b21-9f4d-4e17-8b6a-1d2e3f405162 X-API-Key: {api_key} ``` The rule carries the `name`, `description`, `expression`, `action`, and `scopes` a colleague needs to see why it fired. See [Retrieve a rule](/en/reference/products/tracer/retrieve-rule). Compare `matchedRuleIds` with `evaluatedRuleIds` when the question is the opposite one: "why did my rule *not* fire?". For a rule absent from both lists, start with its status and its scope rather than its expression. > [!TIP] > > A rule deleted since the decision no longer answers on `GET /v1/rules/{id}`. Its history, including who deleted it, stays in the audit trail. See [Audit and compliance](./audit-compliance.mdx). *** ## Step 4: Name the spending limit *** On a `limit_exceeded` denial, `limitUsageDetails` holds one entry per limit Tracer checked, and the entries marked `"exceeded": true` are the ones the amount would push past: ```json theme={null} { "validationId": "8f14e45f-ea3b-4c6e-9a1c-2b7d5e0a91c4", "requestId": "7c3f1b8e-5a2d-4c6b-9e1f-0a4d8b2c6e30", "decision": "DENY", "matchedRuleIds": [], "evaluatedRuleIds": [ "a6b7c8d9-0e1f-4a2b-9c3d-4e5f60718293" ], "reason": "limit_exceeded", "totalRulesLoaded": 2, "truncated": false, "limitUsageDetails": [ { "limitId": "b2d4f6a8-1c3e-4507-9b8d-6f0a2c4e6810", "limitAmount": "50000", "scope": "(segment:2f1c9b7e-40a5-4d18-9c6b-3e7f5a0d1b24,transactionType:CARD)", "period": "DAILY", "currentUsage": "51500", "attemptedAmount": "1500", "exceeded": true }, { "limitId": "d4f6a8b2-3e5c-4709-8b6d-0a2c4e681012", "limitAmount": "400000", "scope": "(account:550e8400-e29b-41d4-a716-446655440100)", "period": "MONTHLY", "currentUsage": "128400", "attemptedAmount": "1500", "exceeded": false } ], "processingTimeMs": 14.2, "evaluatedAt": "2026-07-31T14:05:09.481Z" } ``` Read the exceeded entry as the arithmetic of the denial: `attemptedAmount` against `limitAmount`. The `currentUsage` field reports what that limit's current period and matched scope would hold with this transaction included. In the entry above, a `1500` purchase would take a `50000` daily cap to `51500`. A `PER_TRANSACTION` limit keeps no count, so its entry reports `currentUsage` as `0` and `attemptedAmount` against `limitAmount` is the whole comparison. `GET /v1/limits/{limitId}` gives the limit's current name and configuration. See [Retrieve a limit](/en/reference/products/tracer/retrieve-limit). The decision record keeps the ceiling, period, and scope that applied when Tracer made the decision. A limit changed since then does not change what the record says. For how each period counts and how consumption accumulates, see [Spending limits](./spending-limits.mdx). *** ## Step 5: Retrieve the record later *** Tracer stores every decision under its `validationId`: ```http theme={null} GET /v1/validations/8f14e45f-ea3b-4c6e-9a1c-2b7d5e0a91c4 X-API-Key: {api_key} ``` The record answers with the transaction context Tracer evaluated (`transactionType`, `amount`, `asset`, `transactionTimestamp`, `account`, and the optional `segment`, `portfolio`, `merchant`, and `metadata`), plus the same `decision`, `reason`, `matchedRuleIds`, `evaluatedRuleIds`, and `limitUsageDetails` the original response carried, and a `createdAt`. See [Retrieve a validation](/en/reference/products/tracer/retrieve-validation). Read out this record in a dispute. It holds the input and the outcome in one document. It does not change when rules or limits change afterwards. *** ## Step 6: Find records when you do not have the id *** `GET /v1/validations` lists stored decisions, newest first, with cursor pagination: ```http theme={null} GET /v1/validations?decision=DENY&account_id=550e8400-e29b-41d4-a716-446655440100&start_date=2026-07-01T00:00:00Z&end_date=2026-07-31T23:59:59Z X-API-Key: {api_key} ``` > [!WARNING] > > **A query with no dates covers the last 90 days, not the full retention period.** Tracer applies that default window only when both `start_date` and `end_date` are absent. Send either one, or both, to reach an older range. Two filters answer the questions this guide exists for: * `matched_rule_id={ruleId}`: every stored decision this rule matched * `exceeded_limit_id={limitId}`: every stored decision this limit stopped Each result is a summary: `validationId`, `decision`, `reason`, `amount`, `asset`, `transactionType`, `accountId`, `matchedRuleIds`, `exceededLimitIds`, `processingTimeMs`, and `createdAt`. Take the `validationId` of the one you want and retrieve it with Step 5 for the full record. See [List validations](/en/reference/products/tracer/list-validations) for every filter and the pagination fields. *** ## Step 7: Pull the audit event behind the decision *** The audit trail records the decision under the `validationId` as the event's `resourceId`: ```http theme={null} GET /v1/audit-events?resource_type=transaction&resource_id=8f14e45f-ea3b-4c6e-9a1c-2b7d5e0a91c4 X-API-Key: {api_key} ``` ```json theme={null} { "auditEvents": [ { "hash": "a3f1e2b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2", "previousHash": "b4e2f3a5c6d7e8f9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3", "eventId": "1d0a6f52-7b3c-4e8d-9a01-5c2b6e7f8091", "eventType": "TRANSACTION_VALIDATED", "createdAt": "2026-07-31T14:05:09.492Z", "action": "VALIDATE", "result": "DENY", "resourceId": "8f14e45f-ea3b-4c6e-9a1c-2b7d5e0a91c4", "resourceType": "transaction", "actor": { "actorType": "api_key", "id": "tracer-default", "name": "", "ipAddress": "203.0.113.42" }, "context": { "request": { "requestId": "7c3f1b8e-5a2d-4c6b-9e1f-0a4d8b2c6e30", "transactionType": "CARD", "amount": "1500", "asset": "BRL" }, "response": { "decision": "DENY", "reason": "Rule matched with DENY action", "matchedRuleIds": [ "3c8a5b21-9f4d-4e17-8b6a-1d2e3f405162" ], "evaluatedRuleIds": [ "3c8a5b21-9f4d-4e17-8b6a-1d2e3f405162", "a6b7c8d9-0e1f-4a2b-9c3d-4e5f60718293" ], "totalRulesLoaded": 2, "truncated": false, "limitUsageDetails": [], "processingTimeMs": 9.4 } } } ], "hasMore": false } ``` What the audit event adds to the validation record: | Field | Why it matters in a review | | - | - | | `actor` | Who made the call: the identity behind the request, and the client IP | | `result` | The decision, indexed for filtering, so `result=DENY` scopes a query | | `context.request` | The request snapshot recorded with the decision | | `context.response` | The decision, reason, and rule identifiers as they were returned | | `eventId` | The key for [Step 8](#step-8-verify-the-audit-event-in-an-audit) and for [Retrieve an audit event](/en/reference/products/tracer/retrieve-audit-event) | > [!WARNING] > > The 90-day default window applies here too: `GET /v1/audit-events` with neither `start_date` nor `end_date` covers the last 90 days. Send the range you want when the decision is older than that. The same endpoint carries the lifecycle of rules and limits: who created, activated, or deleted them. See [List audit events](/en/reference/products/tracer/list-audit-events) for the filters, and [Audit and compliance](./audit-compliance.mdx) for the event types and retention periods. *** ## Step 8: Verify the audit event in an audit *** Pass the `eventId` to the verification endpoint: ```http theme={null} GET /v1/audit-events/1d0a6f52-7b3c-4e8d-9a01-5c2b6e7f8091/verify X-API-Key: {api_key} ``` ```json theme={null} { "isValid": true, "totalChecked": 12345, "message": "Hash chain integrity verified successfully" } ``` `isValid: true` establishes that every record from the first one through the one you named still matches the hash stored with it. Each also links to the hash of the record before it. Within that span, no record was removed, re-ordered, or re-dated. `totalChecked` reports how many records the check covered. On a failed check, `isValid` is `false`, `message` reports tampering, and `firstInvalidId` carries an internal sequence number for the diverging record. That number is not an audit-event id, so it is not a value to pass to `GET /v1/audit-events/{id}`. See [Verify an audit event](/en/reference/products/tracer/verify-audit-event). > [!NOTE] > > Hand over the two together: the retrieved event from Step 7 is the content of the decision, and the verification result is the evidence that the chain holding it is intact. The verification call reports on the chain. It does not return the record. *** ## Common pitfalls *** > [!WARNING] > > **What usually goes wrong in a review:** > > * **"My query for last year came back empty."** A query with no `start_date` and no `end_date` covers the last 90 days. Send the range you want. > * **"`matchedRuleIds` has three entries and only one denied."** The array holds every rule that matched, whatever action it carries. Retrieve each rule and read its `action` (Step 3). > * **"`limitUsageDetails` is empty on a denial."** The decision came from a rule, not a limit. Read `reason` (Step 2). > * **"`currentUsage` is higher than what the customer actually spent."** It already includes the attempted amount, so it is a projection. On an exceeded limit the counter stays the same, so the stored consumption does not include this transaction. > * **"The rule that fired no longer exists."** Deleted rules stop answering on `GET /v1/rules/{id}`. Query their lifecycle through `GET /v1/audit-events?resource_type=rule&resource_id={ruleId}`. ### Error codes | Code | Status | What to change | | - | - | - | | `0065` | 400 | The id in the path is not a UUID | | `0432` | 404 | No stored validation has that `validationId` | | `0381` | 404 | No audit event has that `eventId` | | `0077` | 400 | A date is not RFC3339 with a timezone. Send `2026-07-01T00:00:00Z`, not `2026-07-01` | | `0083` | 400 | `end_date` falls before `start_date` | | `0431` | 400 | A filter on `GET /v1/validations` carries a value the endpoint does not accept | | `0080` / `0331` | 400 | `limit` is above 1000, or is not positive | | `0334` | 400 | `cursor` was sent together with `sort_by` or `sort_order`. The cursor already carries them | The complete list is in the [Tracer error list](/en/reference/products/tracer/tracer-error-list). *** ## Quick reference *** | Step | Method | Endpoint | | - | - | - | | Validate a transaction | POST | `/v1/validations` | | Find stored decisions | GET | `/v1/validations` | | Retrieve one decision | GET | `/v1/validations/{id}` | | Name a rule | GET | `/v1/rules/{id}` | | Name a limit | GET | `/v1/limits/{id}` | | Find audit events | GET | `/v1/audit-events` | | Retrieve one audit event | GET | `/v1/audit-events/{id}` | | Verify the hash chain | GET | `/v1/audit-events/{id}/verify` | # Rules engine Source: https://docs.lerian.studio/en/products/tracer/rule-engine Write CEL expressions in the Tracer rules engine to shape ALLOW, DENY, and REVIEW decisions in real time, and manage the DRAFT to ACTIVE lifecycle. The rules engine is what risk and fraud teams use to change how Tracer approves or blocks transactions, without touching application code. **What changes in your operation:** rule changes ship through an API endpoint, not through a release. An analyst can publish a new rule in the morning and see it evaluating real transactions within seconds. Tracer records every match, so you can trace a denied customer call six months later back to the exact rule that fired. **Trade-off:** you have to think in CEL (Common Expression Language) instead of Go, Python, or Java. The learning curve is short (most rules are one line), but the team writing them is no longer your application developers. The upside is no deploys, full audit, and the people closest to the policy own the policy. > [!TIP] > > **This guide is for** risk and fraud analysts who will write rules, developers integrating the validation call, and compliance officers reading the audit trail. The CEL examples get technical further down, but the lifecycle and decision logic are useful for anyone evaluating the product. The **Tracer rules engine** evaluates validation logic written in CEL (Common Expression Language) (A lightweight expression language for writing business rules, for example, 'if transaction amount > 10000 then REVIEW'. Tracer uses CEL for validation rules.), a type-safe expression language from Google. Tracer compiles expressions at rule creation and runs them during every transaction validation. You change behavior by updating rules through the API, without redeploying code. ## Why use the rules engine *** * **Flexibility**: Create and modify rules without code deploys * **Execution model**: Tracer evaluates compiled expressions during validation * **Type safety**: Expression syntax validated at rule creation * **No short-circuit**: Tracer evaluates matching rules together, so the audit trail records the rules that triggered, regardless of the winning category * **Scope-based**: Apply rules to specific segments, accounts, or transaction types By the end of this guide, you will: * Understand rule engine concepts and evaluation flow * Create and test expression-based rules * Manage the rule lifecycle (DRAFT, ACTIVE, INACTIVE, DELETED) * Apply best practices for rule management *** ## What is the rules engine *** The rules engine is the Tracer component responsible for evaluating expressions during transaction validation. It enables fraud analysts and risk managers to configure business logic that executes in real time, without requiring code deployments or engineering support. ### How it works ![How the rules engine evaluates configured expressions against the transaction context during validation and returns a decision](https://mintcdn.com/lerian-49cb71fc/svQbQMf9Iz90q0Cc/images/en/d2/how-rules-works.svg?fit=max\&auto=format\&n=svQbQMf9Iz90q0Cc\&q=85\&s=1ad8112b778cb2db149856477f7718a1) Figure 1. Rules engine evaluation flow In this flow: * **Load rules** fetches active rules from cache * **Evaluate expressions** runs the CEL expression of each rule whose scope matches the transaction * **Collect matches** gathers all rules that matched and determines the decision ### Evaluation pattern **Decision precedence** (highest to lowest): 1. **DENY**: any matching `DENY` rule wins outright. 2. **Limit exceeded**: if no DENY rule matched but the transaction exceeds any applicable limit, the decision is DENY. Rule precedence applies first, and limits come in only when no DENY rule matched. 3. **REVIEW**: if no DENY rule matched and the transaction exceeded no limit, any matching `REVIEW` rule wins. A rule whose expression fails for the request also gives REVIEW, with the reason `rule_evaluation_error`. 4. **ALLOW**: if only `ALLOW` rules matched, the decision is ALLOW. 5. **Default**: if no rule matched at all, a validation returns the configured `DEFAULT_DECISION_WHEN_NO_MATCH` (`ALLOW` unless explicitly set to `DENY`). Tracer accepts only `ALLOW` and `DENY`. `REVIEW` is deliberately not a valid no-match default, and any other value fails the service at boot. When the Midaz ledger reserves a transaction, Tracer runs the same rules with a smaller input. See [Rules on a reserve](/en/products/tracer/integration-guide#rules-on-a-reserve). > [!NOTE] > > **Why DENY beats REVIEW beats ALLOW.** Precedence never changes and you cannot configure it, on purpose. It removes the "which DENY rule wins?" ambiguity at runtime and makes audit trivial. The response always identifies the strictest action that fired. The cost is that you can't write "ALLOW rules that override DENYs". If you need that pattern, the right answer is to make the DENY rule more specific instead. > [!NOTE] > > Tracer returns decisions. It does not block transactions directly. Your system receives the decision and must take the appropriate action (e.g., blocking, allowing, or queuing for review). *** ## Core concepts *** Before creating rules, understand the foundational elements. ### Rules A rule is a unit of business logic composed of: * **Expression** - A type-safe expression that evaluates to true or false * **Action** - What decision to return when the expression is true * **Scopes** - Which transactions the rule applies to * **Status** - The rule's lifecycle state ### Expressions You write expressions in **CEL (Common Expression Language)**, a type-safe language that evaluates transaction context and returns a boolean value (true or false). CEL provides compile-time validation, so syntax errors surface when you create the rule, not when Tracer processes transactions. Example expressions: ``` amount > 10000 ``` ``` segment.segmentId == "high-risk-segment-uuid" && amount > 5000 ``` ``` merchant["category"] == "7995" ``` (`merchant.category` is the 4-digit ISO 18245 MCC code. `"7995"` is the MCC for betting/casino. Tracer accepts both `merchant.category` and `merchant["category"]`. The production examples use bracket notation by convention. If you need to match on a string label like `"gambling"`, store it in `metadata` and match on that instead.) Expressions read the validation request through ten variables. For the field types and formats behind each one, see the [ValidationRequest schema](/en/reference/products/tracer/validate-transaction) in the API reference. | Variable | Type | What you typically match on | | - | - | - | | `amount` | number | The transaction value, in the same units you send in the request: `"1500.00"` reaches an expression as `1500`. Safe range is `±2^53`. | | `transactionType` | string | One of `CARD`, `WIRE`, `PIX`, `CRYPTO`. | | `subType` | string | Free-form, lower case (e.g., `"international"`, `"debit"`). Empty string when not provided. | | `asset` | string | 1 to 100 uppercase letters (e.g., `"BRL"`). | | `transactionTimestamp` | int | Transaction time in Unix nanoseconds. Divide by `1000000000` for seconds. | | `account` | map | `accountId`, `type`, `status`, `metadata`. | | `segment` | map | `segmentId`, `name`, `metadata`. | | `portfolio` | map | `portfolioId`, `name`, `metadata`. | | `merchant` | map | `merchantId`, `name`, `category`, `country`, `metadata`. | | `metadata` | map | Custom fields your integration passes in the request payload. | Field values worth knowing before you write a condition: * `account.status` and `account.type` are free text, up to 50 and 256 characters. Your expression receives them as you send them. * `merchant.category` takes a 4-digit ISO 18245 MCC code. `merchant.country` takes an ISO 3166-1 alpha-2 code. * Those four fields are optional on the request. A field the request omits reaches your expression as an empty string, so a condition that tests it for a specific value is false. * `segment.segmentId`, `portfolio.portfolioId`, `account.accountId`, and `merchant.merchantId` are UUID strings. > [!NOTE] > > `segmentId` and `portfolioId` live on the top-level `segment` and `portfolio` variables, not on `account`. To match by segment, write `segment.segmentId == "..."`, not `account.segmentId == "..."`. > > When presence itself is the condition you want, write `size(segment) > 0` or `"risk_score" in metadata`. > [!NOTE] > > Tracer bounds expression cost with `CEL_COST_LIMIT` (default `10000`). The check runs at compile time (on create, on expression update, and again on activate), not only at activation. Tracer rejects an expression whose worst-case estimated cost exceeds the limit, the first time you submit it, with error code `0342` (cost limit exceeded). Syntax errors surface as `0340`, type errors (including an expression that does not return a boolean) as `0341`, and a failure to estimate cost at all as `0345`. ### Expression examples by use case Here are practical examples by business scenario: #### Amount-based rules ```cel theme={null} // Block transactions above a threshold amount > 10000 // Block high-value international transfers transactionType == "WIRE" && subType == "international" && amount > 50000 // Review large cryptocurrency transactions transactionType == "CRYPTO" && amount > 5000 ``` #### Merchant-based rules ```cel theme={null} // Block gambling merchants merchant.category == "7995" // Block high-risk merchant categories merchant.category in ["7995", "5967", "5966"] // Review transactions from new merchant countries merchant.country != "BR" && amount > 1000 ``` #### Account-based rules ```cel theme={null} // Block suspended accounts account.status == "suspended" // Review transactions from newly created accounts metadata.accountAgeDays < 30 && amount > 500 // Block closed accounts account.status == "closed" ``` #### Combined conditions ```cel theme={null} // High-value transaction from high-risk segment segment.segmentId == "high-risk-segment-uuid" && amount > 5000 // International Pix above threshold transactionType == "PIX" && subType == "international" && amount > 10000 // Large card transaction to foreign merchant transactionType == "CARD" && merchant.country != "BR" && amount > 3000 ``` #### Time-based rules ```cel theme={null} // Review late-night card transactions above BRL 5,000 transactionType == "CARD" && amount > 5000 && timestamp(transactionTimestamp / 1000000000).getHours("UTC") >= 22 // Review weekend transactions above BRL 10,000 timestamp(transactionTimestamp / 1000000000).getDayOfWeek("UTC") in [0, 6] && amount > 10000 ``` #### Using metadata ```cel theme={null} // Block transactions from untrusted devices metadata.deviceTrust == "untrusted" // Review first-time purchases above threshold metadata.isFirstPurchase == true && amount > 1000 // Block transactions outside business hours (using metadata) metadata.isBusinessHours == false && amount > 5000 // VIP customers bypass certain restrictions metadata.customerTier == "vip" && amount < 50000 ``` > [!NOTE] > > Your integration provides metadata fields. Design your payload to include the context your rules need. ### Actions Actions determine the decision when an expression evaluates to true: | Action | Description | | - | - | | `ALLOW` | Allow the transaction | | `DENY` | Deny the transaction | | `REVIEW` | Route to manual review | ### Scopes Scopes define which transactions a rule applies to. A rule with no `scopes` is **global** and evaluates against every transaction. A rule with one or more scope objects evaluates only when the transaction matches at least one of them (OR semantics across scope objects). Within a single scope object, the supported fields are: * `segmentId` - Match transactions from a specific segment * `portfolioId` - Match transactions from a specific portfolio * `accountId` - Match transactions from a specific account * `merchantId` - Match transactions to a specific merchant * `transactionType` - Match specific transaction types (CARD, WIRE, PIX, CRYPTO) * `subType` - Match specific subtypes (debit, credit, instant, etc.) **Matching semantics:** * **Within one scope object:** fields combine with AND. A field you leave out works as a wildcard (matches any value). You must set at least one field. Tracer rejects empty scope objects (`{}`) with error code `0358`. * **Across multiple scope objects on the same rule:** they combine with OR. The rule matches if any scope object matches the transaction. For example, a rule with two scopes (one targeting `transactionType: CARD` and another targeting `transactionType: PIX`) runs for both card and Pix transactions. A single scope with both `segmentId` AND `accountId` requires the transaction to match the segment AND the account. *** ## Rule lifecycle *** Rules progress through a defined lifecycle to ensure safe deployment. ![Lifecycle of rules and limits in Tracer, showing the status transitions a definition moves through from creation to active enforcement](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/rules-limits-lifecycle-tracer.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=0e9996cc0609ad9f3b8d0ee10feb3a56) Figure 2. Rules lifecycle and status transitions ### States | State | Description | | - | - | | `DRAFT` | Not evaluated; expression can be modified freely | | `ACTIVE` | Evaluated during validations; expression is immutable | | `INACTIVE` | Not evaluated; preserved for audit trail; can be reactivated. The expression is still immutable in this state. To edit it, move the rule back to DRAFT via `POST /v1/rules/{id}/draft`. | | `DELETED` | Soft-deleted; not returned by listings and cannot be recovered through the API, but the row is preserved in the database for audit trail. | ### Transitions | Transition | From | To | Description | | - | - | - | - | | `activate` | DRAFT, INACTIVE | ACTIVE | Start evaluation (validates expression) | | `deactivate` | ACTIVE | INACTIVE | Stop evaluation | | `draft` | INACTIVE | DRAFT | Re-edit a previously deactivated rule before reactivating | | `delete` | DRAFT, INACTIVE | DELETED | Permanent removal (cannot delete ACTIVE rules) | > [!NOTE] > > You must deactivate active rules before deletion. This prevents accidental removal of rules that Tracer still evaluates. *** ## Create a rule *** Create rules using `POST /v1/rules`. Tracer creates rules in `DRAFT` status by default. A rule requires: * **name**: A descriptive name, unique within its context. The rule's scopes decide the context (the lowest `segmentId` across them), and rules with no scope share a single global context. So the same rule name can coexist across two different segments, but not twice inside one. Comparison is case-sensitive and preserves internal whitespace, and Tracer trims leading and trailing whitespace before storage. A collision returns `409 Conflict` with error code `0441`. Reference the rule by the `ruleId` in the response. * **expression**: A CEL expression that evaluates to true or false * **action**: The decision to return when the expression matches (ALLOW, DENY, or REVIEW) * **scopes** (optional): Limit which transactions the rule applies to For complete payload structure and field details, see the [API reference](/en/reference/products/tracer/create-rule). *** ## Activate and deactivate rules *** After creating a rule, activate it to start evaluation. Deactivate rules to stop evaluation without deleting them. | Operation | Endpoint | Description | | - | - | - | | Activate | `POST /v1/rules/{id}/activate` | Start evaluating this rule | | Deactivate | `POST /v1/rules/{id}/deactivate` | Stop evaluating (preserves for audit) | > [!NOTE] > > Deactivating a rule preserves it for audit purposes. Use delete only when you want to permanently remove a rule. *** ## List and query rules *** Query rules for management and auditing using `GET /v1/rules`. ### Query parameters | Parameter | Type | Description | | - | - | - | | `name` | string | Filter by name (case-insensitive partial match) | | `status` | string | Filter by status (DRAFT, ACTIVE, INACTIVE). `DELETED` is not a valid filter value. | | `action` | string | Filter by action (ALLOW, DENY, REVIEW) | | `account_id` | UUID | Filter by scope: account ID | | `segment_id` | UUID | Filter by scope: segment ID | | `portfolio_id` | UUID | Filter by scope: portfolio ID | | `merchant_id` | UUID | Filter by scope: merchant ID | | `transaction_type` | string | Filter by scope: transaction type (CARD, WIRE, PIX, CRYPTO) | | `sub_type` | string | Filter by scope: subtype (e.g., debit, credit) | | `limit` | integer | Items per page (default: 10, max: 100) | | `cursor` | string | Pagination cursor from previous response | | `sort_by` | string | Sort field: `created_at`, `updated_at`, `name`, `status` (default: `created_at`) | | `sort_order` | string | Sort direction: `ASC`, `DESC` (default: `DESC`) | ### Get a specific rule Use `GET /v1/rules/{id}` to retrieve the full rule definition including expression and scopes. *** ## Update a rule *** > [!WARNING] > > The `expression` field is immutable in ACTIVE and INACTIVE states. Deactivating a rule is not enough. To edit an expression, move the rule from ACTIVE → INACTIVE (`POST /v1/rules/{id}/deactivate`), then from INACTIVE → DRAFT (`POST /v1/rules/{id}/draft`). Only DRAFT rules accept expression updates. Once edited, reactivate with `POST /v1/rules/{id}/activate`. *** ## Delete a rule *** Delete rules you no longer need. You can delete only DRAFT and INACTIVE rules. Deactivate ACTIVE rules first. ```http theme={null} DELETE /v1/rules/{id} X-API-Key: {api_key} ``` > [!WARNING] > > Deletion is permanent. You cannot recover deleted rules, and they do not appear in any listings. *** ## Best practices *** Follow these practices for effective, maintainable rules. ### Naming * **Use descriptive names** - The name should clearly state what the rule does * **Include context** - Mention the scenario or transaction type * **Avoid abbreviations** - Prefer clarity over brevity | Less clear | More clear | | - | - | | `Rule 1` | `Block night transactions above BRL 5,000` | | `Block high` | `Deny high-value weekend transactions` | | `Pix rule` | `Review Pix transfers to new recipients` | ### Expression design * **Keep expressions simple** - Complex logic is harder to maintain * **Use scopes for filtering** - Don't repeat scope conditions in expressions * **Test edge cases** - Consider boundary values and null fields ### Lifecycle management * **Start in DRAFT** - Test before activating * **Return to DRAFT before editing the expression** - The expression is immutable in ACTIVE and INACTIVE. Move the rule to DRAFT via `POST /v1/rules/{id}/draft` to edit, then reactivate * **Archive unused rules** - Keep audit trail intact * **Delete only when certain** - Deletion is permanent ### Monitoring * **Review matched rules** - Check which rules trigger * **Monitor DENY rates** - High deny rates may indicate overly aggressive rules * **Audit regularly** - Ensure rules still align with business requirements > [!WARNING] > > **Common pitfalls when working with rules:** > > * **"I edited the expression but the change didn't take effect."** The expression is immutable in ACTIVE and INACTIVE states. Move the rule back to DRAFT via `POST /v1/rules/{id}/draft`, edit, then reactivate. INACTIVE alone is not enough. > * **"My rule is ACTIVE but another Tracer instance isn't evaluating it yet."** Activation takes effect right away on the instance that served the activate call. When you run several instances behind a load balancer, the others pick the change up on their next rule sync (`RULE_SYNC_POLL_INTERVAL_SECONDS`, default `10`). Deactivation propagates the same way. Plan integration tests around that gap when the calls can land on different instances. > * **"I want to delete an ACTIVE rule."** You can't. Call `POST /v1/rules/{id}/deactivate` first, then `DELETE /v1/rules/{id}`. This forces a visible step where the rule stops affecting traffic before it disappears from listings. > * **"My empty scope `{}` gets rejected with error code `0358`."** Every scope object must have at least one field set. To run a rule globally (against every transaction), omit the `scopes` array entirely. Don't pass `{}`. *** ## Quick reference *** Key endpoints, actions, and status information. ### Endpoints | Operation | Method | Endpoint | | - | - | - | | Create rule | POST | `/v1/rules` | | List rules | GET | `/v1/rules` | | Get rule | GET | `/v1/rules/{id}` | | Update rule | PATCH | `/v1/rules/{id}` | | Delete rule | DELETE | `/v1/rules/{id}` | | Activate rule | POST | `/v1/rules/{id}/activate` | | Deactivate rule | POST | `/v1/rules/{id}/deactivate` | | Draft rule | POST | `/v1/rules/{id}/draft` | ### Statuses | Status | Evaluated | Editable | Can delete | | - | - | - | - | | `DRAFT` | No | Yes | Yes | | `ACTIVE` | Yes | Partial (expression immutable) | No (deactivate first) | | `INACTIVE` | No | Partial (expression immutable; return to DRAFT first) | Yes | | `DELETED` | No | No | N/A | # Setting up a spending limit Source: https://docs.lerian.studio/en/products/tracer/setting-up-a-spending-limit Cap what an account, a portfolio, or a segment can spend in a period with Tracer, activate the cap, and read how much of it is left from the validation decision. A product or risk decision has landed as a sentence: "no more than R\$ 20,000 per account per month." This guide turns that sentence into a live cap, and shows where to read how much of it a customer has left. **What changes in your operation:** the ceiling moves from a constant compiled into a service to a stored definition you create, activate, raise, and stop. Every decision that touches it reports what it consumed. > [!TIP] > > **This guide is for** product and risk teams setting the ceiling, and developers wiring `POST /v1/validations` into the payment path. Steps 1 and 2 are decisions to make before any call. Steps 3 to 6 are the calls. ## Before you start *** * [ ] Tracer running and reachable, with an API key. See [Getting started](./getting-started.mdx) * [ ] The id of the account, portfolio, or segment the cap applies to * [ ] The asset code for the transactions you want capped * [ ] Familiarity with limit types, time windows, and custom periods. See [Spending limits](./spending-limits.mdx) All calls below send the API key as `X-API-Key` and run against `http://localhost:4020`. *** ## Step 1: Decide what the cap is addressed to *** A limit carries a list of scope objects, and each object names what the limit applies to. These fields are available inside one object: | Field | Applies the limit to | | - | - | | `accountId` | Transactions on one account | | `portfolioId` | Transactions on one portfolio | | `segmentId` | Transactions on one segment | | `merchantId` | Transactions to one merchant | | `transactionType` | One of `CARD`, `WIRE`, `PIX`, `CRYPTO` | | `subType` | One transaction subtype, such as `purchase` (matched without regard to case) | Fields inside one object combine with AND. A field you leave out is a wildcard. Across objects the list combines with OR, so a limit with two objects applies when either one matches. The id you name decides the answer to "R\$ 20,000 per account": **One account, its own budget** ```json theme={null} "scopes": [ { "accountId": "550e8400-e29b-41d4-a716-446655440100" } ] ``` The cap applies to that account. It counts that account's spending and no one else's. For a per-account ceiling across a book of customers, each account gets its own limit. **One segment, one shared budget** ```json theme={null} "scopes": [ { "segmentId": "2f1c9b7e-40a5-4d18-9c6b-3e7f5a0d1b24" } ] ``` The cap applies to the segment as a whole. Every account in that segment draws down the same R\$ 20,000 (the first customers to spend consume it for the rest). **Narrowed to one rail** ```json theme={null} "scopes": [ { "accountId": "550e8400-e29b-41d4-a716-446655440100", "transactionType": "PIX" } ] ``` The cap counts only that account's Pix transactions. Its card and wire traffic passes without touching this limit. > [!NOTE] > > A limit also carries an `asset`, and Tracer checks a limit against a transaction only when the two match. Tracer does not check a limit created in `BRL` against a `USD` transaction. A book that settles in two currencies needs a limit for each. A limit must carry at least one scope object, and each object must set at least one field. Tracer rejects an empty list or an empty object. See [What goes wrong](#what-goes-wrong). *** ## Step 2: Choose the period *** `limitType` decides both the size of the ceiling's window and when a new count starts. Tracer offers five: | `limitType` | The count covers | A new count starts | | - | - | - | | `DAILY` | One day, UTC | Each day at the reset time (00:00 UTC by default) | | `WEEKLY` | One week, UTC | Each Monday at the reset time (00:00 UTC by default) | | `MONTHLY` | One month, UTC | On the 1st at the reset time (00:00 UTC by default) | | `CUSTOM` | A date range you set | Not at all. One count spans the whole range | | `PER_TRANSACTION` | A single transaction | No count is kept; each transaction is measured on its own | "per month" is `MONTHLY`. > [!WARNING] > > **The boundary is UTC, not local time.** A São Paulo customer spending at 21:30 on 31 July is at 00:30 UTC on 1 August. Without `resetTime`, that amount lands in the August count, not July's. To move the boundary, set `resetTime` in `HH:MM` UTC when you create a `DAILY`, `WEEKLY`, or `MONTHLY` limit. For example, `"03:00"` starts each period at midnight in São Paulo (UTC-3). You cannot change `resetTime` after creation. See [Reset time](./spending-limits.mdx#reset-time). `CUSTOM` takes `customStartDate` and `customEndDate` and is the shape for a campaign or a promotion. Those two fields belong to `CUSTOM`. Tracer rejects them on the other four types. To restrict a cap to certain hours of the day instead, see [time windows](./spending-limits.mdx#time-windows). *** ## Step 3: Create the limit *** `POST /v1/limits` stores the definition. Tracer creates it in `DRAFT` and does not check it against transactions yet. ```bash theme={null} curl -X POST http://localhost:4020/v1/limits \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "Monthly Account Spending Cap", "description": "Caps total monthly outflow for one account", "limitType": "MONTHLY", "maxAmount": "20000.00", "asset": "BRL", "scopes": [ { "accountId": "550e8400-e29b-41d4-a716-446655440100" } ] }' ``` A successful call answers `201`. The body carries the stored limit. Two fields matter now: | Field | What to do with it | | - | - | | `limitId` | The id every later call in this guide takes. Keep it | | `status` | `DRAFT`, so Tracer does not check the limit yet. [Step 4](#step-4-activate-it) changes that | > [!NOTE] > > **Names are unique across limits, and the character set is narrow.** `name` accepts ASCII letters and digits, spaces, and `-`, `_`, `.`, `(`, `)`. Tracer rejects an accent, or a dash typed as an em dash, with `0371`. Tracer rejects a name another live limit already holds, with `0442`. Deleting a limit frees its name again. For the full payload and every optional field, see [Create a limit](/en/reference/products/tracer/create-limit). *** ## Step 4: Activate it *** Tracer does not check a `DRAFT` limit. Activation is what puts it in the path: ```bash theme={null} curl -X POST http://localhost:4020/v1/limits/{limitId}/activate \ -H "X-API-Key: your-secure-api-key" ``` The response carries the limit with `status` set to `ACTIVE`. From here, Tracer checks `POST /v1/validations` whose transaction matches the scope and asset against this limit. > [!NOTE] > > **Activating mid-month does not import the month's history.** Tracer counts a transaction against a limit at the moment it decides on it. Spending that happened while the limit was `DRAFT` is not in the count. A cap activated on the 20th governs what happens from the 20th onward. See [Activate a limit](/en/reference/products/tracer/activate-limit). *** ## Step 5: Read how much is left *** Every `POST /v1/validations` response carries `limitUsageDetails`, with one entry per limit Tracer checked: ```json theme={null} { "limitId": "b2d4f6a8-1c3e-4507-9b8d-6f0a2c4e6810", "limitAmount": "20000", "scope": "(account:550e8400-e29b-41d4-a716-446655440100)", "period": "MONTHLY", "currentUsage": "20000", "attemptedAmount": "1500", "exceeded": false } ``` | Field | What it reports | | - | - | | `limitAmount` | The ceiling that was checked | | `currentUsage` | Projected consumption of that limit's current period and matched scope if this transaction is allowed | | `attemptedAmount` | The amount measured against the ceiling | | `period` | The limit's type (`MONTHLY` here) | | `scope` | The limit's scope as text, each object in parentheses, several objects joined by `OR` | | `exceeded` | Whether the amount would push this cap past its ceiling | In the entry above, a R\$ 1,500 purchase took an 18,500 month to exactly 20,000 and spent the cap. Tracer denies the next transaction on that account until August. When a transaction exceeds a limit, the decision is `DENY` and `reason` is `limit_exceeded`. Tracer does not add to the count the amount that would have crossed the ceiling. A limit produces a denial, not a flag for review. To go from such a denial back to the limit that caused it, see [Reviewing a denied transaction](./reviewing-a-denied-transaction.mdx). ### The other reading `GET /v1/limits/{limitId}/usage` answers with a total for the limit rather than for one period. It adds up the usage counters recorded against it, across the periods and scopes it has accumulated. Use it to review how much a limit has absorbed overall, not to answer how much a customer has left this month. See [Retrieve a limit usage snapshot](/en/reference/products/tracer/retrieve-limit-usage). *** ## Step 6: Change it, pause it, remove it *** **1. Raise or lower the ceiling** ```bash theme={null} curl -X PATCH http://localhost:4020/v1/limits/{limitId} \ -H "X-API-Key: your-secure-api-key" \ -H "Content-Type: application/json" \ -d '{ "maxAmount": "30000.00" }' ``` `name`, `description`, `maxAmount`, and `scopes` are editable, and so are the time-window and custom-period fields. `limitType`, `asset`, and `resetTime` are not. Tracer rejects a request that sends any of them with `0380`, and a different period, asset, or reset time means a new limit. Tracer rejects an empty body with `0183`. See [Update a limit](/en/reference/products/tracer/update-limit). Changing the ceiling does not clear the running count. Lower it below what the current period already consumed, and Tracer denies the account until the next period starts. **2. Stop enforcing it** ```bash theme={null} curl -X POST http://localhost:4020/v1/limits/{limitId}/deactivate \ -H "X-API-Key: your-secure-api-key" ``` The limit moves to `INACTIVE` and Tracer stops checking it. It keeps its definition and its place in the audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.), and `POST /v1/limits/{limitId}/activate` puts it back in the path. See [Deactivate a limit](/en/reference/products/tracer/deactivate-limit). **3. Take it back to draft** ```bash theme={null} curl -X POST http://localhost:4020/v1/limits/{limitId}/draft \ -H "X-API-Key: your-secure-api-key" ``` Moves an `INACTIVE` limit back to `DRAFT`. See [Return a limit to draft](/en/reference/products/tracer/draft-limit). **4. Remove it** ```bash theme={null} curl -X DELETE http://localhost:4020/v1/limits/{limitId} \ -H "X-API-Key: your-secure-api-key" ``` Answers `204`. You have to deactivate a limit that is currently `ACTIVE` first. Tracer rejects the deletion with `0363`, which is what keeps a live ceiling from disappearing by accident. See [Delete a limit](/en/reference/products/tracer/delete-limit). ### Find the limits you already have `GET /v1/limits` lists them, and filters by the same scope fields you set in Step 1: ```http theme={null} GET /v1/limits?status=ACTIVE&account_id=550e8400-e29b-41d4-a716-446655440100&limit_type=MONTHLY X-API-Key: {api_key} ``` `name`, `status`, `limit_type`, `account_id`, `segment_id`, `portfolio_id`, `merchant_id`, `transaction_type`, and `sub_type` all filter. Results are cursor-paginated. See [List limits](/en/reference/products/tracer/list-limits), and [Retrieve a limit](/en/reference/products/tracer/retrieve-limit) for one by id. *** ## What goes wrong *** > [!WARNING] > > **What usually goes wrong when you set up a cap:** > > * **"The customer overspent and nothing stopped them."** Check `status` first. Tracer does not check a limit in `DRAFT` or `INACTIVE`. Then check that the limit's `asset` matches the transaction's, and that the scope names the id the transaction actually carried. > * **"The segment cap ran out on day two."** A segment-scoped limit is one budget for the whole segment, not one per account in it. A per-account ceiling means a limit addressed to each account. > * **"The month rolled over a few hours early."** Without `resetTime`, periods start at 00:00 UTC. Spending after 21:00 in UTC-3 belongs to the next UTC day, and on the last day of the month, to the next month. Create the limit with a `resetTime` to move the boundary. > * **"I raised the limit and the account is still denied."** Raising the ceiling does not clear the count. Confirm the new `maxAmount` is above what the period already consumed. > * **"`GET /v1/limits/{id}/usage` reports more than the customer spent this month."** That endpoint totals the counters recorded for the limit, across periods and scopes. For the current period, read `limitUsageDetails` on the validation response. ### Error codes | Code | Status | What to change | | - | - | - | | `0009` | 400 | A field failed validation. `detail` names it, for example `scopes must have at least 1 item(s)`, `scope at index 0 must have at least one field set`, or `maxAmount must be greater than or equal to 0` | | `0065` | 400 | The id in the path is not a UUID | | `0183` | 400 | The `PATCH` body carried no editable field | | `0362` | 404 | No limit has that id | | `0363` | 422 | The transition is not allowed (deleting an `ACTIVE` limit, for instance) | | `0366` | 400 | `asset` carries a character that is not a letter | | `0371` | 400 | `name` carries a character the field does not accept | | `0380` | 422 | The request tried to change `limitType`, `asset`, or `resetTime` | | `0440` | 400 | `resetTime` is not in `HH:MM` format | | `0442` | 409 | Another live limit already holds that name | | `0537` | 400 | `resetTime` was set on a `CUSTOM` or `PER_TRANSACTION` limit | | `0538` | 400 | `resetTime` falls inside the limit's time window | The complete list is in the [Tracer error list](/en/reference/products/tracer/tracer-error-list). *** ## Quick reference *** | Step | Method | Endpoint | | - | - | - | | Create a limit | POST | `/v1/limits` | | Activate it | POST | `/v1/limits/{id}/activate` | | Read consumption with the decision | POST | `/v1/validations` | | Read the limit's cumulative total | GET | `/v1/limits/{id}/usage` | | Change the ceiling | PATCH | `/v1/limits/{id}` | | Stop enforcing it | POST | `/v1/limits/{id}/deactivate` | | Take it back to draft | POST | `/v1/limits/{id}/draft` | | Remove it | DELETE | `/v1/limits/{id}` | | Find limits | GET | `/v1/limits` · `/v1/limits/{id}` | # Spending limits Source: https://docs.lerian.studio/en/products/tracer/spending-limits Configure Tracer spending limits by account, portfolio, or segment with daily, weekly, monthly, and custom periods, time windows, and lifecycle controls. Spending limits are how product and risk teams cap exposure per customer, per segment, or per portfolio without writing code. Common use cases: a daily ceiling on card spending for retail customers, a monthly cap on a specific MCC, a campaign-window limit for a marketing promotion. **What changes in your operation:** spending caps no longer live as constants hardcoded in config files or scattered across services. They become versioned data with a clear lifecycle (DRAFT → ACTIVE → INACTIVE). Each new period starts counting from zero, and they're audit-trailed every time a transaction would have pushed past one. **Trade-off:** counters need to stay consistent across replicas and races. Tracer handles that transactionally. If Tracer denies a transaction or sends it to REVIEW, the counter rolls back. You give up "local clever logic in each service" and gain a single, consistent number. > [!TIP] > > This guide is for product managers configuring caps, risk teams reviewing exposure, compliance auditing what Tracer denied, and developers integrating the validation call. The Limit types section assumes no API knowledge. The lifecycle and PATCH sections assume basic REST. **Spending limits** in Tracer let you control transaction amounts by scope (account, portfolio, segment) and period (daily, weekly, monthly, custom, or per-transaction). Tracer evaluates limits in real-time alongside rules, in the same `POST /v1/validations` call. ## Why use spending limits *** * **Customer protection**: Detect overspending and return DENY decisions for unauthorized large transactions * **Risk management**: Monitor exposure per account, segment, or portfolio * **Flexible scoping**: Apply limits at different granularity levels * **Period counting**: Daily, weekly, and monthly limits start a new count at each period boundary * **Time windows**: Restrict limit enforcement to specific hours of the day * **Custom periods**: Define date-bound limits for campaigns, promotions, or compliance requirements By the end of this guide, you will: * Understand limit types, time windows, and scoping options * Create and configure spending limits with period-based controls * Monitor limit usage in real-time * Manage the limit lifecycle *** ## Core concepts *** Understand the building blocks of spending limits. ### Limit types Tracer supports five types of spending limits: | Type | Description | Period counting | | - | - | - | | `DAILY` | Maximum amount per day | A new count starts each day at the reset time (00:00 UTC by default) | | `WEEKLY` | Maximum amount per week | A new count starts each Monday at the reset time (00:00 UTC by default) | | `MONTHLY` | Maximum amount per month | A new count starts on the 1st of the month at the reset time (00:00 UTC by default) | | `CUSTOM` | Maximum amount within a user-defined date range | One count for the whole range; from `customEndDate` onward Tracer stops checking the limit | | `PER_TRANSACTION` | Maximum amount per single transaction | No count is kept; each transaction is checked on its own | ### Reset time `resetTime` moves the start of each `DAILY`, `WEEKLY`, and `MONTHLY` period to another time of day, in `HH:MM` format (24-hour, UTC). Without it, each period starts at 00:00 UTC. Set it when you create the limit. You cannot change it later. Tracer rejects `resetTime` on a `CUSTOM` or `PER_TRANSACTION` limit with error code `0537`. When the limit has a time window, `resetTime` must not fall inside the window. It can equal `activeTimeStart` or `activeTimeEnd`. A reset time inside the window returns error code `0538`. ### Time windows Time windows restrict when Tracer enforces a limit during the day. When a transaction occurs outside the configured time window, Tracer skips the limit and does not enforce it. The transaction proceeds without counting against that limit. * **Format**: `HH:MM` (24-hour, UTC) * **Both fields required**: If you set `activeTimeStart`, you must also set `activeTimeEnd` (and vice versa) * **Half-open interval**: Start is inclusive, end is exclusive `[start, end)` * **Overnight windows supported**: Setting `activeTimeStart: "20:00"` and `activeTimeEnd: "06:00"` creates a window from 8 PM to 6 AM UTC > [!NOTE] > > You can apply time windows to any limit type (DAILY, WEEKLY, MONTHLY, CUSTOM, or PER\_TRANSACTION). Without a time window, the limit is active 24/7. **Example: Pix compliance** A financial institution needs to enforce lower Pix transfer limits during nighttime hours. The Pix nighttime window runs from 20:00 to 06:00 in Brasília time (UTC-3), which is 23:00 to 09:00 UTC: * `limitType`: `DAILY` * `maxAmount`: `"1000.00"` * `activeTimeStart`: `"23:00"` * `activeTimeEnd`: `"09:00"` * `resetTime`: `"09:00"` * Scope: Pix transactions Tracer checks transactions between 23:00 and 09:00 UTC against the R\$ 1,000 limit. This limit does not affect transactions outside this window. `resetTime: "09:00"` starts each daily count when the window closes. Without it, the count resets at 00:00 UTC (21:00 in Brasília), in the middle of the window. A customer can then spend the full R\$ 1,000 before 21:00 Brasília time and again after it. > [!WARNING] > > Upgrade every Tracer pod to Midaz v4.2.0 or later before you create a limit with `resetTime`. A pod on an earlier version accepts the request but drops the field. Until every pod runs v4.2.0, an earlier pod still resets the count at 00:00 UTC. ### Custom periods Custom periods define a date range during which a limit is active. This is useful for campaigns, promotions, seasonal events, or compliance requirements with specific date boundaries. * **Required fields**: `customStartDate` and `customEndDate` (only for `CUSTOM` type) * **Half-open interval**: Start is inclusive, end is exclusive `[start, end)` * **Maximum duration**: 5 years * **Cannot be in the past**: The `customEndDate` must not be entirely before the current date > [!WARNING] > > The `customStartDate` and `customEndDate` fields are required for `CUSTOM` limits and forbidden for other limit types. **Example: Black Friday campaign** A retailer wants to set a special spending limit for the Black Friday period: * `limitType`: `CUSTOM` * `maxAmount`: `"100000.00"` * `customStartDate`: `"2026-11-25T00:00:00Z"` * `customEndDate`: `"2026-11-30T00:00:00Z"` * Scope: CARD transactions in the retail segment Usage accumulates across the whole window in a single count. From `customEndDate` onward, Tracer stops checking the limit. ### Combining time windows and custom periods You can use time windows and custom periods together on `CUSTOM` limits. Tracer then checks a transaction against the limit only when it falls within both the custom period and the time window. For example, take a `CUSTOM` limit with `customStartDate` Nov 25 to `customEndDate` Nov 30 and a time window of `09:00` to `18:00`. Tracer enforces that limit only during business hours within the Black Friday period. ### Scopes Scopes define which transactions a limit applies to. Unlike rules, every limit must have at least one scope object. Limits cannot be global. Within a single scope object, the supported fields are: * `segmentId` - Apply to transactions from a specific segment * `portfolioId` - Apply to transactions from a specific portfolio * `accountId` - Apply to transactions from a specific account * `merchantId` - Apply to transactions to a specific merchant * `transactionType` - Apply to specific transaction types (CARD, WIRE, PIX, CRYPTO) * `subType` - Apply to a specific transaction subtype (e.g., `debit`, `credit`) **Matching semantics:** * **Within one scope object:** fields combine with AND. A field you leave out works as a wildcard (matches any value). You must set at least one field. Tracer rejects empty scope objects (`{}`) with error code `0009`. * **Across multiple scope objects on the same limit:** they combine with OR. The limit applies if any scope object matches the transaction. **No hierarchy between limits.** A transaction can match multiple limits, for example an account-level and a segment-level limit. Tracer then checks all applicable limits independently in a single transaction. If the transaction exceeds any applicable limit, Tracer denies it. ### Usage tracking For `DAILY`, `WEEKLY`, `MONTHLY`, and `CUSTOM` limits, Tracer keeps one usage counter per limit, per matched scope, per period. The validation decision reports that counter. See [Read consumption](#read-consumption). For `DAILY`, `WEEKLY`, and `MONTHLY` counters, `expiresAt` is 90 days after the period reset. For `CUSTOM` counters, it is 90 days after `customEndDate`. Expired counters are eligible for cleanup. Physical deletion requires an enabled cleanup worker that runs successfully. *** ## How limits work *** Tracer evaluates limits during validation requests. ### Limit check flow ![How Tracer checks all applicable spending limits during a validation request and updates their usage counters](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/how-limits-works.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=edfe5fa47c5158e9f86f97d7dcc9326b) Figure 1. How spending limits work 1. **Find limits** - Query all active limits matching the transaction scope 2. **Check time window** - If the limit has a time window configured, verify the current server time falls within `activeTimeStart`/`activeTimeEnd`. If outside, Tracer skips the limit (it does not use the client-supplied `transactionTimestamp` here) 3. **Check custom period** - If the limit is `CUSTOM`, verify the current server time falls within `customStartDate`/`customEndDate`. If outside, Tracer skips the limit (again, it does not use `transactionTimestamp`) 4. **Calculate projected usage** - Add transaction amount to current usage 5. **Compare threshold** - Check if projected usage exceeds limit amount 6. **Return result** - If the transaction exceeds any applicable limit, or any DENY rule matches, Tracer returns a DENY decision (your system should then block the transaction) > [!NOTE] > > Limit checks and counter increments are transactional. If Tracer denies a transaction (by limits or rules) or flags it for review, it rolls back all counter increments atomically. This prevents limit leakage from partial operations. > [!NOTE] > > When Tracer skips a limit during evaluation, `limitUsageDetails[i]` includes `skipped: true` and a `skipReason` field with one of two values: > > * `"outside_time_window"`: current server time is outside the limit's `activeTimeStart`/`activeTimeEnd` window > * `"outside_custom_period"`: current server time is outside the limit's `customStartDate`/`customEndDate` range > > Tracer reports skipped limits for transparency, but they do not participate in the DENY decision, and their counters do not increment. The window check uses server time, not the client-supplied `transactionTimestamp`, to prevent timestamp-manipulation attacks. > [!NOTE] > > **Why server time instead of `transactionTimestamp`.** The client can set `transactionTimestamp` to whatever they want. That includes a value crafted to fall inside an active window when the real transaction would fall outside it. If Tracer trusted the client clock for time-window enforcement, anyone with access to the payload could bypass off-hours limits. Pinning the window check to Tracer's own clock removes that attack surface. The downside is that small clock drift between Tracer pods can cause edge-case skips around the window boundary. ### Example scenario A corporate segment has a daily limit of R\$ 50,000 (`"50000.00"`) for CARD transactions. If current usage is R$ 45,000 and a new transaction of R$ 8,000 arrives: * Projected usage: R$ 45,000 + R$ 8,000 = R\$ 53,000 * Limit: R\$ 50,000 * Result: Tracer returns DENY decision (your system should block the transaction) *** ## Create a limit *** Create limits using `POST /v1/limits`. Tracer creates limits in `DRAFT` status by default. A limit requires: * **name**: A descriptive name (e.g., "Daily Corporate Card Limit") * **limitType**: DAILY, WEEKLY, MONTHLY, CUSTOM, or PER\_TRANSACTION * **maxAmount**: Maximum amount as a decimal value (e.g., `"50000.00"`). It cannot be negative. `"0"` is accepted and denies every transaction in the limit's scope, which blocks an account without deactivating the limit * **asset**: Asset code of 1 to 100 uppercase letters (e.g., BRL, USD) * **scopes**: At least one scope to define which transactions it applies to Optional fields: * **activeTimeStart**: Start of the daily time window in `HH:MM` format (e.g., `"09:00"`) * **activeTimeEnd**: End of the daily time window in `HH:MM` format (e.g., `"17:00"`) * **customStartDate**: Start date for `CUSTOM` limits (ISO 8601 timestamp, required for CUSTOM) * **customEndDate**: End date for `CUSTOM` limits (ISO 8601 timestamp, required for CUSTOM) * **resetTime**: Start of each `DAILY`, `WEEKLY`, or `MONTHLY` period in `HH:MM` format (e.g., `"09:00"`) > [!NOTE] > > Limit names must be globally unique across all non-deleted limits, unlike rule names, which are unique only within their scope context. Tracer enforces uniqueness on the name exactly as stored, after it trims leading and trailing whitespace. The comparison is case-sensitive and does not collapse whitespace inside the name, so `Daily Card Limit` and `daily card limit` are two distinct, both-acceptable limits. Deleting a limit frees its name for reuse. A collision returns `409 Conflict` with error code `0442`. For complete payload structure and field details, see the [API reference](/en/reference/products/tracer/create-limit). *** ## List and query limits *** Query limits for management and auditing using `GET /v1/limits`. ### Query parameters | Parameter | Type | Description | | - | - | - | | `name` | string | Filter by name (case-insensitive partial match) | | `status` | string | Filter by status (DRAFT, ACTIVE, INACTIVE) | | `limit_type` | string | Filter by limit type (DAILY, WEEKLY, MONTHLY, CUSTOM, PER\_TRANSACTION) | | `account_id` | string | Filter by scope: account ID | | `segment_id` | string | Filter by scope: segment ID | | `portfolio_id` | string | Filter by scope: portfolio ID | | `merchant_id` | string | Filter by scope: merchant ID | | `transaction_type` | string | Filter by scope: transaction type (CARD, WIRE, PIX, CRYPTO) | | `sub_type` | string | Filter by scope: subtype (e.g., debit, credit) | | `limit` | integer | Items per page (default: 10, max: 100) | | `cursor` | string | Pagination cursor | | `sort_by` | string | Sort field: `created_at`, `updated_at`, `name`, `max_amount` | | `sort_order` | string | Sort direction: `ASC`, `DESC` (default: DESC) | ### Get a specific limit Use `GET /v1/limits/{id}` to retrieve the full limit definition including scopes and current status. *** ## Read consumption *** ### From the decision Every successful `POST /v1/validations` response, `201` or `200`, carries `limitUsageDetails`. Each entry reports: * **limitId** and **limitAmount**: which cap Tracer checked, and its ceiling * **currentUsage**: the projected consumption of that cap's current period and matched scope if Tracer allows this transaction * **attemptedAmount**: the amount checked against the cap * **exceeded**: whether the attempted amount would push this cap past its ceiling. Tracer evaluates every cap, so more than one entry can carry `exceeded: true`, and any of them produces the DENY ### From the limit `GET /v1/limits/{id}/usage` reports a cumulative total. Its `currentUsage` adds up the usage counters recorded for the limit, across periods and scopes. Use it to review a limit's overall consumption rather than to answer how much a customer has left in the current period. On a long-running limit, this total covers only the periods still retained, not the limit's full lifetime. *** ## Update a limit *** Update limits using `PATCH /v1/limits/{id}`. The `limitType`, `asset`, and `resetTime` fields are immutable. You cannot change them after creation. > [!WARNING] > > Changing the limit amount does not clear the current count. If you reduce a limit below what the current period already consumed, Tracer denies subsequent transactions until the next period starts. *** ## Limit lifecycle *** Limits follow the same lifecycle as rules: ![Lifecycle of rules and limits in Tracer, showing the status transitions a definition moves through from creation to active enforcement](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/rules-limits-lifecycle-tracer.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=0e9996cc0609ad9f3b8d0ee10feb3a56) Figure 2. Spending limits lifecycle ### States | State | Description | | - | - | | `DRAFT` | Limit created but not active; can be modified freely | | `ACTIVE` | Limit is checked during validations | | `INACTIVE` | Limit is not checked; preserved for audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.); can be reactivated | ### Transitions | Operation | From | To | Description | | - | - | - | - | | Create | - | DRAFT | Limits are created in DRAFT status by default | | Activate | DRAFT, INACTIVE | ACTIVE | Start checking this limit | | Deactivate | ACTIVE | INACTIVE | Stop checking this limit | | Draft | INACTIVE | DRAFT | Return to draft for editing | *** ## Best practices *** Recommendations for effective limit management. ### Naming * **Be descriptive** - Include the scope and type in the name * **Use consistent patterns** - e.g., "Daily Limit" | Less clear | More clear | | - | - | | `Limit 1` | `Daily Corporate Card Limit` | | `VIP limit` | `Monthly VIP Pix Limit` | | `BF promo` | `Custom Black Friday Card Limit` | ### Scope design * **Start broad, refine as needed** - Begin with segment-level limits, add account-level for exceptions * **Avoid overlapping scopes** - Multiple limits on the same scope can cause confusion * **Use transaction types** - Different payment methods may need different limits ### Time window design * **Use for regulatory compliance** - BACEN nighttime Pix limits are a common use case * **Consider timezone impact** - Time windows use UTC. Account for your users' local timezone offset * **Combine with custom periods** - Use time windows inside custom periods for precise campaign controls ### Monitoring * **Review denied transactions** - High denial rates may indicate limits are too restrictive * **Adjust seasonally** - Consider temporary limit increases during high-spending periods or use `CUSTOM` limits for specific date ranges > [!WARNING] > > **Common pitfalls when working with limits:** > > * **"My customer is reporting overspend. They should have hit the limit."** Check whether the limit is `ACTIVE`. Tracer does not evaluate a limit in DRAFT or INACTIVE state. Also confirm the limit's scope actually matches the transaction (segment, transaction type, etc.). > * **"My PATCH lowered the limit but transactions are still denied."** Lowering the limit does not clear the count. If the current period already consumed more than the new ceiling, Tracer denies subsequent transactions until the next period starts. > * **"I tried to delete an ACTIVE limit and it was rejected."** The deletion comes back `422` with code `0363`. Send `POST /v1/limits/{id}/deactivate` first, then `DELETE /v1/limits/{id}`. This is intentional: it prevents accidentally removing a live enforcement. > * **"`GET /v1/limits/{id}/usage` reports more than the customer spent this period."** That endpoint totals the usage counters recorded for the limit, across periods and scopes. For the current period, read `limitUsageDetails` on the validation response. *** ## Quick reference *** Key endpoints and configuration options. ### Endpoints | Operation | Method | Endpoint | | - | - | - | | Create limit | POST | `/v1/limits` | | List limits | GET | `/v1/limits` | | Get limit | GET | `/v1/limits/{id}` | | Update limit | PATCH | `/v1/limits/{id}` | | Activate limit | POST | `/v1/limits/{id}/activate` | | Deactivate limit | POST | `/v1/limits/{id}/deactivate` | | Draft limit | POST | `/v1/limits/{id}/draft` | | Delete limit | DELETE | `/v1/limits/{id}` | | Get usage | GET | `/v1/limits/{id}/usage` | For limit type definitions (DAILY, WEEKLY, MONTHLY, CUSTOM, PER\_TRANSACTION), see [Limit types](#limit-types) earlier in this guide. The same guide covers the optional time-window and custom-period fields and the full scope-field list. The [API reference](/en/reference/products/tracer/create-limit) has schema-level details. # Environment variables Source: https://docs.lerian.studio/en/products/tracer/tracer-environment-variables Reference for Tracer environment variables covering ports, TLS, PostgreSQL, authentication, RI permission declaration, background workers, CEL cost limits, and reservation gRPC. This reference lists the environment variables used to configure **Tracer**, the real-time spending-control and transaction-risk service. 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, see the [BYOC configuration reference](/en/reference/byoc-configuration). Those blocks cover TLS posture, OpenTelemetry, Access Manager authentication, service discovery, and event streaming. This page focuses on what is distinctive to Tracer. ## Ports and health endpoints See the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract. | Surface | Port variable | Default | Endpoints | | - | - | - | - | | REST + health | `SERVER_PORT` / `SERVER_ADDRESS` | `4020` | `/health`, `/readyz`, `/version`, `/metrics` | | Reservation gRPC seam | `TRACER_GRPC_PORT` | `:4021` | gRPC reserve / confirm / release | ## Deployment and TLS | Variable | Description | Default | Required | | - | - | - | - | | `DEPLOYMENT_MODE` | Deployment flavor: `local`, `byoc`, or `saas`. In `saas`, TLS on the PostgreSQL connection is validated at boot and the server refuses to start without it. In `byoc` and `local`, that boot-time check is skipped, but per-connection TLS enforcement still applies unless `ALLOW_INSECURE_TLS=true`. 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 | ## Application | Variable | Description | Default | Required | | - | - | - | - | | `LOG_LEVEL` | Log verbosity: `debug`, `info`, `warn`, or `error` | `debug` | No | | `CEL_COST_LIMIT` | Maximum evaluation cost for a single CEL rule expression | `10000` | No | | `OPENAPI_DOCS_ENABLED` | Serve the OpenAPI 3.1 spec and interactive Scalar docs at `/v1/openapi.{json,yaml}` and `/v1/docs` | `false` | No | ## Rule evaluation | Variable | Description | Default | Required | | - | - | - | - | | `DEFAULT_DECISION_WHEN_NO_MATCH` | Decision a validation returns when no rule matches. Only `ALLOW` (fail-open) and `DENY` are accepted. Tracer deliberately rejects `REVIEW`, and any other value fails the service at boot. Leaving it unset keeps `ALLOW` and logs a startup warning. | `ALLOW` | No | | `MAX_RULES_PER_REQUEST` | Ceiling on how many active rules are evaluated in a single validation. When more rules apply, the excess is truncated (a warning is logged) and the response reports `totalRulesLoaded` with `truncated: true`. Must be positive and at most `100000`; an invalid value fails the service at boot. | `1000` | No | ## Authentication and request handling | Variable | Description | Default | Required | | - | - | - | - | | `API_KEY_ENABLED` | Enforce API-key authentication | `false` | No | | `API_KEY` | API key for request authentication. Sensitive. Use at least 32 characters in production. | — | Yes (if `API_KEY_ENABLED=true`) | | `API_KEY_ENABLED_ONLY_VALIDATION` | Validate-only mode: check keys without hard-enforcing them | `false` | No | | `API_KEY_LABEL` | Audit actor identifier recorded for the API-key principal | `tracer-default` | No | | `CORS_ALLOWED_ORIGINS` | Allowed CORS origins (CSV). When unset or empty, cross-origin requests are not allowed; set an explicit allow-list in production. The explicit value `*` is rejected at boot when `API_KEY_ENABLED=true`. | unset (no cross-origin requests allowed) | No | | `TRUSTED_PROXY_CIDRS` | Trusted proxy CIDRs for `X-Forwarded-For` parsing. Set when running behind a load balancer. | unset (uses peer IP) | No | ## RI permission declaration | Variable | Description | Default | Required | | - | - | - | - | | `IDP_DECLARATION_ENABLED` | Publish Tracer's permission declaration to the identity service at boot. This integration is optional and fail-open: a publish or configuration failure logs a warning and Tracer continues serving. | `false` | No | | `IDP_HOST` | Absolute URL of the identity service (distinct from the Access Manager address in `PLUGIN_AUTH_ADDRESS`). When declaration is enabled in `saas`, an explicit `http://` URL makes boot fail; use `https://`. | — | No (needed to publish) | | `IDP_M2M_CLIENT_ID` | Client ID used for the declaration's machine-to-machine token. | — | No (needed to publish) | | `IDP_M2M_CLIENT_SECRET` | Secret used for the declaration's machine-to-machine token. Sensitive. | — | No (needed to publish) | ## Database (PostgreSQL) Tracer stores rules and usage counters in its own `tracer` database on the shared Midaz PostgreSQL primary. A dedicated migration-runner image applies the schema migration before the application starts. The service boots against an already-migrated schema and does not run migrations in-process. > [!WARNING] > > Plan a maintenance window for the upgrade to Midaz v4.2.0. Its migration `000025` widens the asset column of the validation history table. PostgreSQL rewrites that table and rebuilds its indexes under an exclusive lock, so validation reads and writes wait until it ends. The duration grows with the number of stored validations. > > Each migration waits at most 5 seconds for the lock and fails with SQLSTATE `55P03` when traffic holds it. Run it again in a quieter window. A second run is safe. | Variable | Description | Default | Required | | - | - | - | - | | `DB_HOST` | PostgreSQL host | — | Yes | | `DB_PORT` | PostgreSQL port | — | Yes | | `DB_USER` | Database user | — | Yes | | `DB_PASSWORD` | Database password. Sensitive. | — | Yes | | `DB_NAME` | Database name | — | Yes | | `DB_SSL_MODE` | libpq SSL mode: `disable`, `require`, `verify-ca`, or `verify-full`. Use `require` or stronger in production. | `disable` | No | ## Background workers | Variable | Description | Default | Required | | - | - | - | - | | `CLEANUP_WORKER_ENABLED` | Run the expired usage-counter cleanup worker | `false` | No | | `CLEANUP_INTERVAL_HOURS` | Cleanup interval (hours). The retention window itself is fixed at 90 days and is not configurable. | `24` | No | | `RULE_SYNC_POLL_INTERVAL_SECONDS` | Rule-cache sync poll interval (seconds) | `10` | No | | `RULE_SYNC_STALENESS_THRESHOLD_SECONDS` | Rule-cache staleness threshold (seconds) | `50` | No | | `RULE_SYNC_OVERLAP_BUFFER_SECONDS` | Rule-cache sync overlap buffer (seconds) | `2` | No | ## Reservations | Variable | Description | Default | Required | | - | - | - | - | | `RESERVATION_REAPER_ENABLED` | Run the worker that expires reservations past their lifetime and returns their held capacity | `true` | No | | `RESERVATION_REAPER_INTERVAL_SECONDS` | Reservation reaper sweep interval (seconds). Must be from `1` to `3600`; an invalid value fails the service at boot. | `30` | No | | `RESERVATION_LONG_LIVED_TTL_HOURS` | Lifetime recorded on a reservation that the ledger holds for a pending transaction (hours). Must be positive and at most `8760`; an invalid value fails the service at boot. Reservations for direct transactions carry a fixed lifetime this variable does not change. | `720` | No | ## Readiness and draining | Variable | Description | Default | Required | | - | - | - | - | | `READYZ_DRAIN_GRACE_SECONDS` | Window during which `/readyz` returns 503 after SIGTERM, so Kubernetes de-registers the pod before shutdown | `12` | No | | `READYZ_CACHE_STALENESS_THRESHOLD_SECONDS` | Rule-cache age at which `/readyz` reports `degraded` | `300` | No | ## Reservation gRPC seam Server side of the seam the Midaz ledger calls to reserve spending limits. Tracer always opens the reservation port, `:4021` by default. Protect it with Access Manager authentication (`PLUGIN_AUTH_ENABLED`), an API key (`API_KEY_ENABLED`), mutual TLS (`TRACER_TLS_MODE=mtls`) or your service mesh. Tracer starts without any of these, and any workload that reaches the port can reserve and release limit capacity. | Variable | Description | Default | Required | | - | - | - | - | | `TRACER_GRPC_PORT` | gRPC listen address for the reservation seam | `:4021` | No | | `TRACER_TLS_MODE` | Seam transport mode: `mtls`, `server`, `mesh`, or empty | empty | No | | `TRACER_TLS_CERT_FILE` | Server certificate PEM path | — | Yes (if `TRACER_TLS_MODE` is `mtls` or `server`) | | `TRACER_TLS_KEY_FILE` | Server private-key PEM path. Sensitive. | — | Yes (if `TRACER_TLS_MODE` is `mtls` or `server`) | | `TRACER_TLS_CLIENT_CA_FILE` | Client CA certificate PEM path | — | Yes (if `TRACER_TLS_MODE=mtls`) | | `TRACER_TLS_CLIENT_ALLOWED_NAMES` | Comma-separated client certificate names for `mtls` mode | — | No | | `TRACER_SEAM_ALLOWED_CLIENTS` | Comma-separated Access Manager application client IDs for the seam, read when `PLUGIN_AUTH_ENABLED=true` | — | Yes (if `PLUGIN_AUTH_ENABLED=true`, outside `DEPLOYMENT_MODE=local`) | | `AUTH_M2M_INVERSION_ENABLED` | Set it to `true` when `PLUGIN_AUTH_ENABLED=true`. Otherwise Access Manager authorizes application tokens under one shared role, and only `TRACER_SEAM_ALLOWED_CLIENTS` limits which applications can reserve. | `false` | No | | `AUTH_CACHE_TTL` | Cache lifetime for Access Manager decisions, as a Go duration such as `60s` | unset (no cache) | Yes (if `PLUGIN_AUTH_ENABLED=true` with `DEPLOYMENT_MODE=saas`) | | `AUTH_BREAKER_ENABLED` | Circuit breaker on Access Manager calls | unset (off) | No | | `AUTH_JWT_VERIFY_CERT` | PEM public keys or certificates for token signatures | — | No | ## Configuration shared across products The following blocks are identical across Lerian products. The [BYOC configuration reference](/en/reference/byoc-configuration) documents them in full. They default off. * **Access Manager authentication**: `PLUGIN_AUTH_ENABLED`, `PLUGIN_AUTH_ADDRESS`. Enable in production. With service discovery on (see **Service discovery** below), Tracer resolves the Access Manager host through Consul. It falls back to `PLUGIN_AUTH_ADDRESS` if resolution fails. * **Service discovery**: `SD_*` (Consul). Off by default. When `SD_ENABLED=true`, Tracer registers itself as `midaz-tracer` (advertising the HTTP port from `SERVER_ADDRESS`, default `4020`) and resolves Access Manager through Consul. If that resolution fails, it falls back to the static `PLUGIN_AUTH_ADDRESS`. Discovery requires `SD_EXTERNAL_ADDRESS` or `SD_INTERNAL_ADDRESS`. Tracer is the server on the reservation gRPC seam and does not advertise it. * **Event streaming**: `STREAMING_*`. Off by default. * **OpenTelemetry**: `ENABLE_TELEMETRY`, `OTEL_*`. Tracer also exposes a Prometheus `/metrics` endpoint. # Tracer in Lerian Console Source: https://docs.lerian.studio/en/products/tracer/using-tracer-in-lerian-console Find how to access the Tracer module in Lerian Console to operate decision rules, spending limits, and validation history from one web interface. Tracer is available as a product module within the [Lerian Console](/en/platform/console/about-lerian-console), the unified web interface for managing all Lerian products. Through the Console, you can operate: * Decision results over a period (`Dashboard`) * Decision rules (`Rules`) * Transaction caps (`Limits`) * Validation history (`Validations`) > [!WARNING] > > To access Tracer in the Lerian Console, you need to acquire the product. Contact your account manager for more information. > **[Tracer module overview](/en/products/tracer/console/tracer-module)** > > Access the full documentation for using Tracer in the Lerian Console. ## Available actions *** From the Lerian Console, you can: * [Dashboard](/en/products/tracer/console/tracer-module#dashboard): See what Tracer decided over a period * [Rules](/en/products/tracer/console/managing-rules): Create and manage transaction rules * [Limits](/en/products/tracer/console/managing-limits): Configure and monitor spending limits * [Validations](/en/products/tracer/console/managing-validations): Inspect validation history and details # What is Tracer? Source: https://docs.lerian.studio/en/products/tracer/what-is-tracer Validate transactions in real time with Tracer: enforce spending limits, detect unauthorized activity, and decide allow/deny under 100ms. **Tracer** is Lerian's platform for real-time transaction validation and spending controls. It helps financial institutions enforce spending policies, detect unauthorized transactions, and maintain audit-ready compliance records. Tracer does this in real time, before a transaction executes. In Midaz v4, Tracer ships in the Midaz repository and release. It remains a separate runtime and deployment from the Ledger service. The Midaz chart installs it as an optional component: see the [Midaz chart README](https://github.com/LerianStudio/helm/tree/main/charts/midaz). ## Tracer in a complete core banking *** Tracer is one of the pieces of a Lerian core banking, its real-time control layer. A ledger records what happened. Tracer decides what's allowed to happen first. In that flow, before your application records a transaction in Midaz, it calls Tracer to check the move against your rules and spending limits. Your application submits to the ledger only if the decision is `ALLOW`, so unauthorized or over-limit movements never reach the ledger. Tracer stays separate from the ledger. It never initiates a call to Midaz and never reads balances. It evaluates the context it receives, records validation history, and manages reservation state. By default your application orchestrates the two, submitting to the ledger only on `ALLOW`. Midaz can also call Tracer directly through an optional per-ledger [reservation seam](/en/products/tracer/integration-guide#midaz-ledger-reservation-seam), which is off by default. In that seam the Ledger HTTP v2 transaction path asks Tracer to evaluate your rules and hold limit capacity before it commits a transaction. Then it confirms or releases the hold. Tracer's public HTTP API remains `/v1`. The seam is not a public Tracer v2 API. Only the Ledger HTTP v2 transaction path invokes the seam. Ledger HTTP v1 never does. In both flows, calls go toward Tracer. Tracer does not call back into Midaz. For where this sits in the bigger picture, see [Building a complete core banking](/en/products/building-a-complete-core-banking). ## Why use Tracer? *** Financial institutions need to control how money moves: enforcing spending caps, blocking unauthorized transactions, and producing audit trails for regulators. Doing this manually or through batch processes creates delays, errors, and compliance gaps. Tracer solves this by evaluating every transaction in real time against your policies. It provides two capabilities: * **Validation rules**: Business rules that evaluate transaction context and return instant decisions, without requiring code deployments. You define rules as expressions, so analysts and compliance teams can use them without writing code. * **Dynamic spending limits**: Configurable limits per transaction, per account, per portfolio, per segment, or per merchant, with real-time usage tracking and automatic period resets. ### Solving control and compliance challenges Tracer helps institutions evaluate spending policies, identify unauthorized transactions, and satisfy audit requirements. ## Key capabilities *** * Real-time validation with response times under 80ms (p99) * Expression-based rule engine using CEL (Common Expression Language) (A lightweight expression language for writing business rules, for example, 'if transaction amount > 10000 then REVIEW'. Tracer uses CEL for validation rules.) for type-safe, analyst-friendly rules * Product-agnostic design supporting multiple transaction types: * Card transactions (debit, credit, prepaid) * Wire transfers (domestic, international, ACH) * Pix (instant, scheduled) * Cryptocurrency (bitcoin, ethereum, stablecoin) * Dynamic spending limits: * Per transaction, account, portfolio, segment, or merchant * Daily, weekly, monthly, custom, or per-transaction periods * Real-time limit usage tracking with automatic period resets * Hash-chained audit trail * Configurable no-match default (`DEFAULT_DECISION_WHEN_NO_MATCH`): when no rule matches a validation request, Tracer returns the configured default (ALLOW for fail-open, DENY for fail-closed). Infrastructure failures surface as HTTP errors. The calling system decides the fallback behavior. ## How it works *** Tracer has four core contexts: 1. **Validation Context** - Orchestrates validation requests, coordinates rule and limit evaluation, and records the audit trail. 2. **Rules Context** - Manages rule definitions, compiles expressions, and evaluates rules against transaction context. 3. **Limits Context** - Manages spending limit configurations, tracks usage counters, and enforces thresholds. 4. **Audit Context** - Keeps the immutable event log and verifies its hash chain. **Your system must act on the validation decision** (e.g., blocking the transaction, showing an error to the user, or queuing for review). ### Who does what Tracer sits between several teams. Each team owns a different part of the loop: | Actor | What they do | Touches | | - | - | - | | **Risk / fraud analyst** | Writes and tunes rules ("deny Pix over R\$ 50k from new accounts") | `POST /v1/rules`, `/v1/rules/{id}/activate`, audit trail review | | **Product / risk manager** | Configures spending limits per segment, portfolio, account | `POST /v1/limits`, lifecycle endpoints, usage monitoring | | **Integration engineer** | Builds the call from the authorization system into Tracer | `POST /v1/validations`, retry / timeout / fallback handling | | **Authorization system** | Sends every transaction for validation, acts on ALLOW / DENY / REVIEW | Hits `POST /v1/validations` on the critical path | | **Midaz ledger** (optional seam) | When the per-ledger reservation seam is enabled, has Tracer evaluate rules and hold limit capacity before committing, then settles the hold | The gRPC `Reserve` call, then the confirm / release transitions | | **Tracer** | Evaluates rules and limits, records the result, returns the decision | Runs internally and never calls back | | **Compliance / audit** | Queries the trail, verifies hash chain integrity | `GET /v1/validations`, `/v1/audit-events`, `/v1/audit-events/{id}/verify` | | **SRE / platform** | Monitors latency, capacity, readiness | Watches `/readyz`, OTel metrics, error rates | A typical request flows: **authorization system → Tracer (evaluate rules + check limits → decision) → audit log → response back to authorization system**. Risk and product teams set up rule and limit definitions beforehand. Compliance reads the audit log later. Tracer never reaches back into your stack. There are no webhooks or callbacks. > [!NOTE] > > Monetary values (transaction `amount`, spending limit `maxAmount`, and usage counters) use decimal strings, for example `"1500.00"` or `"50000.00"`. ## When to use Tracer *** Tracer fits into any workflow where you need to control how money moves through your institution. * **Spending governance**: Evaluate transactions against internal or customer-level spending policies * **Transaction validation**: Evaluate custom business rules and return decisions before transaction execution * **Regulatory compliance**: Produce audit-ready logs and ensure traceability * **Customer protection**: Detect overspending and return decisions that your system can act on ## Scope and design principles *** Tracer is a transaction validation engine, and complete fraud management is outside its scope. It is a specialized component optimized for low-latency decisions (\< 100ms) that validates pre-enriched payloads against configurable rules and limits. ### What Tracer focuses on * **Rule-based validation**: Deterministic, expression-based logic using CEL * **Spending limits**: Real-time enforcement with automatic period reset * **Audit trail**: Immutable records * **Low-latency decisions**: Synchronous API responses under 80ms (p99) ### What Tracer does not include | Capability | Reason | Alternative | | - | - | - | | **Machine learning scoring** | ML inference adds latency incompatible with sub-100ms target | Integrate external ML services upstream and pass scores via metadata | | **Data enrichment** | External calls during validation break latency guarantees | Enrich payloads in your integration before calling Tracer | | **Manual review workflows** | Case management is outside core validation scope | Build review queues in your system and use `REVIEW` decision as trigger | | **User interface** | API-first design enables flexible integration | Build custom UIs or use third-party tools | | **Payment execution** | Tracer returns validation decisions; it does not move funds | Execute or block payments in the upstream system based on the decision | > [!TIP] > > These boundaries keep Tracer fast and predictable: it returns instant transaction decisions with audit trails. ## Integration *** Your authorization system sends validation requests with complete transaction context, and Tracer responds with a decision, typically in under 35ms. With this **Payload-Complete Pattern**, all the context Tracer needs arrives in a single request. There are no external calls during validation, and latency stays predictable. > [!NOTE] > > On Lerian Cloud, Tracer scopes every request to your tenant from your token. See [Access Manager](/en/platform/access-manager) for the auth flow. Like all Lerian products, Tracer ships with native [Access Manager](/en/platform/access-manager) integration. When enabled, Access Manager handles authentication and authorization for Tracer's APIs. This is an optional feature available under the **Enterprise model**. ## Next steps *** > **[Explore the Tracer API](/en/reference/products/tracer/tracer-api-quick-start)** > > Browse endpoints for validation rules, spending limits, and audit trails. > **[Getting started](/en/products/tracer/getting-started)** > > Set up Tracer and configure your first authorization rule. # What is Access Manager? Source: https://docs.lerian.studio/en/platform/access-manager Access Manager controls human and machine access across Lerian products using Auth, Identity, and product-level authorization for fine-grained permissions. **Access Manager** handles authentication and authorization for Lerian products. Authentication answers who makes a request. Authorization answers what they are allowed to do. **Access Manager** is Lerian's access-control layer for our products and plugins. Instead of wiring identity and permissions into every product, you can manage them in one place: who your users are, which tokens they carry, and what those tokens can reach. When a product configures route-level enforcement and its Auth client is enabled, Auth evaluates the requested resource and action before the product handler runs. In the products that read `AUTH_REQUIRED`, set `AUTH_REQUIRED=true` when requests must be refused if the Auth client is disabled or misconfigured. [Product-level enforcement](/en/platform/access-manager/product-level-enforcement) lists these products. Two services do the work, and protected products plug into them at the route level: * **Auth** runs the live side of access: issuing and refreshing tokens, validating sessions, checking permissions, handling logout and user info, and running MFA challenges. * **Identity** holds the data behind those decisions: users, groups, applications, communication providers, application-provider links, and MFA configuration. On a protected route whose product Auth client is enabled and configured, the product sends the subject, resource, and action to Auth before business logic runs. A back-office operator in a group like `midaz-viewer-group` can read Midaz data but can't change it. A service integration receives access according to its effective M2M subject. By default, non-user tokens use `admin/-editor-role`. ## Why use Access Manager? *** Use Access Manager when you want native, fine-grained access control across Lerian products, instead of stitching something together product by product. It lets you: * manage human users and the product groups that define their access * create machine-to-machine applications for service integrations * enforce permissions down to the resource and action, like `reports:get`, `templates:post`, or `accounts:patch` * apply one access-control model across every Lerian product you run * keep product APIs protected behind bearer tokens and route-level checks. How it fits your stack depends on how you deploy it: * In **SaaS deployments**, it's the platform access layer. JWT claims carry the authenticated subject and tenant context the platform relies on. * In **BYOC deployments**, you may already run your own identity provider. Access Manager can still add Lerian-native authorization and application credentials wherever you need that control. > [!TIP] > > Access Manager is available as an optional feature under the **Enterprise model**. If you'd like to learn more or evaluate it for your use case, [get in touch with our team](https://lerian.studio/contact). ## Technical specs *** What you get out of the box: * REST APIs for [Auth](/en/reference/platform/access-manager/am-auth-apis) and [Identity](/en/reference/platform/access-manager/am-identity-apis) operations. * Lerian Console settings for supported visual user and application management. * Product-level authorization enforcement for protected HTTP APIs. * Per-product Auth client configuration for route-level authorization enforcement. * OAuth2/OIDC token flows for password and client credentials access. * MFA support for user authentication flows. * Valkey-backed caching for token, permission, and MFA-related operations. * RBAC aligned with product resources, actions, groups, and machine-to-machine applications. ## Bootstrap and operation *** Access Manager has two distinct lifecycle layers: | Layer | What it is used for | | - | - | | Bootstrap | Initial environment setup, including base organizations, built-in roles, groups, applications, and permission sets. | | Operation | Day-to-day management of users, group assignments, applications, providers, MFA, and access reviews. | Bootstrap is what seeds a brand-new environment. Once that environment starts, it's no longer where you make changes. For the operator-level walkthrough of getting Auth and Identity up and ready before any product enforces access, see [Installing Access Manager](/en/platform/access-manager/installing-access-manager). From there on, manage access through the Identity APIs or Lerian Console. Console covers the everyday user and application work: creating users, assigning groups, updating passwords, and creating machine-to-machine applications. The Identity APIs give you the full operational surface, including providers, application-provider links, and MFA. Ship changes to built-in resources, actions, roles, and permission sets through controlled platform updates, like migrations or an idempotent reconciler. Don't edit bootstrap seed data to change access in a running environment. ## Tenant behavior on Lerian Cloud *** On Lerian Cloud, the tenant is part of the caller's identity. Clients never send tenant ownership in payloads, query parameters, or headers. That shapes behavior in three places: * **Identity management**: user, group, and application APIs return only records in the caller's tenant organization. * **Authentication**: password and refresh-token flows keep token handling scoped to the tenant carried by the user context. * **Authorization**: permission checks evaluate only the groups, roles, and application permissions that belong to the resolved tenant. Single-tenant deployments skip all of this and fall back to the configured default organization. > [!WARNING] > > Don't reuse a token from one tenant to manage users, applications, or permissions in another. Access Manager scopes those operations to the authenticated context. ## Use cases *** Access Manager fits scenarios such as: * Teams that want built-in authentication and authorization across Lerian products. * Organizations without an existing IAM solution. * Teams that already run an identity provider but still need product-level authorization. * Integrations that depend on secure machine-to-machine access. * Multi-product deployments that need one consistent access model for users, services, and tenants. > [!TIP] > > [Explore the Access Manager API](/en/reference/platform/access-manager/am-api-introduction). Browse endpoints for users, groups, applications, and permissions. # Enabling Access Manager Source: https://docs.lerian.studio/en/platform/access-manager/enabling-access-manager Activate Access Manager across Lerian products by setting the Auth environment variables in each `.env` so your services start enforcing access. [Installing Access Manager](/en/platform/access-manager/installing-access-manager) isn't enough on its own. To start enforcing access, turn it on in each product. Set the Auth variables in the `.env` file of any Lerian product or plugin where you want it active. > [!NOTE] > > Enabling Access Manager only turns on authorization enforcement in a product or plugin. You manage access data separately through Access Manager. This includes users, groups, applications, providers, roles, and permissions. Every protected product must enable enforcement and point to Auth. The address variable is not the same in every repository, so use the variable expected by the product you configure: ```text Text theme={null} # Most products and plugins PLUGIN_AUTH_ENABLED=true PLUGIN_AUTH_ADDRESS=http://plugin-auth:4000 ``` ```text Text theme={null} # Midaz Ledger and Pix via JD PLUGIN_AUTH_ENABLED=true PLUGIN_AUTH_HOST=http://plugin-auth:4000 ``` > [!CAUTION] > > After you enable Access Manager, protected API requests must include an `Authorization` header with a valid Bearer access token. > > Without this header, protected requests will be rejected, even for endpoints that were previously accessible without authentication. > > [**Learn how to generate and use access tokens.**](/en/platform/access-manager/using-access-manager) ## Where to update *** You'll find the relevant `.env` files in these locations: * **Midaz** * `/midaz/components/ledger` uses `PLUGIN_AUTH_HOST`. * `/midaz/components/tracer` uses `PLUGIN_AUTH_ADDRESS` * **Other products and plugins** * Use the `.env` file in the product or plugin root, or in the component directory when the repository is split into components. * Reporter and Bank Transfer use `PLUGIN_AUTH_ADDRESS`. * Pix Indirect BTG uses `PLUGIN_AUTH_ADDRESS`. * Pix via JD uses `PLUGIN_AUTH_HOST`. > [!TIP] > > If you can't see the files, adjust your system settings to show hidden files. `.env` files are often hidden by default. ## Rebuild after changes *** After updating a `.env` file, rebuild and restart from the root of the repository that owns that file. Lifecycle commands differ by repository: in the Midaz repository root, run `make rebuild-up`, because its `make up` target starts services without rebuilding them. For other products and plugins, use the lifecycle command documented for that product. ## Deployment lifecycle *** Access Manager setup has two phases: * **Bootstrap** seeds a new environment with the base organization, roles, groups, applications, and permission sets required by the platform. * **Operation** starts after the environment is up. From that point on, manage access through the Identity APIs or Lerian Console. Use the operational APIs for user access, group assignment, application credentials, providers, and MFA. Don't change a running environment by editing bootstrap seed files. > [!WARNING] > > Bootstrap applies its seed data only during initial environment setup. Changes to built-in resources, actions, roles, groups, applications, or permission sets in an existing environment must be delivered through controlled platform updates. Examples include migrations and an idempotent reconciler. # Manage the IP allowlist via API Source: https://docs.lerian.studio/en/platform/access-manager/features/ip-allowlist/api Read and replace your workspace's IP allowlist with two Identity API operations, and handle the errors the operations can return. Two operations on the Identity API manage the [IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview): one reads it, one replaces it. Use them when you manage security settings from your own tooling instead of the Console. The API reference documents both operations: [Get Organization IP Allowlist](/en/reference/platform/access-manager/get-organization-ip-allowlist) and [Set Organization IP Allowlist](/en/reference/platform/access-manager/set-organization-ip-allowlist). ## The two operations *** | Operation | What it does | Permission | | - | - | - | | `GET /v1/security/ip-allowlist` | Returns the stored entries and the surfaces where the list applies. | `security` / `get` | | `PUT /v1/security/ip-allowlist` | Replaces the whole list and, optionally, the surfaces. | `security` / `put` | Both operations live on the Identity API base URL and need a bearer token. The platform resolves your workspace from the token. There is no organization identifier in the path or in the body. ## The body *** The request body of `PUT` and the response body of both operations have these fields: ```json JSON theme={null} { "entries": ["203.0.113.0/24", "198.51.100.7"], "scopes": ["console", "api"] } ``` | Field | Required | Meaning | | - | - | - | | `entries` | Yes | The full list of addresses and CIDR ranges. What you send replaces what is stored. | | `scopes` | No | Where the list applies: `console`, `api`, or both. | | `labeledEntries` | Response only | The stored entries in the same order, each with its `description`. | Rules that apply to the fields: * `entries` is a full replacement. To add one address, send the current list plus the new address. * In a request, you can send an entry as an address string or as an object with `cidr` and `description`. You can mix the two forms. * The platform stores a string entry with an empty description. To keep a description, send that entry as an object again. * An entry broader than `/24` for IPv4 or `/48` for IPv6 is rejected with `400`, and nothing changes. * The response echoes the entries as the platform stored them. A single address comes back with its prefix: `198.51.100.7` becomes `198.51.100.7/32`. * `entries: []` deactivates the list. The operation returns `200`. * An empty stored list serializes as `[]`, never as `null`. * When you omit `scopes`, the stored scopes stay as they are. * `scopes: []` keeps the entries but stops enforcement everywhere. * When you omit `entries`, the operation returns `400` with code `IDE-0001` and nothing changes. ## Examples *** Replace the placeholders with your Identity API base URL and a bearer token that holds the `security` permission. ```bash Read the list theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" curl -sS "${IDENTITY_BASE_URL}/v1/security/ip-allowlist" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" ``` ```bash Activate for Console access theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" curl -sS -X PUT "${IDENTITY_BASE_URL}/v1/security/ip-allowlist" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "entries": ["203.0.113.0/24", "198.51.100.7"], "scopes": ["console"] }' ``` ```bash Add API access theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" curl -sS -X PUT "${IDENTITY_BASE_URL}/v1/security/ip-allowlist" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "entries": ["203.0.113.0/24", "198.51.100.7", "192.0.2.10"], "scopes": ["console", "api"] }' ``` ```bash Deactivate theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" curl -sS -X PUT "${IDENTITY_BASE_URL}/v1/security/ip-allowlist" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "entries": [] }' ``` A successful `PUT` returns `200` with the stored list: ```json Response theme={null} { "entries": ["203.0.113.0/24", "198.51.100.7/32"], "scopes": ["console"], "labeledEntries": [ { "cidr": "203.0.113.0/24", "description": "" }, { "cidr": "198.51.100.7/32", "description": "" } ] } ``` ## Recommended order *** **1. Register the addresses with only the console scope** Send the full list with `"scopes": ["console"]`. Include the address you call from. **2. Make sure you can still sign in** Sign in to the Console from a listed address. Then try from an unlisted address and expect a refusal. **3. List every integration's outgoing address** Collect the address each ERP, webhook sender, scheduled job, and application calls from. Add each one to `entries`. **4. Add the api scope** Send the full list again with `"scopes": ["console", "api"]`. Watch your integrations for `403` responses. ## Errors *** Every error uses the Access Manager envelope with the fields `code`, `title`, and `message`. | Status | `code` | `title` | `message` | | - | - | - | - | | 400 | `IDE-0001` | Missing Fields in Request | Your request is missing one or more required fields. | | 400 | `IDE-0035` | IP Allowlist Too Large | The IP allowlist is too large: the combined, comma-separated entries must not exceed `{value}` characters. Please remove some entries and try again. | | 400 | `IDE-0036` | Invalid IP Allowlist Entry | The IP allowlist entry `{value}` is not a valid IP address or CIDR block. Please correct it and try again. | | 400 | `IDE-0037` | Invalid IP Allowlist Scope | The IP allowlist enforcement scope `{value}` is not supported. Please use console, api, or both, and try again. | | 401 | `IDE-0008` | Token Missing | A valid token must be provided in the request header. Please include a token and try again. | | 401 | `IDE-0009` | Invalid Token | The provided token is expired, invalid or malformed. Please provide a valid token and try again. | | 403 | `AUT-0021` | IP Not Allowed | Access from your network is not allowed for this workspace. Contact your administrator. | The stored entries, separated by commas, must fit in 200 characters. Descriptions do not count. > [!WARNING] > > Inspect the `code` field, not only the status. A `403` with code `AUT-0021` can come back from any protected endpoint, including these two, when your own address is outside an active list. In that case the list did not change and you must call from a listed address. ## Related pages *** > **[IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview)** > > What the feature protects, how it decides, and what it does not cover. > **[Access Manager error list](/en/reference/platform/access-manager/access-manager-error-list)** > > Every Auth and Identity error code, with title and message. # Manage the IP allowlist in the Console Source: https://docs.lerian.studio/en/platform/access-manager/features/ip-allowlist/console Add trusted addresses, choose whether the list applies to Console access, API access, or both, and deactivate it from the Security page of Lerian Console. This guide shows how to manage the [IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview) from the Console. You add addresses, choose where the list applies, and deactivate it when you no longer need it. ## Before you start *** You need the `security` permission with the `get` action to open the page and the `put` action to change the list. Lerian's default administrator role has both. Without `get`, the page shows "You are not authorized to access this page." Know the address you connect from. The Console shows it to you in several dialogs and in the **Add my IP** button. If you connect through a VPN or a corporate proxy, the address you see is the address of that VPN or proxy. ## Open the IP allowlist tab *** **1. Open Settings** Click the gear icon () in the top navigation bar. **2. Select Security** Select **Security** in the Settings sidebar. The page title is "Security". **3. Select the IP allowlist tab** The page opens on the **Single Sign-On** tab. Select the **IP allowlist** tab. You can also open the path `/settings/security?tab=ip-allowlist` directly. ![The Security page in the Console, open on the IP allowlist tab. The amber banner reads "IP allowlist inactive" and the empty-state box reads "No IP addresses registered. While the list is empty, access is not restricted." The "Add my IP" and "Add IP" buttons sit above the box.](https://mintcdn.com/lerian-49cb71fc/kktgwl8ajkkMHFzZ/images/en/console/security-ip-allowlist-empty.png?fit=max\&auto=format\&n=kktgwl8ajkkMHFzZ\&q=85\&s=bc6b84025d249245cd3429fb75bf570a) Figure 1. The IP allowlist tab with an empty list ## Read the page *** The tab subtitle reads "Restrict platform access to a list of trusted IP addresses." A status banner tells you whether the platform enforces the list now: | Banner | Color | Description shown | | - | - | - | | IP allowlist inactive | Amber | "Access is not restricted. Add an IP address to activate the allowlist." | | IP allowlist inactive | Amber | "The list is saved but enforced nowhere — access is not restricted. Choose where it applies below." | | IP allowlist active | Green | "Only the IP addresses below can sign in and use the console. API access is not restricted." | | IP allowlist active | Green | "Only the IP addresses below can reach the API with your credentials. Console access is not restricted." | | IP allowlist active | Green | "Only the IP addresses below can sign in to the console or reach the API with your credentials." | Three buttons sit above the list: * **Add my IP** adds the address you connect from. When the Console cannot determine it, it disables the button and shows the tooltip "Your IP address could not be determined." * **Add IP** opens a side sheet where you type an address or a range. * **Deactivate** appears only when the list has at least one entry. ## Add an address *** **1. Open the side sheet** Click **Add IP**. The side sheet "Add IP address" opens with the description "Add an IP address or CIDR range to the allowlist." **2. Type the address or range** Fill in **IP address or CIDR range**. The placeholder shows the accepted forms, such as `203.0.113.5` or `203.0.113.0/24`. Optionally, fill in **Description** with a label of up to 255 characters, for example "São Paulo office". The description is only a label and does not change access. To add your own address instead, click **Add my IP** inside the sheet. When the Console knows your address, the button shows it in parentheses. The entry keeps the description you typed. **3. Save** Click **Save**. The Console validates the entry before it saves it. The Console rejects two kinds of input: * A value that is not an address or a CIDR range shows "Enter a valid IP address or CIDR range". * A value already on the list shows "This entry is already in the list." `203.0.113.5` and `203.0.113.5/32` count as the same entry. ### When you add the first entry When you add the first entry, the Console asks you to confirm with the dialog "Activate the IP allowlist?". The text reads: > Once activated, only requests from your listed IP addresses are accepted on the surfaces ticked under "Where this list is applied". Your current IP is `{ip}`. Click **Activate** to continue or **I changed my mind** to go back. ### When your own address is not on the list The list can apply to **Console access** and not cover the address you connect from. If your address is not a private network address, the Console adds it to the list before it saves. It then shows the notice "We also added your current IP (`{entry}`)". The notice explains why and how to delete the entry later. If the list has no room for your address, the Console opens the dialog "We could not add your current IP to this list" instead. ### When your address is a private network address You can connect from a private network address that a list for **Console access** does not cover. In that case, the Console asks you first. The dialog "We see your access coming from `{ip}`, a private network address" explains that the address may belong to your VPN or a proxy. Click **Include it anyway** to add it, or **Continue without including it** to leave it out. ## Choose where the list applies *** The section "Where this list is applied:" appears below the table once the list has at least one entry. It holds two checkboxes: * **Console access**: "only the addresses above can sign in and use the console". * **API access**: "only the addresses above can use the API with your credentials". The Console saves each change as soon as you tick or untick a checkbox. ### Apply the list to API access When you tick **API access**, the Console asks you to confirm. The dialog "Your integrations have to be on the list above" opens. It names the systems that call the API with your credentials: webhooks, ERPs, scheduled jobs, application tokens. It asks you to review the list before you confirm. Click **Apply to API access** to confirm. > [!WARNING] > > Do not tick **API access** until every integration's outgoing address is on the list. ### Untick both checkboxes If you untick both checkboxes, the list stays saved but does nothing. The banner turns amber. The note below the checkboxes reads "The list is saved but enforced nowhere — access is not restricted." ## Delete an entry *** **1. Find the entry** Find the row in the table. The "IP address / CIDR" column shows the entry, and the "Description" column shows its label. The footer reads "Showing N IP entries." **2. Click the trash icon** Click the trash icon ("Delete") on the row. The dialog "Remove this entry?" opens. **3. Confirm** Read the text "Once removed, requests from this IP range will no longer be allowed." Click **Yes, remove it**. The deletion can leave your own address outside a list for **Console access**. In that case, the Console then opens the dialog "Your IP is not in this list". It warns that you may lose access to this workspace if you save. Click **Save anyway** only if you control another address on the list. Deleting the last entry deactivates the list, so the Console shows the deactivation dialog instead. ## Deactivate the list *** Deactivation clears every entry and allows access from any address again. **1. Click Deactivate** Click **Deactivate** above the table. **2. Confirm** The dialog "Deactivate the IP allowlist?" reads "This clears every entry and allows access from any IP address again." Click **Yes, deactivate**. ![The dialog "Deactivate the IP allowlist?" over the Security page. The body reads "This clears every entry and allows access from any IP address again." The buttons read "I changed my mind" and "Yes, deactivate".](https://mintcdn.com/lerian-49cb71fc/kktgwl8ajkkMHFzZ/images/en/console/security-ip-allowlist-deactivate-dialog.png?fit=max\&auto=format\&n=kktgwl8ajkkMHFzZ\&q=85\&s=fa5b71ef7f692b72f8488e7c23e6111b) Figure 2. The deactivation dialog > [!TIP] > > To pause the list without losing the entries, untick both checkboxes instead. The entries stay saved and you can apply them again later. ## Messages the Console shows *** After a successful change, the Console shows "IP allowlist updated". When the platform refuses a change, the Console shows the refusal in a notification. The [API errors](/en/platform/access-manager/features/ip-allowlist/api#errors) list the causes. ## Related pages *** > **[IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview)** > > What the feature protects, how it decides, and what it does not cover. > **[Manage the list via API](/en/platform/access-manager/features/ip-allowlist/api)** > > Read and replace the list with two Identity API operations. # Deployment requirements (BYOC) Source: https://docs.lerian.studio/en/platform/access-manager/features/ip-allowlist/deployment Configure the trusted proxies so the IP allowlist enforces in your own BYOC deployment. The [IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview) compares the caller's network address with the list. Behind a load balancer or an ingress, the address the service sees is the proxy's, not the caller's. This page tells a BYOC operator what to configure so the platform learns the real address. > [!NOTE] > > This page applies to BYOC deployments only. On Lerian Cloud, Lerian operates this configuration for you. You only manage the list itself. For the architecture view, read [Tenant IP allowlist](/en/platform/access-manager/product-level-enforcement#tenant-ip-allowlist). ## Trusted proxies *** Set `TRUSTED_PROXIES` on these Lerian services, not only on the Access Manager: * The Access Manager **Auth** and **Identity** services. * The products Midaz, Reporter, Lender, and Streaming Hub. * The rails Pix via JD, Pix Lerian, and Bank Transfer. Each of these services resolves the caller's address on its own and forwards it to the Access Manager Auth service. A service without the variable forwards no address. If the service calls Auth from a network in `PLATFORM_INTERNAL_CIDRS`, Auth allows the request without checking the address. When `PLATFORM_INTERNAL_CIDRS` is set and the service calls Auth from outside it, Auth checks the service's own address against the list. On the Access Manager itself, the variable decides whether the allowlist enforces at all, as the table below shows. The value is a comma-separated list of CIDR ranges. List only the proxies that connect directly to that service. ```text Example theme={null} TRUSTED_PROXIES=10.0.0.0/16,10.1.0.0/16 ``` Rules the platform applies to the value: * CIDR ranges only. A bare address without a prefix is rejected. * Very broad ranges are rejected: broader than `/8` for IPv4 or `/48` for IPv6. `0.0.0.0/0` never works. * On the Access Manager, a malformed value stops the service at startup. What happens when the value is absent: | Where it is unset | Effect | | - | - | | Access Manager Auth | The allowlist does not enforce. A metric and a log line signal the untrusted-proxy condition. | | A listed product or rail, such as Midaz | The service forwards no caller address. If the service calls Auth from a network in `PLATFORM_INTERNAL_CIDRS`, Auth allows the request without checking the address. When `PLATFORM_INTERNAL_CIDRS` is set and the service calls Auth from outside it, Auth checks the service's own address against the list. | When Auth has `TRUSTED_PROXIES` set and `PLATFORM_INTERNAL_CIDRS` unset, it trusts no forwarded address. It then refuses product requests that an active list applies to. ## Validate the deployment *** Run this check once after you set the variables, from a workspace whose list you control. **1. Activate a list with your own address** Send the list through the [Identity API](/en/platform/access-manager/features/ip-allowlist/api) with `"scopes": ["console", "api"]`. Include the address you connect from and the address your test client calls from. **2. Sign in from a listed address** Sign in from that address. The sign-in must succeed. **3. Sign in from an unlisted address** Sign in from another network, such as a mobile connection. Expect a refusal. **4. Call a Midaz endpoint from both addresses** Call any protected Midaz endpoint from the listed address, then from the unlisted one. Expect `200` and then `403` with code `AUT-0021`. If the platform accepts a request from the unlisted address, it does not learn the real address. Check `TRUSTED_PROXIES` on the service that answered and `PLATFORM_INTERNAL_CIDRS` on Auth and Identity. ## Related pages *** > **[Tenant IP allowlist architecture](/en/platform/access-manager/product-level-enforcement#tenant-ip-allowlist)** > > How Auth, Identity, and each product share the check. > **[Manage the list in the Console](/en/platform/access-manager/features/ip-allowlist/console)** > > The administrator's guide to the Security page. # IP allowlist Source: https://docs.lerian.studio/en/platform/access-manager/features/ip-allowlist/overview Restrict access to your Lerian workspace to a list of trusted network addresses, for people who sign in and for systems that call the API. The IP allowlist is a list of network addresses that your workspace trusts. The list belongs to the whole workspace, not to one person or one application. ## What the allowlist protects *** The allowlist has two switches. You can turn on one, the other, or both. | Switch | What it restricts | Who it affects | | - | - | - | | **Console access** | People who sign in to the Console with a user account. | Your team: administrators, analysts, operators. | | **API access** | Systems that call the Lerian APIs with your workspace's credentials. | Your integrations: ERPs, webhooks, scheduled jobs, application tokens. | > [!WARNING] > > Do not turn on **API access** before you list every address your integrations call from. Those addresses are usually not the same ones your team uses to reach the Console. ## How the platform decides *** A request to a protected route goes through this check, in this order: 1. A request arrives at the platform with your workspace's credentials. 2. The platform works out the network address the request came from. Behind your load balancer, it reads the address your proxy forwards. On BYOC, see [Deployment requirements](/en/platform/access-manager/features/ip-allowlist/deployment). On Lerian Cloud, Lerian handles this for you. 3. The platform checks whether the list is active for that kind of access, Console or API, and whether the address is on the list. ## What a refused person or system sees *** See the [Access Manager error list](/en/reference/platform/access-manager/access-manager-error-list). ## What you can register *** An entry is a single address or a range of addresses, in IPv4 or IPv6. | You type | The platform stores | Why | | - | - | - | | `203.0.113.5` | `203.0.113.5/32` | A single IPv4 address is a range of one. | | `2001:db8::7` | `2001:db8::7/128` | A single IPv6 address is a range of one. | | `203.0.113.0/24` | `203.0.113.0/24` | A CIDR range stays as you typed it. | | `203.0.113.77/24` | `203.0.113.0/24` | The platform normalizes the range to its network address. | Rules that apply to the list: * Use CIDR notation for ranges. The platform does not accept "from-to" ranges such as `203.0.113.1-203.0.113.50`. * The platform merges duplicates. `203.0.113.5` and `203.0.113.5/32` are the same entry. * The platform rejects a range broader than `/24` for IPv4 or `/48` for IPv6. * The stored entries, separated by commas, must fit in 200 characters. Prefer ranges over long lists of single addresses. * An entry can have a description, such as the name of the office. The description is text for your administrators. It does not change the access decision. ## The three states of the list *** | State | What it means | Does the platform check addresses? | | - | - | - | | **Empty** | No entries. | No. | | **Saved, not applied** | Entries exist, but no switch is on. | No. The list is stored and does nothing. | The Console shows the current state in a banner at the top of the IP allowlist tab. ## Safety behaviors in the Console *** The Console helps you avoid locking yourself out. * Before the first entry activates the list, the Console asks you to confirm. * If the list applies to Console access and does not cover your own address, the Console adds your address or asks you before it saves. * An **Add my IP** button adds the address you connect from. * The Console asks for a separate confirmation before it applies the list to API access. Read the step-by-step guide in [Manage the IP allowlist in the Console](/en/platform/access-manager/features/ip-allowlist/console). ## What the allowlist does not cover *** * **Lerian's own platform services keep working.** Requests that carry a platform-internal token bypass the list, so your workspace does not stop by accident. * **There is no administrator exemption.** Keep at least one address you control on the list. * **Break-glass.** If every administrator is locked out, contact Lerian support to restore access. ## Next steps *** > **[Manage the list in the Console](/en/platform/access-manager/features/ip-allowlist/console)** > > Add addresses, choose where the list applies, and deactivate it. > **[Manage the list via API](/en/platform/access-manager/features/ip-allowlist/api)** > > Read and replace the list with two Identity API operations, with examples. > **[Deployment requirements](/en/platform/access-manager/features/ip-allowlist/deployment)** > > What a BYOC operator configures so the platform learns the caller's real address. > **[Error list](/en/reference/platform/access-manager/access-manager-error-list)** > > Every code the allowlist can return, with title and message. # Manage MFA via API Source: https://docs.lerian.studio/en/platform/access-manager/features/mfa/api Enroll and manage MFA methods with Identity, then complete MFA challenges with Auth. MFA uses two Access Manager APIs: * Identity enrolls methods and manages the user's MFA settings. * Auth completes the second verification step during sign-in. Read [Multi-factor authentication](/en/platform/access-manager/features/mfa/overview) before you integrate these operations. ## Account enrollment operations *** The Identity API exposes these self-service operations: | Operation | Purpose | | - | - | | `POST /v1/users/{id}/mfa/setup` | Start setup for `app` or `email`. | | `POST /v1/users/{id}/mfa/verify` | Verify the setup passcode. | | `POST /v1/users/{id}/mfa/enable` | Enable the verified method. | | `GET /v1/users/{id}/mfa` | Read the current MFA status and enabled methods. | | `PATCH /v1/users/{id}/mfa/preferred` | Select the preferred enabled method. | | `DELETE /v1/users/{id}/mfa` | Disable all methods. | | `DELETE /v1/users/{id}/mfa/{mfaType}` | Disable one method. | | `POST /v1/users/{id}/mfa/recovery-codes` | Generate a new set of recovery codes. The previous set stops working. | The standard operations are self-service. The bearer-token subject must match `{id}`. ### Start setup Send the method in the request body: ```json Request theme={null} { "mfaType": "app" } ``` This feature supports `app` and `email`. Authenticator-app setup returns a secret, a QR-code URL, and recovery codes. Email setup returns recovery codes and sends a passcode to the user's saved email address. ### Verify setup Submit the 6–8-character setup passcode and the method. Authenticator-app verification also needs the secret returned by setup. ```json Request theme={null} { "mfaType": "app", "passcode": "123456", "secret": "setup-secret" } ``` ### Enable the method After verification, enable the method with one recovery code from the setup response. Authenticator-app enablement also needs the setup secret. ```json Request theme={null} { "mfaType": "app", "secret": "setup-secret", "recoveryCode": "recovery-code" } ``` > [!WARNING] > > Treat setup secrets and recovery codes as credentials. Do not log them or store them in source control. ### Turn off MFA or remove a method To turn off MFA, send the code that the [Disable MFA](/en/reference/platform/access-manager/disable-mfa) reference describes. To remove the authenticator app or the email method, send a code from that method. To get the code by email, call `POST /v1/users/{id}/mfa/disable/challenge`. Only the user can request it, and only when the email method is on. The code is valid for 10 minutes and allows 5 wrong attempts. A user can request 3 codes in 15 minutes. ### Administrator operations An administrator manages the methods of another user with two more operations. `{id}` is the target user. The bearer token needs the `mfa-admin` permission with the `post` action to enable a method and the `delete` action to remove one. | Operation | Purpose | | - | - | | [`POST /v1/users/{id}/mfa/admin/enable`](/en/reference/platform/access-manager/admin-enable-mfa) | Enable `email` for the target user without setup or verification. The response contains the recovery codes of the target user. | | [`DELETE /v1/users/{id}/mfa/admin/{mfaType}`](/en/reference/platform/access-manager/admin-disable-mfa) | Remove one method (`app`, `email`, or a legacy `sms` enrollment) from the target user. The operation takes no code. | ## Login operations *** The first factor uses [Request Access Token](/en/reference/platform/access-manager/request-access-token). When MFA is required, the operation returns an MFA challenge response instead of access tokens. The response includes: * `mfaRequired: true`. * a short-lived `mfaToken`. * the enabled methods. * the preferred method. ### Request email delivery Use [Initiate MFA Challenge](/en/reference/platform/access-manager/initiate-mfa-challenge) for email. ```json Request theme={null} { "mfaToken": "short-lived-mfa-token", "mfaType": "email" } ``` Do not request delivery for `app`. The authenticator app generates the passcode locally. ### Verify the second factor Use [Verify MFA Login](/en/reference/platform/access-manager/verify-mfa-login). Send either `passcode` or `recoveryCode`, not both. ```json Passcode theme={null} { "mfaToken": "short-lived-mfa-token", "mfaType": "app", "passcode": "123456" } ``` ```json Recovery code theme={null} { "mfaToken": "short-lived-mfa-token", "mfaType": "app", "recoveryCode": "unused-recovery-code" } ``` A successful verification returns the standard access-token response. A recovery code is consumed after one successful use. ## Error handling *** Important login errors include: | Code | Meaning | | - | - | | `AUT-0016` | The passcode is invalid. | | `AUT-0017` | The MFA session expired. Restart sign-in. | | `AUT-0018` | The verification limit was exceeded. | | `AUT-0020` | The MFA token is invalid. | Identity can also reject an unsupported method, missing setup secret, missing email destination, or unverified setup. See the [Access Manager error list](/en/reference/platform/access-manager/access-manager-error-list) for the full error envelope and current codes. ## Related pages *** > **[Complete MFA in the Console](/en/platform/access-manager/features/mfa/console)** > > The user workflow for verification during sign-in. > **[Identity APIs](/en/reference/platform/access-manager/am-identity-apis)** > > The generated reference for account-management operations. # Complete MFA and remove a method in the Console Source: https://docs.lerian.studio/en/platform/access-manager/features/mfa/console Complete MFA at Console sign-in with a passcode or a recovery code, and remove an enrolled method. Lerian Console completes the second verification step for users who already have [multi-factor authentication](/en/platform/access-manager/features/mfa/overview) enabled. Account enrollment and method management are available under Settings › Security and through the [Identity API](/en/platform/access-manager/features/mfa/api). ## Before you start *** You need: * an active user account with authenticator-app or email MFA enabled. * access to the selected method or an unused recovery code. ## Complete the second verification step *** After the password or SSO step succeeds, the Console opens the MFA verification page. **1. Select a method** Use the preferred method or select another enabled method. **2. Get the passcode** For an authenticator app, open the app and read the current code. At sign-in, the Console sends the email code automatically. **3. Enter the passcode** Enter the six-digit passcode and continue. ## Use a recovery code *** Select the recovery-code option when an enrolled method is unavailable. Enter one unused recovery code. > [!WARNING] > > A recovery code works once. Store the remaining codes securely after sign-in. ## Recover from a failed verification *** | Situation | Action | | - | - | | The passcode is invalid | Get the current passcode and try again before the submission limit is reached. The verification limit applies per user, not per sign-in session. | | The email does not arrive | Wait for the resend cooldown, then request one new code. | | The MFA session expires | Restart the complete sign-in flow, including the first factor. | | No enrolled method is available | Use an unused recovery code. If none is available, contact an administrator and follow your organization's account-recovery procedure. | ## Remove a method *** In **Settings** › **Security**, open the **MFA** tab and select **Remove** next to the method. To remove the authenticator app, enter its current code. To remove the email method, enter the code that the Console sends by email when the dialog opens. ## Related pages *** > **[Multi-factor authentication](/en/platform/access-manager/features/mfa/overview)** > > How MFA enrollment, verification, and recovery work. > **[Manage MFA via API](/en/platform/access-manager/features/mfa/api)** > > Enroll methods, select a preference, and manage recovery codes. > **[MFA deployment requirements](/en/platform/access-manager/features/mfa/deployment)** > > Configure Auth for MFA sign-in. # MFA deployment requirements (BYOC) Source: https://docs.lerian.studio/en/platform/access-manager/features/mfa/deployment Configure MFA session protection, challenge limits, and email delivery in a BYOC deployment. This page tells a BYOC operator what to configure before users enable [multi-factor authentication](/en/platform/access-manager/features/mfa/overview). > [!NOTE] > > This page applies to BYOC deployments only. On Lerian Cloud, Lerian operates this configuration for you. ## Protect MFA sessions *** Set `MFA_SECRET` in the Auth process environment. Auth uses this secret to sign short-lived MFA-session tokens. These tokens connect the verified first sign-in factor to the MFA challenge. Set `MFA_ENABLED=true` in the Auth process environment to make Auth check `MFA_SECRET` at startup. If the secret is empty, Auth starts and logs an error. The MFA routes then return `503` with code `AUT-0028` until you set the secret and restart Auth. This startup check does not enable or disable MFA for individual users. `MFA_ENABLED=false` does not bypass their MFA requirement. > [!WARNING] > > Configure `MFA_SECRET` before users enable MFA. If the value is empty, Auth cannot create the MFA token required to continue an MFA-enabled sign-in. Inject the value from a secret store. Do not commit it to a values file. ## Configure session and attempt limits *** The Auth deployment supports these variables: | Variable | Default | Purpose | | - | - | - | | `MFA_SESSION_TTL_SEC` | `300` | Lifetime of the short MFA sign-in session, in seconds. | | `MFA_MAX_ATTEMPTS` | `5` | Maximum verification attempts allowed per user within the counter window. | | `MFA_MAX_RESEND_ATTEMPTS` | `3` | Maximum email challenge sends allowed per user and email method within the counter window. | Short session times reduce the period in which a stolen MFA token is useful. Verification requests count only after input, token, session, organization, and IP checks pass. The per-user verification counter expires after `MFA_SESSION_TTL_SEC` without another counted request. Each counted request refreshes that expiry. The resend counter is per user and method, with a window of `MFA_SESSION_TTL_SEC` from its first reservation. Failed sends release their reservation. Successful verification clears both counters. Restarting sign-in does not reset them. Attempt and resend limits reduce repeated passcode guessing and message delivery abuse. ## Configure email delivery *** Email MFA needs: * an email communication provider in Identity. * a link between the provider and the application that sends MFA messages. * an email address on the user account. Use the provider and application-provider operations described in [Identity APIs](/en/reference/platform/access-manager/am-identity-apis). ## Validate the deployment *** **1. Enable an authenticator app for a test user** Complete setup and store the issued recovery codes. **2. Sign out and sign in again** Make sure Auth returns the MFA step after the first factor. **3. Verify a valid passcode** Enter the current authenticator passcode. Expect the normal user session. **4. Verify expiration** Start sign-in again and wait longer than `MFA_SESSION_TTL_SEC`. Expect the MFA session to expire. **5. Test configured delivery methods** Enable email for the test user. Request a challenge and make sure the message arrives at the saved address. **6. Test a recovery code** Sign in with one recovery code, then make sure the same code cannot be used again. Do not run limit tests against a production user. Repeated invalid attempts can temporarily block MFA verification for that user. ## Related pages *** > **[Complete MFA in the Console](/en/platform/access-manager/features/mfa/console)** > > Test passcode and recovery-code verification during sign-in. > **[Manage MFA via API](/en/platform/access-manager/features/mfa/api)** > > Integrate account enrollment and login verification. # Multi-factor authentication Source: https://docs.lerian.studio/en/platform/access-manager/features/mfa/overview Add a second verification step to user sign-in with an authenticator app or email. Multi-factor authentication (MFA) requires a user to prove their identity twice. The user first signs in with a password or single sign-on. Access Manager then asks for a passcode or recovery code before it issues access tokens. MFA applies to user accounts. It does not apply to machine-to-machine applications that use client credentials. ## Supported methods *** | Method | How the user gets the passcode | Additional requirement | | - | - | - | | **Authenticator app** | A TOTP application generates a six-digit passcode on the user's device. | The user scans the setup QR code or enters its secret. | | **Email** | Access Manager sends a six-digit passcode to the user's saved email address. | The deployment needs an email communication provider. | A user can enable more than one method and select one as the preferred method. The preferred method appears first during sign-in, but the user can select another enabled method. ## How MFA sign-in works *** 1. The user signs in with a password or [single sign-on](/en/platform/access-manager/features/sso/overview). 2. Auth validates the first factor. 3. If MFA is enabled for the user, Auth returns a short-lived MFA token instead of access tokens. 4. The user selects one of the enabled methods. 5. For email, Auth sends a challenge code. An authenticator app generates its code locally. 6. The user submits a six-digit passcode or one recovery code. 7. Auth verifies the second factor and returns the access tokens. The MFA token expires after the deployment's configured session time. Auth also limits verification and resend attempts. > [!WARNING] > > Do not treat an MFA token as an access token. It grants access only to the MFA challenge and verification operations. ## Setup and recovery codes *** A user must set up and verify a method before enabling it. The setup differs by method: * An authenticator app setup returns a QR-code URL and a secret. * Email setup sends a verification code to the saved email address. Access Manager issues recovery codes during MFA enrollment. Each recovery code works once. The user can use one when the preferred method is unavailable. > [!WARNING] > > Store recovery codes when Access Manager issues them. You cannot retrieve the same codes later. Generating a new set invalidates the previous set. The public Identity API can disable all MFA methods for an account. This operation also removes the account's recovery codes. To turn off MFA, send the code that the [Disable MFA](/en/reference/platform/access-manager/disable-mfa) reference describes. ## Self-service actions *** Users can enroll and manage their own MFA methods in the Console, under Settings › Security, or through the Identity API. In the Console, a user can enable an authenticator app or email, remove a method, choose the default method, and generate recovery codes. The Console guide covers MFA at sign-in and removing a method. Self-service API operations require the authenticated subject to match the user ID in the request path. ## Failure and recovery behavior *** | Situation | Result | | - | - | | The passcode is invalid | An invalid passcode evaluated after the preliminary checks counts toward the per-user verification limit. | | The MFA session expires | The user must restart sign-in. | | The resend limit is reached | Auth stops sending new email challenges for that user until the email-method counter expires or successful MFA verification clears it. | | A recovery code succeeds | Access Manager consumes the code so it cannot be used again. | | The user loses every method and recovery code | Contact an administrator and follow your organization's account-recovery procedure. | ## Next steps *** > **[Complete MFA in the Console](/en/platform/access-manager/features/mfa/console)** > > Verify a passcode or use a recovery code during sign-in. > **[Manage MFA via API](/en/platform/access-manager/features/mfa/api)** > > Use the Auth and Identity operations for enrollment and sign-in. > **[MFA deployment requirements](/en/platform/access-manager/features/mfa/deployment)** > > Configure MFA session protection, limits, and delivery providers. > **[Single sign-on](/en/platform/access-manager/features/sso/overview)** > > Use an external identity provider as the first sign-in factor. # Manage partners via API Source: https://docs.lerian.studio/en/platform/access-manager/features/partners/api Create, read, change, and delete partners with the Identity API, read the product scope catalogs, create partner applications, and handle the error codes. The Identity API manages [partners](/en/platform/access-manager/features/partners/overview) and their applications. Use it when you manage partners from your own tooling instead of the Console. ## Before you start *** To manage partners, a user needs the `partners` permissions of `plugin-identity`: `get` to read partners and scope catalogs, `post` to create a partner, `patch` to change one and `delete` to delete one. Managing its applications needs the `applications` permissions `get`, `post` and `delete`. ## The operations *** | Operation | What it does | Reference | | - | - | - | | `GET /v1/partners` | Lists your partners, one page at a time, with `applicationsCount` for each one. | [List partners](/en/reference/platform/access-manager/list-partners) | | `POST /v1/partners` | Creates a partner. It returns `201` with the new partner and its `id`. It creates no application. | [Create a partner](/en/reference/platform/access-manager/create-a-partner) | | `GET /v1/partners/{id}` | Returns one partner. | [Retrieve a partner](/en/reference/platform/access-manager/retrieve-a-partner) | | `PATCH /v1/partners/{id}` | Changes a partner: its name, its permissions, its scope, its validity window, its IP allowlist, or its state. | [Update a partner](/en/reference/platform/access-manager/update-a-partner) | | `DELETE /v1/partners/{id}` | Deletes a partner without applications. It returns `204`. | [Delete a partner](/en/reference/platform/access-manager/delete-a-partner) | | `GET /v1/partners/ceiling` | Returns the most you can give a partner in the product that the `product` query parameter names. | [Retrieve the partner ceiling of a product](/en/reference/platform/access-manager/retrieve-the-partner-ceiling-of-a-product) | | `GET /v1/scope-catalog` | Lists the scope catalogs that products published, with `partners` for each one. | [List scope catalogs](/en/reference/platform/access-manager/list-scope-catalogs) | | `GET /v1/scope-catalog/{product}` | Returns the scope catalog of one product. A product without a catalog returns `404` with `IDE-1053`. | [Retrieve a product's scope catalog](/en/reference/platform/access-manager/retrieve-a-product-s-scope-catalog) | To learn which dimensions a product accepts in `scope`, read its scope catalog. Each dimension has a `name`, and the flags `required` and `multi`. ## The partner fields *** | Field | Required on create | Meaning | | - | - | - | | `displayName` | Yes | The partner's name, 1 to 128 characters. It is unique in your tenant, and the comparison ignores case. | | `permissions` | Yes | One or more entries, each with its own `product`, `resources`, and `actions`. Each entry needs at least one resource and one action. | | `scope` | No | One entry per restriction: `product`, `field`, and `values`. `field` is the `name` of a dimension in the product's scope catalog. | | `ipAllowlist` | No | The partner's own IP allowlist. Each entry has a `cidr` and an optional `description` of up to 255 characters. | | `validFrom` | No | The start of the validity window, as an RFC 3339 instant. Absent means no start date. | | `validUntil` | No | The end of the validity window, as an RFC 3339 instant. Absent means no end date. | | `state` | Not accepted | `active` or `suspended`. Every new partner is `active`. You change it with `PATCH`. | The response also carries `id`, `applicationsCount`, `createdAt`, and `updatedAt`. Rules that apply to the fields: * `product` is the product slug that the product's scope catalog carries, such as `plugin-br-pix-jd`. * Write `actions` as lowercase HTTP verbs: `get`, `post`, `put`, `patch`, `delete`. * `resources` × `actions` is a full cross product. To give different actions on different resources, send more than one entry for the same product. * Access Manager refuses the wildcard `*` in `resources`, `actions`, and `values`. List the items. * A product in `scope` must also be in `permissions`. * A dimension that the catalog marks as `required` needs a scope entry on every partner with permissions on that product. A dimension without `multi` takes one value only. * Access Manager does not check that the `values` exist in the product. Use the identifiers that the product's own API returns. * `state` is `active` or `suspended`, never `expired`. After `validUntil`, a read still returns the stored state. * After the partner's `validUntil` date, Access Manager refuses new tokens for it. ## The IP allowlist field *** `ipAllowlist` has three meanings on `PATCH`: | You send | What happens | | - | - | | The field omitted | The stored list stays as it is. | | `null` | The partner's own list is deleted. The partner goes back to your tenant's list. | | A list of entries | These entries replace the partner's own list. | | `[]` | Refused with `IDE-1048`. | On `POST`, omit the field or send `null` to use your tenant's list. When a partner has its own IP list, that list replaces the tenant's IP list for the partner's requests. `validFrom` and `validUntil` work in a similar way on `PATCH`. Omit a bound to keep it, send an instant to set it, or send `null` to remove it. ## Examples *** Replace the placeholders with your Identity API base URL, your bearer token, and identifiers from your own deployment. ```bash Create a partner theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" curl -sS -X POST "${IDENTITY_BASE_URL}/v1/partners" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "displayName": "Partner A", "permissions": [ { "product": "plugin-br-pix-jd", "resources": ["entries"], "actions": ["get", "post"] } ], "scope": [ { "product": "plugin-br-pix-jd", "field": "participantId", "values": ["12345678"] } ], "ipAllowlist": [ { "cidr": "203.0.113.10", "description": "Partner A gateway" } ] }' ``` ```bash Create its application theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" PARTNER_ID="paste-the-partner-id-here" curl -sS -X POST "${IDENTITY_BASE_URL}/v1/applications" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "target": "plugin-br-pix-jd", "description": "Partner A access to Pix JD", "partnerId": "'"${PARTNER_ID}"'" }' ``` ```bash Suspend it theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" PARTNER_ID="paste-the-partner-id-here" curl -sS -X PATCH "${IDENTITY_BASE_URL}/v1/partners/${PARTNER_ID}" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "state": "suspended" }' ``` ```bash Go back to the tenant's IP list theme={null} IDENTITY_BASE_URL="https://identity." # your Identity API base URL IDENTITY_BEARER_TOKEN="paste-your-access-token-here" PARTNER_ID="paste-the-partner-id-here" curl -sS -X PATCH "${IDENTITY_BASE_URL}/v1/partners/${PARTNER_ID}" \ -H "Authorization: Bearer ${IDENTITY_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "ipAllowlist": null }' ``` ## Create the partner's applications *** A partner calls a product with an application that belongs to it. After you create the partner: 1. Create an application with [Create an Application](/en/reference/platform/access-manager/create-an-application). Send the partner's `id` in `partnerId`, and the product slug in `target`, such as `plugin-br-pix-jd`. 2. The Identity API returns the client secret only in the response that creates the application. Store it then. If you lose the secret, delete the application and create a new one. 3. Send the client ID and the secret to the partner through a secure channel. 4. Repeat steps 1 to 3 for each product the partner calls. Create one application per product. To list the applications of one partner, send its `id` in the `partnerId` query parameter of [List Applications](/en/reference/platform/access-manager/list-applications). If `partnerId` does not name a partner of your tenant, both operations return `404` with `IDE-1046`. ## Change, suspend, or delete a partner *** * On `PATCH`, send only the fields you change. A `permissions` or a `scope` list replaces the stored list as a whole. Read the partner first, then send the complete new list. * To suspend a partner, send `"state": "suspended"`. To reactivate it, send `"state": "active"`. * Suspending a partner in Access Manager stops its access, including the tokens it already holds. Reactivating the partner inside its validity window restores access with the same credentials. * You cannot delete a partner that still has applications. The response is `409` with `IDE-1049`, and its `errors` list names each blocking application with its client ID. Delete those applications first. * Deleting an application stops its tokens from working. ## Error codes *** Errors in the partner operations: | Code | Status | Title | When | | - | - | - | - | | `IDE-0001` | 400 | Missing Fields in Request | A required field is missing, or the scope has no entry for a dimension the product requires. | | `IDE-0036` | 400 | Invalid IP Allowlist Entry | An `ipAllowlist` entry is not a valid address or CIDR range. | | `IDE-1040` | 409 | Partner Display Name Already Exists | Another partner of your tenant has the same `displayName`. | | `IDE-1042` | 400 | Unknown Scope Field | A scope `field` is not in the product's scope catalog, or the product has no scope catalog. | | `IDE-1043` | 400 | Permission Above Product Ceiling | A permission is above what the product's editor role holds in your tenant. | | `IDE-1044` | 400 | Scope Without Permissions | A product is in `scope` but not in `permissions`. | | `IDE-1045` | 400 | Invalid Validity Window | `validUntil` is not later than `validFrom`. | | `IDE-1046` | 404 | Partner Not Found | No partner with this `id` exists in your tenant. | | `IDE-1047` | 400 | Wildcard Not Allowed | `*` is in `resources`, `actions`, or `values`. | | `IDE-1048` | 400 | Empty IP Allowlist | `ipAllowlist` is an empty array. | | `IDE-1049` | 409 | Partner Has Applications | The partner still has applications, so it cannot be deleted. | | `IDE-1050` | 400 | Duplicate IP Allowlist Entry | The same network appears twice in `ipAllowlist`. | | `IDE-1052` | 400 | Partner Rejected | The identity provider refused the partner for another reason. The message repeats that reason. | | `IDE-1053` | 404 | Scope Catalog Not Found | The product has not published a scope catalog. | | `IDE-1054` | 400 | Product Not Ready For Partners | A permission names a product that has not published its scope catalog. | | `IDE-1055` | 400 | Product Not Opted In To Partners | A permission names a product whose scope catalog does not accept partners. | | `IDE-1056` | 400 | Partner Write Exceeds Its Scope | A write permission acts at tenant level, or at a level wider than the partner's scope. | | `IDE-1057` | 400 | Partner Read Rejected | The identity provider refused the ceiling read, for example for an unknown product. | For every other code, see the [Access Manager error list](/en/reference/platform/access-manager/access-manager-error-list). # Manage partners in the Console Source: https://docs.lerian.studio/en/platform/access-manager/features/partners/console Create a partner step by step, create its applications, change what it can reach, suspend it, and delete it from Settings in Lerian Console. This guide shows how to manage [partners](/en/platform/access-manager/features/partners/overview) from the Console. You create the partner, create its applications, and change or end its access later. ## Before you start *** To manage partners, a user needs the `partners` permissions of `plugin-identity`: `get` to see Partners in Settings, `post` to create a partner, `patch` to change one and `delete` to delete one. Managing its applications needs the `applications` permissions `get`, `post` and `delete`. Creating a partner, changing its permissions or scope, and creating its applications need Access Manager 3.11.0 or later. With an earlier version, the Console shows "Requires Access Manager 3.11.0 or later" in place of those controls. Collect this information before you start: * The products the partner needs, and what it may do in each one. * The identifiers of the items the partner may reach, for each dimension you restrict. Copy them from the product's own API or Console. The Console does not check that they exist. * The network addresses the partner calls from, if you want to restrict them. * The start and end dates of its access, if it has a time limit. ## Open the Partners page *** **1. Open Settings** Click the gear icon () in the top navigation bar. **2. Select Partners** Select **Partners**. The subtitle reads "Give each customer its own credentials and limit what it can reach." The **Give partners access** card on the Console home page opens the same page. The list shows each partner's **Name**, **Status**, **Validity window**, **Applications**, and **Creation Date**. The status is **Active**, **Suspended**, **Expired**, or **Not yet valid**. The Console shows **Expired** and **Not yet valid** from the validity window. With no partners yet, the page reads "You haven't registered any partners yet". ## Create a partner *** Click **New partner**. A wizard opens with five steps: **Details**, **Products**, **Scope**, **Permissions**, and **Review**. The **Next** button opens only when the current step is complete. The **Back** button keeps what you entered. **1. Details** Type the partner's **Name**. It must be unique in your tenant. Under **Validity window**, fill in **Valid from** and **Valid until** if the access has a time limit. Leave **Valid from** empty to start now. Leave **Valid until** empty so it never expires. After the partner's **Valid until** date, Access Manager refuses new tokens for it. Under **Where it calls from**, choose one option: * **Use your organization's IP list**: the partner uses your tenant's IP allowlist. * **Give it its own list**: type each **IP address or CIDR range** and an optional **Description**. Click **Add IP** for each new line. When a partner has its own IP list, that list replaces the tenant's IP list for the partner's requests. **2. Products** Choose the products the partner can use. The list shows only what your tenant can give. A product that does not accept partners is unavailable, with one of these messages: * "This product isn't ready for partners yet." The product has not published its scope catalog. * "This product hasn't enabled partner access yet." The product's scope catalog does not accept partners. **3. Scope** Say where the partner can act on each product. A restriction that the product requires is already open, with **(Required)** after its name. To add an optional restriction, check **Restrict by** followed by the name of the restriction. Type one identifier, or several identifiers separated by commas. **4. Permissions** Check the resources and the actions the partner can use on each product. To give a different set of actions on other resources, click **Add another line on** followed by the product name. The Console offers only what your tenant can give. It shows a reason next to each entry that it cannot save: * **Not allowed**: the product's editor role in your tenant does not hold this permission. * **Outside the scope**: the write acts at a level wider than the partner's scope. * **Granted twice**: another line already grants the same action on the same resource. **5. Review** Read the summary under "Check what this partner can and can't do". The **Only inside** part lists the restrictions. The **It can** part lists the actions on each resource. The **It can't** part lists what stays outside. Click **Save**. The Console creates the partner with no applications and opens its **Overview** tab. If Access Manager refuses the partner, the Console shows "The Access Manager refused this change". It also shows the reason and the error code, and opens the step that holds the field to fix. ## Create the partner's applications *** A partner calls a product with an application that belongs to it. Create one application per product. **1. Open the Applications tab** In the partner list, click the partner. Select the **Applications** tab. **2. Create the application** Click **New Application**. In the **New application** side sheet, choose the **Product**. The list shows only the products that the partner has permissions on. Fill in **Description** and click **Create application**. **3. Save the credentials** The dialog "Save the credentials" shows the **Client ID** and the **Client secret**. The Console shows the client secret only when you create the application. Store it then. Click **I've saved them** to close the dialog. Partner applications do not appear in **Settings → Applications**. They appear only on the partner's **Applications** tab. ## Change a partner *** Open the partner from the list. The detail page has five tabs: | Tab | What you do there | | - | - | | **Overview** | Rename the partner, change its **Validity window**, suspend or reactivate it, and delete it. | | **Permissions** | Add or remove products, resources, and actions. | | **Scope** | Add, change, or remove restrictions. Click **Add restriction** and choose the item under **Restrict by**. | | **IPs** | Give the partner its own list, change it, or go back to **Use your organization's list**. | | **Applications** | Create and delete the partner's applications. | Saving on **Permissions** or **Scope** replaces the permissions and the scope together. ## Suspend or reactivate a partner *** On the **Overview** tab, click **Suspend** and confirm. This deletes nothing. To end the suspension, click **Reactivate** and confirm. Suspending a partner in Access Manager stops its access, including the tokens it already holds. Reactivating the partner inside its validity window restores access with the same credentials. ## Delete a partner *** You can delete only a partner without applications. **1. Delete its applications** On the **Applications** tab, delete each application of the partner. Deleting an application stops its tokens from working. **2. Delete the partner** On the **Overview** tab, under **Delete partner**, click **Delete** and confirm with **Yes, delete it**. Access Manager also deletes its permissions, scope, and IP list. You cannot undo this. If the partner still has applications, the Console lists them and offers **Go to Applications**. ## Partners that use your tenant's IP list *** When some partners have no IP list of their own, **Settings → Security** shows a notice on the **IP allowlist** tab. The notice says how many partners use your tenant's list, and its button lists them. # Partners Source: https://docs.lerian.studio/en/platform/access-manager/features/partners/overview Give each of your own customers its own applications, and choose what it can do, where, from which addresses, and for how long. A partner is one of your own customers that calls your Lerian platform from its own systems. Access Manager gives each partner its own applications. You decide what the partner can do and in which part of a product's data. You also decide its network addresses and its validity window. ## The building blocks *** A partner is a record of your tenant. The rows after it describe what you set for a partner. | Concept | What it is | | - | - | | **Partner** | A record in your tenant that represents one of your own customers. A partner is not a tenant. | | **Partner application** | An application that belongs to one partner. A partner can have several applications. | | **Permissions** | What the partner can do: a product, its resources, and the actions on them. | | **Scope** | Where the partner can do it: one restriction per dimension of the product's scope catalog, with the values you choose. | | **Scope catalog** | The list of dimensions that a product publishes for partners, such as a participant identifier. | | **Ceiling** | The most you can give a partner in one product: the permissions of that product's editor role in your tenant. | | **Partner IP allowlist** | A list of network addresses that belongs to one partner. | | **Validity window and state** | The start and end of the partner's access (`validFrom` and `validUntil`), and its state: `active` or `suspended`. | ## Products that accept partners *** You can give a partner permissions only on a product that accepts partners. A product accepts partners when it publishes a scope catalog with the partner opt-in. To see which products accept partners in your deployment, read the scope catalogs with [List scope catalogs](/en/reference/platform/access-manager/list-scope-catalogs). A product accepts partners when its entry has `partners: true`. A product without an entry has not published a scope catalog. In the Console, the **Products** step of the partner wizard shows the products that do not accept partners as unavailable. ## What you need to know *** * Each product sets its own dimensions in its scope catalog. The catalog says if a dimension is required, and if it takes more than one value. * When a product marks a dimension as required, a partner with permissions on that product needs a scope entry for it. * Access Manager does not check that scope values exist in the product. It stores the values you enter. Copy them from the product's own API or Console. * You cannot delete a partner that has applications. Delete its applications first. * Access Manager refuses a permission above the ceiling with `IDE-1043`. The Console marks it as **Not allowed**. ## Next steps *** > **[Manage partners in the Console](/en/platform/access-manager/features/partners/console)** > > Create a partner step by step, create its applications, suspend it, or delete it. > **[Manage partners via API](/en/platform/access-manager/features/partners/api)** > > The partner operations, their fields, examples, and error codes. > **[IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview)** > > Your tenant's list of trusted network addresses. > **[Error list](/en/reference/platform/access-manager/access-manager-error-list)** # Configure SSO in the Console Source: https://docs.lerian.studio/en/platform/access-manager/features/sso/console Test, save, review, reconfigure, and remove a tenant's single sign-on provider in Lerian Console. This guide shows how to configure [single sign-on](/en/platform/access-manager/features/sso/overview) from the Security page of Lerian Console. ## Before you start *** You need permission to read Security settings and identity providers. You also need permission to configure or remove an application provider. Create an OAuth or OpenID Connect application in your identity provider. Keep its client ID and client secret available. ## Open the Single Sign-On tab *** **1. Open Settings** Click the gear icon () in the top navigation bar. **2. Select Security** Select **Security** in the Settings sidebar. **3. Select Single Sign-On** The page opens on the **Single Sign-On** tab. You can also open `/settings/security?tab=sso` directly. The page shows the active provider when one is configured. It also shows the authorized redirect URI that you must register with the provider. ## Register the callback URL *** Copy the authorized redirect URI from the Console and add it to the provider application. The Console derives the value from the current browser origin. Confirm that this is the canonical Console URL that your users open. The path ends with: ```text theme={null} /signin/sso/callback ``` The scheme, host, and path must match exactly. Use HTTPS outside local development. ## Configure a provider *** **1. Open the configuration form** Click **Configure SSO**. If a provider exists, click **Reconfigure** instead. **2. Select the provider type** Select Google, Microsoft Entra ID, Okta, or Custom (OIDC). **3. Enter the credentials** Enter the client ID and client secret. You must enter the complete secret on every save or reconfiguration. **4. Enter the provider-specific settings** For Okta, enter the Okta domain. For Custom OpenID Connect, enter the issuer URL and optional scopes. **5. Test the connection** Run the connection test before you save. **6. Review the checks** Fix each failed check. Review the discovered authorization, token, and user-info endpoints. **7. Save the provider** Register the callback URL and resolve every required preflight failure before you save. The Console does not block saving when preflight has not passed. The connection test checks the candidate configuration without changing the active provider. ## Provider-specific settings *** ### Google and Microsoft Entra ID Enter the client ID and client secret from the provider application. Register the callback URL shown by the Console. ### Okta Enter the client ID, client secret, and Okta organization domain. Register the callback URL manually in Okta. The Console does not present the callback probe as a reliable Okta check. ### Custom OpenID Connect Enter an absolute issuer URL. Access Manager uses OpenID Connect discovery to resolve the authorization, token, and user-info endpoints. Scopes are optional. Enter them as a space-separated list, such as `openid profile email`. ## Review the active provider *** The provider summary shows its type, display name, and masked client ID. The callback URL appears in the configuration form. Custom OpenID Connect endpoints appear during configuration when they apply. The client secret is write-only and never appears after you save it. To change the provider, open the reconfiguration form and enter the full credentials again. ## Remove the provider *** Removing the provider stops new SSO sign-ins for the tenant. Existing sessions continue until they expire or are revoked through the relevant session controls. > [!WARNING] > > Make sure an administrator can still use an allowed sign-in method before you remove the provider. ## Test user sign-in *** **1. Open a private browser session** Use a private session so an existing provider session does not hide the login flow. **2. Enter a tenant email address** Enter an email address whose domain belongs to the configured tenant. **3. Continue with the provider** Select the provider if password login is also available. **4. Complete provider authentication** Sign in at the identity provider and return to the Console callback. **5. Complete MFA if required** Finish the second verification step for users who have MFA enabled. ## Related pages *** > **[Single sign-on](/en/platform/access-manager/features/sso/overview)** > > Provider support and tenant resolution. > **[SSO deployment requirements](/en/platform/access-manager/features/sso/deployment)** > > Configure the callback and browser-reachable Auth route. # SSO deployment requirements (BYOC) Source: https://docs.lerian.studio/en/platform/access-manager/features/sso/deployment Configure the Console callback, browser-reachable Auth route, Identity route, and tenant resolution for SSO in BYOC. This page tells a BYOC operator what to configure before an administrator enables [single sign-on](/en/platform/access-manager/features/sso/overview). > [!NOTE] > > This page applies to BYOC deployments only. On Lerian Cloud, Lerian operates this configuration for you. ## Set one callback URL *** Set `PLUGIN_AUTH_SSO_CALLBACK_URL` on both the Auth and Identity services. Use the exact public Console callback URL: ```text theme={null} https://console.example.com/signin/sso/callback ``` Auth sends this URL to the identity provider. Identity includes it in the allowed redirect configuration. > [!WARNING] > > Use the same absolute URL in Auth, Identity, the Console, and the identity-provider application. A scheme, host, or path mismatch makes the authorization-code exchange fail. ## Configure Console routes *** The Console needs these routes: | Variable | Purpose | | - | - | | `PLUGIN_AUTH_BASE_PATH` | Server-to-server Auth API base URL, including `/v1`. | | `PLUGIN_AUTH_PUBLIC_BASE_PATH` | Browser-reachable Auth API base URL, including `/v1`. | | `PLUGIN_IDENTITY_BASE_PATH` | Server-to-server Identity API base URL, including `/v1`. | Set `PLUGIN_AUTH_PUBLIC_BASE_PATH` to an HTTPS address that the user's browser can reach. The browser follows the redirect from the SSO start operation directly. To set these variables with the Product Console chart, see the [Product Console chart README](https://github.com/LerianStudio/helm/tree/main/charts/product-console). The public and server-to-server Auth URLs can point to the same address. Use an internal address for server-to-server calls and the public address for browser redirects. ## Configure tenant resolution *** Before SSO starts, Auth must know which Caradhras organization the user belongs to. Each tenant is one organization. Choose one of the two setups below. ### Fixed organization (recommended) Set `PLUGIN_AUTH_SSO_STATIC_ORGANIZATION` in Auth to the name of the Caradhras organization your users belong to. This is normally the value of `AUTHORIZER_ORGANIZATION_NAME`, which defaults to `lerian`. Auth then uses that organization for every SSO request and does not compare the email domain with the organization. This setup needs no change to Caradhras data and works for users of any email domain. ### Without a fixed organization If you do not set `PLUGIN_AUTH_SSO_STATIC_ORGANIZATION`, SSO uses the organization fields: * Set `domain` on the organization to your users' email domain, such as `example.com`. Without it, Auth refuses every SSO login. * Set `domainHome` to `true` if people without an account must be able to sign in with SSO for the first time. Without it, the Console offers these people password login only. An organization holds one `domain`, so only users of that email domain can sign in with SSO. If your users have more than one email domain, use the fixed organization instead. ### Set the organization fields You can set `domain` and `domainHome` on the organization's edit page in the Caradhras web UI, or with the Caradhras API operation `POST /api/update-organization`. The Console has no input for them. To use the API, send the request from a host that can reach Caradhras at the address in `AUTHORIZER_ADDRESS`, the address that Auth and Identity use. Authenticate with HTTP Basic and the client ID and secret that Access Manager uses for Caradhras, `AUTHORIZER_CLIENT_ID` and `AUTHORIZER_CLIENT_SECRET`. A Caradhras global administrator can also make the call. By default, a tenant administrator cannot. This example makes the `acme` organization the home of the `example.com` domain: ```bash theme={null} AUTHORIZER_ADDRESS="paste-the-AUTHORIZER_ADDRESS-value-here" AUTHORIZER_CLIENT_ID="paste-the-AUTHORIZER_CLIENT_ID-value-here" AUTHORIZER_CLIENT_SECRET="paste-the-AUTHORIZER_CLIENT_SECRET-value-here" curl -X POST "$AUTHORIZER_ADDRESS/api/update-organization?id=admin/acme" \ -u "$AUTHORIZER_CLIENT_ID:$AUTHORIZER_CLIENT_SECRET" \ --json '{"owner":"admin","name":"acme","domain":"example.com","domainHome":true}' ``` Keys that you leave out of the body keep their stored values. A key that you send replaces the stored value, even when it is empty or `false`. Send `owner` and `name` with the organization's current values, because a different `name` renames the organization. Leave out `domainHome` for an organization that is not the home of its domain. To check the result, list the organizations of a domain: ```bash theme={null} curl "$AUTHORIZER_ADDRESS/api/get-organizations?owner=admin&field=domain&value=example.com" \ -u "$AUTHORIZER_CLIENT_ID:$AUTHORIZER_CLIENT_SECRET" ``` ### Refuse users from other domains (optional) Caradhras can also compare the email domain with the organization's `domain` when it creates a user, including a first-time SSO user, and when an administrator changes a user's email. The `emailDomainPolicy` setting controls this check: | Value | Effect | | - | - | | `off` | Caradhras does not check the email domain. | | `warn` | Default. Caradhras logs the mismatch and saves the user. | | `enforce` | Caradhras refuses the user with `CRDH-2009`. | The check applies only to organizations that have a `domain`. To set it with the Access Manager chart, see the [Access Manager chart README](https://github.com/LerianStudio/helm/tree/main/charts/plugin-access-manager). ## Expose Auth to the browser *** The browser must reach the Auth SSO start route over HTTPS. Expose Auth at a public address and allow the Console origin according to your deployment's cross-origin policy. Identity can remain on a server-to-server route when administrators manage it only through the Console. ## Configure the identity provider *** In the provider application: 1. Register the exact Console callback URL. 2. Allow the OpenID Connect scopes required to return the user's identity and email. 3. Issue a client ID and client secret for the tenant. 4. Make the issuer and endpoints reachable from the Access Manager services. For a Custom OpenID Connect provider, use an issuer that publishes valid discovery metadata. API integrations can instead supply the complete authorization, token, and user-info endpoint set. ## Validate the deployment *** **1. Open the Single Sign-On tab** Make sure the Console can read the current provider state from Identity. **2. Run provider preflight** Test the candidate configuration without saving it. Fix every required failure. **3. Register the callback URL** Compare the provider registration with `PLUGIN_AUTH_SSO_CALLBACK_URL` character by character. **4. Save the provider** Keep password login available during the first test. **5. Test from a private browser session** Enter a tenant email, complete provider sign-in, and make sure the browser returns to the Console. **6. Test tenant isolation** Try an email that does not belong to the tenant. Make sure Auth does not complete the login or reveal tenant details. **7. Test MFA continuation** Sign in with an MFA-enabled user and make sure the Console opens the MFA verification step after SSO. ## Related pages *** > **[Configure SSO in the Console](/en/platform/access-manager/features/sso/console)** > > Test and save the provider configuration. # Single sign-on Source: https://docs.lerian.studio/en/platform/access-manager/features/sso/overview Let users sign in to a Lerian workspace through Google, Microsoft Entra ID, Okta, or another OpenID Connect provider. Single sign-on (SSO) lets an external identity provider authenticate users for a Lerian workspace. Access Manager resolves the user's tenant, redirects the browser to the provider, and exchanges the provider's authorization code for Lerian access tokens. SSO applies to user sign-in. Machine-to-machine applications continue to use client credentials. ## Supported providers *** | Provider | Configuration | | - | - | | **Google** | OAuth client ID, client secret, and the Lerian callback URL. | | **Microsoft Entra ID** | OAuth client ID, client secret, and the Lerian callback URL. The API identifies this provider as `AzureAD`. | | **Okta** | OAuth client ID, client secret, Okta domain, and the Lerian callback URL. | | **Custom OpenID Connect** | Client ID, client secret, optional scopes, the Lerian callback URL, and either an issuer URL or explicit provider endpoints. | Access Manager supports one active SSO provider per tenant. Configuring another provider replaces the current configuration. > [!NOTE] > > This SSO contract uses OAuth 2.0 and OpenID Connect. It does not configure a SAML provider. ## How sign-in works *** 1. The Console asks for the user's email address. 2. Auth resolves the tenant from the email address, as described in [Tenant resolution](#tenant-resolution). It then returns the available sign-in method. 3. The Console creates an S256 PKCE challenge. 4. Auth redirects the browser to the configured identity provider. 5. The provider authenticates the user and returns an authorization code to the Console callback URL. 6. The Console sends the code, state, and PKCE verifier to Auth. 7. Auth verifies the flow and the returned email identity. 8. Auth returns Lerian access tokens or continues to [MFA verification](/en/platform/access-manager/features/mfa/overview). Auth verifies that the email returned by the provider belongs to the same tenant that started the flow. It refuses the login when the email is absent or resolves to another tenant. ## Tenant resolution *** Each tenant is a Caradhras organization. SSO uses two fields of that organization: * `domain` is the email domain of the tenant's users, such as `example.com`. Auth refuses the SSO login when the user's email domain differs from the tenant's `domain`. * `domainHome` marks the one organization where people of that domain get their account when they sign in with SSO for the first time. Each domain has at most one home organization. A person with accounts in several tenants signs in to their default tenant. If the person has no default tenant, Auth selects one. On Lerian Cloud, Lerian sets these fields for you. In a BYOC deployment, set them as described in [SSO deployment requirements](/en/platform/access-manager/features/sso/deployment). A BYOC deployment can use one fixed organization instead, set with `PLUGIN_AUTH_SSO_STATIC_ORGANIZATION`. Auth then does not compare the email domain with the organization. Discovery does not reveal tenant identifiers. An unknown email receives the same general response as a tenant without SSO. ## Configuration validation *** Run preflight validation before saving a provider. Preflight does not change the tenant configuration. It checks: * required fields. * OpenID Connect discovery when the provider uses an issuer URL. * explicit endpoint validation when the API supplies authorization, token, and user-info URLs. * client credentials. * the callback URL. * the resolved authorization, token, and user-info endpoints. Okta cannot always confirm the callback registration through preflight. Register the callback URL in Okta before you enable the provider. ## Next steps *** > **[Configure SSO in the Console](/en/platform/access-manager/features/sso/console)** > > Test, save, review, and remove the tenant's provider. > **[SSO deployment requirements](/en/platform/access-manager/features/sso/deployment)** > > Configure callback URLs, public Auth routing, and tenant resolution. > **[Multi-factor authentication](/en/platform/access-manager/features/mfa/overview)** > > Add a second verification step after SSO. # Installing Access Manager Source: https://docs.lerian.studio/en/platform/access-manager/installing-access-manager Prepare your environment, deploy the Auth and Identity services, and get Access Manager ready before any Lerian product enforces access. Installing Access Manager means getting the **Auth** and **Identity** services running, connected to their dependencies, and ready to answer access decisions. This step has to happen before any Lerian product can enforce authentication or permissions. This guide is for operators who install Access Manager. To install or upgrade the services, see the [Access Manager chart README](https://github.com/LerianStudio/helm/tree/main/charts/plugin-access-manager). ## Prerequisites *** You can plan an installation without deep Kubernetes knowledge. Use the list below as a checklist before you start. ### License Access Manager is an Enterprise feature and requires a valid Lerian license configuration. The services apply license checks through HTTP middleware, so validate the license before enabling protected products. See the [License documentation](/en/start-here/evaluate-and-deploy/lerians-license) for details. If you don't have a license yet, [get in touch with our team](https://lerian.studio/contact). ### Deployment model Decide how you're going to run Access Manager. The choice shapes tenant resolution, Caradhras configuration, and a few operational defaults later on. * **SaaS** - Lerian runs the platform. You manage users, groups, and applications through Lerian Console or the Identity APIs. * **BYOC** - You run Access Manager against a configured default organization and Caradhras identity backend. If you're not sure which model applies, confirm with your Lerian contact before installing. ### Auth service Auth is the runtime side of Access Manager. It issues and refreshes tokens, validates sessions, checks permissions, handles logout and user info, and runs MFA challenges. Auth needs to be reachable from every protected Lerian product over the network. ### Identity service Identity is the data side of Access Manager: users, groups, applications, communication providers, application-provider links, and MFA configuration. Identity is the management surface. Both Identity and Auth connect to the configured Caradhras backend: Identity manages identity data through it, while Auth uses its own connection for tokens, MFA, and authorization decisions. ### Identity backend Access Manager uses a configured Caradhras backend for identity storage. Caradhras persists its identity data in the configured PostgreSQL database. PostgreSQL backs Caradhras and does not replace it. For a BYOC deployment, configure that Caradhras backend for your environment. ### Data and cache services * A **PostgreSQL** instance as the backing database for Caradhras-managed identity data. * A **Valkey** cache for token, permission, and MFA-related operations. Provision both and make them reachable before Auth and Identity start. Sizing and high-availability decisions depend on your deployment model. ### Product connectivity Plan the network paths now so you're not debugging them later: * Protected products must reach Auth over the network. * Administrators (and any back-office tooling) must reach Identity to manage users, groups, applications, and providers. * Do not expose either service publicly without the appropriate ingress controls. ### Environment and bootstrap data A fresh environment seeds base access data, including its organization, built-in roles, groups, applications, and permission sets, once at startup. This is the **bootstrap** layer. After the environment is up, day-to-day changes happen through the Identity APIs or Lerian Console, not by editing bootstrap data. > [!WARNING] > > Bootstrap applies its seed data only during initial environment setup. Changes to built-in resources, actions, roles, groups, applications, or permission sets in an existing environment must be delivered through controlled platform updates, such as migrations or an idempotent reconciler. ## Installation flow *** Follow these steps in order. Each one builds on the previous one. **1. Choose your deployment model** Pick SaaS or BYOC and confirm any model-specific decisions with your Lerian contact: Caradhras configuration and licensing scope. Write this down because every later step refers back to it. **2. Prepare dependencies** Provision the supporting infrastructure so Auth and Identity have everything they need at startup: * a valid Lerian license configured for the environment. * a PostgreSQL instance to back Caradhras-managed identity data. * a Valkey cache for tokens, permissions, and MFA. * the configured Caradhras identity backend. * network paths between Access Manager, its dependencies, and the products that will eventually enforce access. Don't move on until each dependency is reachable from where Access Manager will run. **3. Deploy Auth and Identity** Install the Access Manager services into your environment. See the [Access Manager chart README](https://github.com/LerianStudio/helm/tree/main/charts/plugin-access-manager). **4. Bootstrap base access data** First startup seeds the base organization, built-in roles, groups, applications, and permission sets that the platform relies on. Let bootstrap finish before pointing any product at the services. From this point on, treat bootstrap data as read-only. Manage everyday access through the Identity APIs or Lerian Console. **5. Validate the services** Confirm that the installation is healthy before any product depends on it: * Auth and Identity report healthy and are reachable from the products that will use them. * PostgreSQL (Caradhras's backing database) and Valkey are connected and responsive. * The Caradhras identity backend is configured and reachable. * A test token request succeeds against Auth, and a basic Identity call (for example, [List Groups](/en/reference/platform/access-manager/list-groups)) succeeds for the selected deployment mode. If anything in this list fails, fix it here. Don't enable products on top of an unhealthy install. **6. Enable protected products** After you validate the installation, turn enforcement on inside each Lerian product or plugin by setting the Auth variables in its `.env`. The full per-product configuration lives in [Enabling Access Manager](/en/platform/access-manager/enabling-access-manager). **7. Use Access Manager** With installation done and products enforcing access, move on to day-to-day operation: requesting tokens, managing users, groups, and applications, configuring MFA, and calling protected APIs. Start with [Using Access Manager](/en/platform/access-manager/using-access-manager) for the API workflow, or [Access Manager via Lerian Console](/en/platform/access-manager/using-access-manager-with-midaz-console) for the visual flow. > [!TIP] > > Auth and Identity should be deployed, healthy, and validated before you flip any product into enforcement mode. ## Next steps *** * [Enabling Access Manager](/en/platform/access-manager/enabling-access-manager) - turn on enforcement in each Lerian product. * [Using Access Manager](/en/platform/access-manager/using-access-manager) - request tokens, manage users and applications, configure MFA. * [Access Manager chart README](https://github.com/LerianStudio/helm/tree/main/charts/plugin-access-manager) - installation and upgrade. * [How Access Manager works](/en/platform/access-manager/am-components) - a closer look at Auth, Identity, and product-level enforcement. # Using Access Manager Source: https://docs.lerian.studio/en/platform/access-manager/using-access-manager Use the Access Manager APIs to configure human access, machine-to-machine credentials, MFA challenges, and token-based authentication in your environment. Access Manager is how you decide who reaches your Lerian products and how systems prove who they are before they call a protected API. This guide walks through that setup with the APIs. If you'd rather work visually for the common user and application tasks, use [Access Manager via Lerian Console](/en/platform/access-manager/using-access-manager-with-midaz-console) instead. ## Before you start *** First, make sure each product's route-level Auth client is enabled and configured with an Auth address. Protected routes then expect an `Authorization` header carrying a valid bearer token. ```text theme={null} Authorization: Bearer ``` > [!WARNING] > > A product with its Auth client enabled and configured rejects requests without a valid bearer token on its protected routes. This applies even if the endpoint was previously reachable without authentication. On Lerian Cloud, that token also carries your tenant context, so you never pass tenant identifiers in payloads or headers yourself. With the Identity APIs, the token is also your tenant boundary. List endpoints return only the users, groups, and applications in your tenant. Create, update, and delete operations stay inside it. ## Human access *** Follow this flow when a person needs to access Lerian products. **1. Inspect available groups** Use [List Groups](/en/reference/platform/access-manager/list-groups) to see the groups available in your environment. On Lerian Cloud, the list covers only your tenant. Use the returned group IDs as-is when you create or update users. **2. Review the permission surface** Some products use HTTP-method-style actions, while others use semantic actions such as `read`, `write`, `create`, or `process`. Use the permissions returned by the API instead of deriving permission names from endpoint paths. **3. Create the user** Use [Create a User](/en/reference/platform/access-manager/create-user) and assign the correct groups during creation. Group assignment defines what the user can access. For example, assigning a read-only Midaz group lets the user inspect Midaz resources without changing them. **4. Request a user token** Use [Request an Access Token](/en/reference/platform/access-manager/request-access-token) with the `password` grant type. Use the returned access token as the bearer token when the user calls protected APIs. **5. Refresh the token when needed** Use [Refresh the Access Token](/en/reference/platform/access-manager/refresh-token) to exchange a valid refresh token for a new access token. ### User management endpoints Use these endpoints to maintain human access over time: * [List Users](/en/reference/platform/access-manager/list-users): list users. * [Retrieve User Details](/en/reference/platform/access-manager/retrieve-user-details): inspect a user. * [Update a User](/en/reference/platform/access-manager/update-user): update user information and group assignments. * [Delete a User](/en/reference/platform/access-manager/delete-user): remove user access. * [Reset a User Password](/en/reference/platform/access-manager/reset-user-password): reset a user's password through an administrative flow. * [Update a User Password](/en/reference/platform/access-manager/update-user-password): update a user's password with the current password and the new one. ## Machine-to-machine access *** When a service, job, or integration needs to call Lerian APIs without a human in the loop, give it its own application. **1. Create an application** Use [Create an Application](/en/reference/platform/access-manager/create-an-application) to create credentials for the integration. Each integration should have its own application. This makes credential rotation and access review easier. The response includes the `clientId` and `clientSecret` used by Auth in the `client_credentials` flow. Store the secret at once: later reads of the application do not return it. **2. Review or manage the application** Use [List Applications](/en/reference/platform/access-manager/list-applications), [Retrieve Application Details](/en/reference/platform/access-manager/retrieve-application-details), or [Delete Application](/en/reference/platform/access-manager/delete-application) when you need to review or remove machine-to-machine access. Identity hides internal applications from the list. On Lerian Cloud, it only returns applications of your tenant. **3. Request an application token** Use [Request an Access Token](/en/reference/platform/access-manager/request-access-token) with the `client_credentials` grant type. Use the returned access token as the bearer token for the integration's API calls. ### Application catalog Identity accepts only application names from its catalog when it creates or deletes machine-to-machine applications. [List Available Applications](/en/reference/platform/access-manager/list-available-applications) returns the catalog, with the name and display name of each application. ## Provider setup *** Use an email provider when an application needs email delivery for MFA. Browser SSO uses a separate OAuth-provider and SSO-policy configuration. See the [Identity service](/en/platform/access-manager/identity-plugin). 1. Create or review a provider with the [Providers API](/en/reference/platform/access-manager/list-providers). 2. Link the provider to the application with [Link Provider to Application](/en/reference/platform/access-manager/link-provider-to-application). 3. If the application has multiple linked providers, use [Set Default Application Provider](/en/reference/platform/access-manager/set-default-application-provider) to select the default provider. Use the application-provider endpoints when you need to list, update, unlink, or reorder provider links for an application. ## MFA setup *** Use MFA for users who need an additional login verification step. **1. Start setup** Use [Initiate MFA Setup](/en/reference/platform/access-manager/initiate-mfa-setup) on the signed-in user's own account and selected MFA method. **2. Verify setup** Use [Verify MFA Passcode](/en/reference/platform/access-manager/verify-mfa-passcode) to confirm the method. **3. Enable MFA** Use [Enable MFA](/en/reference/platform/access-manager/enable-mfa) after setup verification. **4. Manage MFA over time** Use [Get MFA Status](/en/reference/platform/access-manager/get-mfa-status), [Set Preferred MFA Method](/en/reference/platform/access-manager/set-preferred-mfa-method), or [Disable MFA](/en/reference/platform/access-manager/disable-mfa) as the user's access requirements change. During login, users with MFA enabled may need to complete [Initiate MFA Challenge](/en/reference/platform/access-manager/initiate-mfa-challenge) with the MFA token and selected method, then [Verify MFA Login](/en/reference/platform/access-manager/verify-mfa-login), before receiving usable access tokens. Administrative MFA changes use separate administrative operations. ## User information and session control *** Once a user is active, a few endpoints help inspect and control that session. [Retrieve User Information](/en/reference/platform/access-manager/retrieve-user-information) returns their OIDC-compatible profile, and [Retrieve User Permissions](/en/reference/platform/access-manager/retrieve-user-permissions) shows the resources and actions they can reach. To end a session, call [End User Session](/en/reference/platform/access-manager/end-user-session) with the `id_token_hint` form field from that session. This endpoint does not select another user by ID. ## Permission checks *** Protected products call Auth with the resource and action they need to enforce. Use [Validate User Permission](/en/reference/platform/access-manager/validate-user-permission) when an integration needs to check an access decision explicitly. ```json theme={null} { "resource": "reports", "action": "get" } ``` The response tells you whether the authenticated subject is authorized for that resource-action pair. ## Tenant access rules on Lerian Cloud *** On Lerian Cloud, Access Manager resolves your tenant from your token or application. For password grants and SSO, Auth finds the tenant from the user's email address. A person with accounts in several tenants signs in to their default tenant. If the person has no default tenant, Auth selects one. Use [List Own Tenants](/en/reference/platform/access-manager/list-own-tenants) and [Set Own Default Tenant](/en/reference/platform/access-manager/set-own-default-tenant) to see the tenants and change the default. Identity accepts a new user, or a change to the email of a user, only when the email domain matches the `domain` field of your organization. It refuses the request with `IDE-0034` when the domain is different or when that field is empty. So don't add tenant IDs to Identity or Auth payloads unless an endpoint explicitly documents that field. After authentication, bearer-token claims scope normal Identity management and permission calls. Password and client-credentials grants resolve tenant context before a bearer token exists. # Platform services Source: https://docs.lerian.studio/en/platform/platform-introduction Console, Access Manager, Streaming Hub, Multi-tenancy, and Observability: the services that serve every Lerian product, what each one owns, and what you keep. Console, Access Manager, Streaming Hub, Multi-tenancy, and Observability are platform services. They are not products themselves. In BYOC you deploy them into your own cloud or data center alongside the products. In Lerian Cloud, Lerian operates them for you. ## The five services *** > **[Lerian Console](/en/platform/console/about-lerian-console)** > > One web interface for the products that ship a Console module. > **[Access Manager](/en/platform/access-manager)** > > Authentication and authorization for protected Lerian products: tokens, users, groups, applications, and MFA. > **[Streaming Hub](/en/platform/streaming-hub/what-is-streaming-hub)** > > Delivers platform events to destinations you own: webhook, pull cursor, SQS, RabbitMQ, and EventBridge. It consumes the internal stream and never produces onto it. > **[Multi-tenancy](/en/platform/multi-tenancy)** > > One deployment serves independent tenants. Each product derives tenant context according to its own authentication contract. > **[Observability](/en/platform/observability)** > > Products export traces, metrics, and logs over OpenTelemetry to a collector. ## How the services sit around the products *** ```mermaid theme={null} flowchart LR App["Your applications and operators"] Console["Lerian Console"] AM["Access Manager
Auth and Identity"] Products["Lerian products"] Hub["Streaming Hub"] Sinks["Your webhook, queue, or cursor"] Otel["Your collector and telemetry backend"] IdP["Your identity provider (optional)"] App -->|"presents a token"| Products Products -->|"authorizes subject and action"| AM Console -->|"manages users and applications"| AM Console -->|"operates the modules"| Products Products -.->|"publish events"| Hub Hub -.->|"delivers, at least once"| Sinks Products -.->|"OTLP traces, metrics, logs"| Otel IdP -.->|"as an upstream provider"| AM classDef lerian fill:#fff8e1,stroke:#f4b400,color:#1f1f1f; classDef yours fill:#e8f0fe,stroke:#4285f4,color:#1f1f1f; classDef ext fill:#f1f3f4,stroke:#9aa0a6,color:#1f1f1f; class Console,AM,Products,Hub lerian; class App,Sinks,Otel,IdP yours; ``` The diagram shows five interactions. 1. Your application sends a bearer token to a product route that Access Manager protects. The product asks Access Manager whether that subject may perform the route's action, then admits or rejects it. 2. Lerian Console reads and writes access data through Access Manager, and operates each product module. 3. Streaming Hub matches events on the internal stream against your subscriptions and delivers each match. 4. Products push traces, metrics, and logs over OTLP to the collector address you set. 5. Your identity provider, when you run one, plugs into Access Manager as an upstream provider. ## Services and products *** | Service | Midaz | Matcher | Flowker | Lender | Reporter | Streaming Hub | Brazil rails | | - | - | - | - | - | - | - | - | | Access Manager | protected routes | bearer token on protected routes | protected routes | protected routes, public jurisdiction reads | resource and action per route | the control plane | bearer token on protected routes, default differs per rail | | Console | Midaz Module, with the plugins the Plugins screen shows | module | module | | module | module | Pix Indirect (BTG), Payments, and, when enabled, Bank Transfer (TED JD) | | Streaming Hub | fact topics | | workflow trigger | fact topics | report completion facts | — | per rail | | Multi-tenancy | organizations, ledgers, accounts, transactions | reconciliation resources | per-tenant database connections | tenant from the validated identity | data sources and reports | subscriptions isolated by tenant | per rail | | Observability | OTLP export | | OTLP export | telemetry toggle and endpoint | OTLP push | OTLP export | OTLP export | ## What you bring and what Lerian ships *** You bring: * Cloud infrastructure and Kubernetes clusters. * Database administration and backups. * Network security, container patching, and secrets management. * Your monitoring pipelines and the collector that receives telemetry. * The destinations Streaming Hub delivers to. * Your identity provider. It connects to Access Manager as an upstream provider and does not replace the identity backend. Lerian ships: * Helm charts for supported products and infrastructure components, through an OCI-compatible registry. * Terraform examples for the base infrastructure Midaz needs on AWS, GCP, or Azure. * Lifecycle Management for Enterprise BYOC: an agent in your cluster, through which Lerian installs and upgrades products. * Access Manager and Lerian Console, as Enterprise features. The Midaz chart lets you disable a bundled Valkey, PostgreSQL, MongoDB, or RabbitMQ and use an external service instead. ## Adopt one service at a time *** **1. Access Manager** Get the Auth and Identity services running with Caradhras, PostgreSQL, and Valkey. Then turn enforcement on per product. Install first, validate, then enable. **2. Lerian Console** Point the Console at Access Manager. Its user and application screens read and write that data. **3. Streaming Hub** Deploy the dedicated `streaming-hub-helm` chart, separate from any product chart. Its API routes require an Access Manager token. **4. Observability** Deploy a collector in your Kubernetes cluster. Then configure the product workloads that support it to send telemetry to it. Multi-tenancy is not a step in that order. Lerian Cloud provides it. ## Deploy and operate *** > **[Helm charts](https://github.com/LerianStudio/helm)** > > Install and upgrade the published charts. > **[Lifecycle Management](/en/platform/lifecycle-management)** > > Lerian installs and upgrades products in your cluster through an agent that you install. > **[Licensing](/en/platform/licensing/license-manager)** > > How a Lerian product validates its license key, and what each result allows. > **[Terraform examples](/en/products/midaz/midaz-terraform-foundation)** > > Create the network, DNS, database, and Kubernetes base Midaz needs on AWS, GCP, or Azure. ## Build with Narya *** Narya runs coding agents behind one HTTP API. One long-lived host on your machine owns the sessions, the store and the model connections. Lerian ships a terminal client and a one-shot command on top of it, and you can write your own client against the same contract. > **[Narya](/en/platform/narya/what-is-narya)** > > What Narya is, how you run it, and how it fits a pipeline. > **[The host API](/en/platform/narya/the-host-api)** > > One contract for every client: socket, event stream, envelopes. ## Where to go next *** [Deployment models](/en/start-here/evaluate-and-deploy/deployment-models) · [Multi-tenancy](/en/platform/multi-tenancy) · [What is Access Manager?](/en/platform/access-manager) · [What is Lerian Console?](/en/platform/console/about-lerian-console) · [Streaming Hub quick start](/en/platform/streaming-hub/streaming-hub-quick-start) # Compliance Source: https://docs.lerian.studio/en/products/flowker/compliance How Flowker supports regulatory compliance with validation before the ledger write and a step-level record of every workflow execution. Flowker helps compliance teams answer two questions: was the transaction validated before it reached the ledger, and what did each step of that validation do? ## Validation before the ledger *** Flowker runs a workflow's nodes by following its graph connections, and it has no join node that waits for parallel branches to converge. To place checks before the node that writes to the ledger, build a linear validation chain, and: * The ledger write runs only after each earlier check in that chain returns a result * To stop before the write, configure no outgoing edge whose `sourceHandle` matches the conditional's failing outcome. If the failing branch must run notification or audit nodes, ensure that branch has no path to the ledger-write node. Flowker follows only the edge whose `sourceHandle` matches the evaluated outcome. If no edge matches, that branch ends. * You do not depend on post-write validation and the corrections it forces * The execution record shows the sequence: the checks that ran, their verdicts, then the write This is a preventive design. You place the checks ahead of the write, and Flowker preserves that order in the linear chain. Do not use a fan-out and later merge as a synchronization barrier: a node after the merge can run before another branch finishes. ## Execution records *** Each workflow execution keeps a step-level record of what ran. For every step, the record holds the step name and number, when it ran, and its status. It also holds how long the step took, the output it returned, and the error message if it failed. * Use [List executions](/en/reference/products/flowker/list-executions) to find executions. Filter by workflow and by status, then page through the results with a cursor. * Use [Get execution results](/en/reference/products/flowker/get-execution-results) to see an execution's step details after it reaches a terminal status (`completed` or `failed`). It returns an in-progress error for `pending` or `running` executions. ## What's next *** > **[Security](/en/products/flowker/security)** > > Learn about Flowker's authentication model, encryption, and resilience features. > **[Executions API](/en/reference/products/flowker/list-executions)** > > Query execution history and step-level results through the API. # Configuring a webhook trigger Source: https://docs.lerian.studio/en/products/flowker/configuring-a-webhook-trigger Start a Flowker workflow from an inbound HTTP call. Choose the payload contract, decide how the webhook answers, and confirm the route serves your caller. A webhook trigger is the entry point of a workflow that starts from an inbound HTTP call. You declare a path and a method on the trigger node. When you activate the workflow, Flowker serves that path. Each fresh accepted call starts a workflow execution. A repeat with the same `Idempotency-Key` returns the existing execution instead. The optional `input_contract` decides which payloads Flowker accepts and how it decodes them. Set it explicitly for new nodes: it fixes the payload format for the whole route. Older nodes that omit it remain valid. Flowker derives `xsd` when they carry an XSD schema and XML format. Otherwise Flowker treats them as `open` with JSON as the default format. ## Before you start *** * A workflow in `draft` status. An active workflow is locked, so add the trigger before you activate it. See [Step 4: Run the workflow](/en/products/flowker/integration-guide#step-4-run-the-workflow) for the create-and-activate path. * Grant the `execute` permission on the `webhooks` resource to every system you let call the path. See [Securing a webhook](/en/products/flowker/integration-guide#securing-a-webhook). * For the `xsd` contract: an XSD document in the registry. Upload it with [Upload an XSD schema](/en/reference/products/flowker/upload-xsd-schema) and keep the id it returns. * For the `openapi` contract: an OpenAPI document in the registry ([Upload an OpenAPI schema](/en/reference/products/flowker/upload-openapi-schema), covered end to end in [Connecting your own API](/en/products/flowker/connecting-your-own-api)). You also need the path and the method of the operation whose request body describes your payload. [Derive an operation schema](/en/reference/products/flowker/derive-openapi-operation-schema) shows you that request body. * For a Streaming Hub webhook subscription: set its `endpoint` to [Trigger a Webhook from Streaming Hub](/en/reference/products/flowker/trigger-webhook-from-streaming-hub). That route starts the same workflow and checks the Hub signature instead of a bearer token. The deployment must turn it on, and the subscription's signing secret must match the one the deployment holds for your tenant. ## Step 1: Read the trigger contract from the catalog *** Triggers are built in. You discover them in the catalog, and you never create one. **1. List the built-in triggers** [List catalog triggers](/en/reference/products/flowker/list-catalog-triggers) returns each trigger with its `id`, `name` and `version`. The webhook trigger's id is `webhook`. ```bash theme={null} curl -s https://your-flowker-host/v1/catalog/triggers \ -H "Authorization: Bearer " | jq . ``` **2. Read the webhook trigger's schema** [Get a catalog trigger](/en/reference/products/flowker/get-catalog-trigger) returns the same fields plus `schema`, the JSON Schema Flowker validates your trigger node against. Read it when you want the field list from the running instance. ```bash theme={null} curl -s https://your-flowker-host/v1/catalog/triggers/webhook \ -H "Authorization: Bearer " | jq -r '.schema' | jq . ``` ## Step 2: Choose the input contract *** | Mode | What the route accepts | What Flowker does with the payload | Fields the mode requires | | - | - | - | - | | `open` | JSON or XML, as you declare in `format` | Decodes the body and runs no contract validation. | `format`: `"json"` or `"xml"` | | `xsd` | XML | With XSD validation configured, validates the document against the XSD schema you referenced. Without it, Flowker decodes well-formed XML but skips XSD validation. | `xsd_schema_id` | | `openapi` | JSON | Validates the payload against exactly one operation of the OpenAPI document you referenced, and rejects a payload that does not conform. | `openapi_schema_id`, `operation_path`, `operation_method` | The mode fixes the route's payload format. An `xsd` route is XML and an `openapi` route is JSON. An `open` route uses the `format` you declare. Omit `format` on `xsd` and `openapi`. A value other than `"xml"` on `xsd`, or other than `"json"` on `openapi`, fails the save with `FLK-0934`. Choose `open` when the caller's payload has no published contract, or when you want the workflow itself to decide what is acceptable. When a partner sends XML that an XSD document defines, choose `xsd`. Choose `openapi` when a partner sends JSON and you hold the OpenAPI document that describes it. > [!NOTE] > > An `openapi` route never accepts an unchecked payload: when Flowker cannot reach a verdict, it rejects the call with `FLK-0720`, and the workflow never sees that payload. When XSD validation is configured, an `xsd` route reaches its verdict. A document that does not conform is rejected with `XML_VALIDATION_FAILED`. Flowker rejects a verdict it cannot trust with `FLK-0720`. ## Step 3: Decide how the webhook answers *** | `response_mode` | What the caller receives | | - | - | | `async` (default) | For a fresh, non-terminal execution, HTTP `202` with the execution receipt as soon as it starts. The workflow continues in the background, and the caller reads the outcome from [Get execution results](/en/reference/products/flowker/get-execution-results). If the resolved execution is already terminal, including an `Idempotency-Key` replay, Flowker returns the receipt with HTTP `200` and replay metadata. | | `sync` | Flowker holds the connection until the execution reaches a terminal state, for up to 15 seconds, then returns the outcome. If the window closes first, the caller receives the same `202` receipt plus a `Location` header pointing at the results endpoint. | On a `sync` route, `response_view` shapes the body: | `response_view` | Body | | - | - | | `full` (default) | The complete execution-results envelope: `executionId`, `workflowId`, `status`, `stepResults`, `finalOutput` when available, `startedAt`, optional `completedAt` and `inputData`, plus idempotency replay metadata when applicable. | | `final_output` | For a completed execution, only the final business-output map (`{}` when absent). For a failed execution, a failure object with `status: "failed"` and, when available, `errorMessage` and `errorClass`. | | `receipt` | The lean receipt: `executionId`, `workflowId`, `status` and `startedAt`. | | `passthrough` | The shape the terminal step implies: a relayed provider response, or a terminal `set_output` node's own output. When the execution failed, its last step captured no provider response, and no `set_output` node that ran set a `responseStatusCode`, the response is the `full` envelope at HTTP `200`. | `response_view` is inert on an `async` route. For the full `passthrough` rules and for the `responseStatusCode` override, see [Synchronous response mode](/en/products/flowker/integration-guide#synchronous-response-mode). > [!TIP] > > Pick `async` when the caller only needs to know the event arrived. Pick `sync` when the caller needs the answer in the same call (a partner that expects a decision on the same connection, for example). ## Step 4: Write the trigger node *** The webhook trigger is a node with `type: "trigger"` and these fields in its `data`: | Field | When you set it | Value | | - | - | - | | `triggerType` | Always | `"webhook"`. | | `path` | Always | The path to serve, such as `"payments/received"`. It carries as many segments as you need. | | `method` | Always | The method the route answers: `GET`, `POST`, `PUT`, `PATCH` or `DELETE`, in upper case. | | `input_contract` | Optional | `"open"`, `"xsd"` or `"openapi"`. Set it explicitly for new nodes. When omitted, the legacy rules apply. | | `format` | With `open` | Required with `open`: `"json"` or `"xml"`. Omit it with `xsd` and `openapi`. | | `xsd_schema_id` | With `xsd` | The id [Upload an XSD schema](/en/reference/products/flowker/upload-xsd-schema) returned. | | `openapi_schema_id` | With `openapi` | The id [Upload an OpenAPI schema](/en/reference/products/flowker/upload-openapi-schema) returned. | | `operation_path` | With `openapi` | The operation path as the OpenAPI document writes it, such as `"/orders"`. Flowker matches it exactly. | | `operation_method` | With `openapi` | The operation's method: `GET`, `POST`, `PUT`, `PATCH` or `DELETE`. | | `response_mode` | Optional | `"async"` (default) or `"sync"`. | | `response_view` | Optional | `"full"` (default), `"final_output"`, `"receipt"` or `"passthrough"`. | | `accepted_headers` | Optional | Up to 50 extra request-header names to persist in `_webhook.headers`, in every contract mode. Names must be unique case-insensitively; each must be a non-blank HTTP header name, at most 256 characters. The fixed denylist rejects `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`, `X-Auth-Token` and `X-Webhook-Signature`; it does not detect credentials in other headers. Never allowlist another secret-bearing name such as `X-Amz-Security-Token`. | The trigger configuration is a closed contract. A save fails with `FLK-0934` when the webhook trigger: * omits `path` or `method` * misses a field its selected `input_contract` mode requires * names another mode's schema id or operation field * carries a key or value the schema rejects An `accepted_headers` name that is not a valid header name, repeats another name or is on the denylist fails with `FLK-0957` instead. ```json open JSON theme={null} { "id": "trigger-1", "type": "trigger", "name": "Payment received", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "webhook", "path": "payments/received", "method": "POST", "input_contract": "open", "format": "json" } } ``` ```json open XML theme={null} { "id": "trigger-1", "type": "trigger", "name": "Statement received", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "webhook", "path": "statements/received", "method": "POST", "input_contract": "open", "format": "xml" } } ``` ```json xsd theme={null} { "id": "trigger-1", "type": "trigger", "name": "STR0008 received", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "webhook", "path": "spb/str0008", "method": "POST", "input_contract": "xsd", "xsd_schema_id": "0f9a1c3e-5b7d-4c2a-9e18-6d4b2f7a1c05" } } ``` ```json openapi theme={null} { "id": "trigger-1", "type": "trigger", "name": "Order paid", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "webhook", "path": "orders/paid", "method": "POST", "input_contract": "openapi", "openapi_schema_id": "3c7e9b21-84af-4d6c-b0f1-2a5c8e93d7b4", "operation_path": "/orders", "operation_method": "POST" } } ``` ```json sync response theme={null} { "id": "trigger-1", "type": "trigger", "name": "Authorize payment", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "webhook", "path": "payments/authorize", "method": "POST", "input_contract": "open", "format": "json", "response_mode": "sync", "response_view": "passthrough" } } ``` Flowker registers the path with a leading slash and without a trailing one, so `payments/received`, `/payments/received` and `payments/received/` all register the same route. ## Step 5: Activate the workflow *** **1. Create the workflow** Send the node with the rest of your workflow to [Create a workflow](/en/reference/products/flowker/create-workflow). The workflow lands in `draft` status, and Flowker validates the trigger configuration here. A contract error answers `FLK-0934`. **2. Activate it** Call [Activate a workflow](/en/reference/products/flowker/activate-workflow). Activation registers the path and the method. It also resolves what the contract references. A missing XSD schema answers `FLK-0930` and a missing OpenAPI schema `FLK-0931`. An operation the document does not declare answers `FLK-0932`, and an operation with no request body answers `FLK-0933`. ```bash theme={null} curl -s -X POST https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/activate \ -H "Authorization: Bearer " | jq . ``` One active workflow owns a path and method pair within your tenant. Activating a second workflow on the same pair answers `FLK-0360`. [Deactivating a workflow](/en/reference/products/flowker/deactivate-workflow) releases its routes, so you can hand a path to a new version. ## Step 6: Call the route and confirm it works *** Send the call the way your caller will: ```bash theme={null} curl -i -X POST https://your-flowker-host/v1/webhooks/payments/received \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b" \ -d '{ "transactionId": "txn-123", "amount": 1500.00 }' ``` A fresh `async` route whose execution is not terminal answers `202` with the receipt: ```json theme={null} { "executionId": "019c96a0-10ce-75fc-a273-dc799079a99c", "workflowId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", "status": "running", "startedAt": "2026-03-18T14:35:00Z" } ``` A `sync` route answers with the execution's outcome, in the shape its `response_view` selects. The status it carries depends on the view and on how the execution ended. [Synchronous response mode](/en/products/flowker/integration-guide#synchronous-response-mode) holds those rules. Three signals tell you the route worked: * A response that started an execution carries `X-Webhook-Workflow-ID` and `X-Webhook-Execution-ID`, so you can tie a call to the workflow it reached and the execution it started. * [Get execution results](/en/reference/products/flowker/get-execution-results) reports the step results and the final output for that `executionId`. * The execution's input carries a `_webhook` object with the method, the path and the caller's address. Use it to confirm the workflow saw the call it should. See [Webhook metadata](/en/products/flowker/integration-guide#webhook-metadata). A repeat delivery with the same `Idempotency-Key` returns the original execution instead of starting another one. On an `async` route, a terminal replay returns an HTTP `200` receipt with `idempotencyReplayed: true` and the original status. On a `sync` route, the status and body follow `response_view` and any terminal `responseStatusCode`: `full` and `receipt` include replay metadata, while `final_output` and a direct `passthrough` response do not guarantee it. Send a new key to run the workflow again. The five verbs each have their own reference page: [POST](/en/reference/products/flowker/trigger-webhook), [GET](/en/reference/products/flowker/trigger-webhook-get), [PUT](/en/reference/products/flowker/trigger-webhook-put), [PATCH](/en/reference/products/flowker/trigger-webhook-patch) and [DELETE](/en/reference/products/flowker/trigger-webhook-delete). ## When a call fails *** | Code | When it happens | What to do | | - | - | - | | `FLK-0934` | You save the workflow. | Compare the trigger `data` against the field table in [Step 4](#step-4-write-the-trigger-node). Check this code first when a webhook trigger does not save. | | `FLK-0930` … `FLK-0933` | You activate the workflow. | Confirm the schema id, and for `openapi` confirm the operation path and method exist in the document and that the operation declares a request body. | | `FLK-0360` | You activate the workflow. | Another active workflow owns that path and method. Choose another path, or deactivate the other workflow. | | `FLK-0361` | The caller sends a request. | No route answers that path and method. Confirm the workflow is active, and that the caller uses the method the trigger declares. | | `FLK-0501` | The caller sends a request. | The workflow resolved but is not active. Activate it. | | `FLK-0001` | The caller sends a request. | The body of a JSON route is not valid JSON. | | `XML_MALFORMED` | The caller sends a request. | The body of an XML route is not well-formed XML. XML routes answer with this value in the `` element; no `FLK-` code is returned for it. | | `XML_VALIDATION_FAILED` | The caller sends a request. | The body of an `xsd` route is well-formed XML but does not conform to the XSD document. The `` document names the failing line and column. | | `FLK-0935` | The caller sends a request. | The JSON body of an `openapi` route does not conform to the pinned operation's request body. The message identifies the first failing JSON Pointer and includes the validation detail when available. | | `FLK-0720` | The caller sends a request. | Flowker could not validate the payload, so it rejected the call. Confirm the referenced schema exists in the registry. If it does and the call still fails, contact Lerian support. | | `FLK-0363` | The caller sends a request. | The body is over 1 MB. Send less in one call. | After Flowker resolves a route, JSON-route errors return `code`, `title` and `message`, while XML-route errors return an `` document. The `FLK-0363` body-size check runs before route resolution, so it returns the JSON error envelope for every request. See the [Flowker error list](/en/reference/products/flowker/flowker-error-list) for every code and both shapes. ## What's next *** > **[Integration guide](/en/products/flowker/integration-guide)** > > Connect the workflow to external services, and read the full synchronous-response rules. > **[Workflow design guide](/en/products/flowker/workflow-design-guide)** > > Build the rest of the graph the trigger enters. # Connecting your own API Source: https://docs.lerian.studio/en/products/flowker/connecting-your-own-api Upload your service's OpenAPI document to Flowker and call its operations from a workflow node. Register the document, point a provider configuration at it, and address one operation per node. Flowker ships connectors for the services in its catalog. The service you want to call can also be your own: an internal API, a partner API, anything with a published OpenAPI document. You upload that document, and a workflow node calls its operations directly. You do this once per document. Upload it, create one provider configuration that points at it, then address one operation from each node that calls the service. ## Before you begin *** * Your service's OpenAPI 3.x document as a file, at most 8 MiB, declaring at least one operation. * The credentials your service requires, and the authentication method it expects. See [Authentication](/en/products/flowker/integration-guide#authentication) for the methods Flowker supports. * A workflow in `draft` status to edit. An active workflow is locked. Deactivate it first, then move the inactive workflow to `draft` before editing and activating it again. > [!TIP] > > The Lerian Console covers the same path. In **Providers → + New Provider → Add your own API**, you select an uploaded document and set the base URL and the authentication. See [Adding a provider](/en/products/flowker/console/adding-a-provider). ## Step 1: Upload the OpenAPI document *** **1. Send the file** Call [Upload an OpenAPI schema](/en/reference/products/flowker/upload-openapi-schema) as `multipart/form-data` with three parts: the `file`, a `name`, and a `version`. ```bash theme={null} curl -X POST https://your-flowker-host/v1/openapi-schemas \ -H "Authorization: Bearer $TOKEN" \ -F "file=@acme-kyc.json" \ -F "name=acme-kyc" \ -F "version=v1.0.0" ``` **2. Keep the id** The `201` response describes what Flowker read out of the file. Its `id` is the value every later step references. | Field | What it tells you | | - | - | | `id` | The document's identifier. A provider configuration and a webhook trigger point at it. | | `name`, `version` | The pair you sent. | | `title` | The `info.title` of the document. | | `openapiVersion` | The `openapi` version the document declares. | | `operationCount` | How many path-and-method operations the document declares. | | `contentHash`, `byteSize` | The stored file's digest and size. | | `createdBy`, `createdAt` | Who uploaded it and when. | ### What a stored document is keyed by `name` and `version` are yours to choose, up to 255 characters each. The pair is unique in your tenant: uploading the same `name` and `version` again answers `FLK-0812`. The `id` Flowker returns is a fresh UUID for each upload. Everything else references that id, never the name or the version. Flowker parses the file before it stores it. A file that is not an OpenAPI 3.x document, or one that declares no operation, answers `FLK-0900`. A file over 8 MiB answers `FLK-0901`. Your uploaded documents are yours alone. A document is only visible to the tenant that uploaded it, and an id from another tenant never resolves. ## Step 2: Read the operations you can call *** **1. List what you have stored** [List OpenAPI schemas](/en/reference/products/flowker/list-openapi-schemas) returns your documents as metadata only, without their content. It is paginated: `limit`, `cursor`, `sortBy` and `sortOrder`, and the response carries `nextCursor` and `hasMore`. **2. Read one document's operations** [Get an OpenAPI schema](/en/reference/products/flowker/get-openapi-schema) returns the same metadata plus `content` (the stored file) and `operations`, one entry per operation the document declares. | Field | What it tells you | | - | - | | `path` | The operation path, exactly as the document writes it, such as `/v1/checks`. | | `method` | The operation's HTTP method. | | `operationId` | The document's `operationId`, when it declares one. | | `hasRequest` | Whether the operation declares a JSON request body. | | `hasResponse` | Whether the operation declares a JSON success response. | Copy the `path` and the `method` of the operation you want. Step 4 puts them on the node. **3. Read one operation's field names** [Derive an operation schema](/en/reference/products/flowker/derive-openapi-operation-schema) takes a `path` and a `method` and returns `inputSchema` for the operation's `application/json` request body and `outputSchema` for its first 2xx `application/json` response. Either field is absent when the document declares no such schema. For a non-JSON request body, use `hasBody`, `bodyRequired`, and `bodyContentType`. It also returns `params`, one entry per parameter the operation declares, each with its `name`, its `in` location and whether it is `required`. Those are the field names you write as mapping targets and sources in Step 4. Both query parameters are required, and `method` is case-insensitive and must be one of `GET`, `PUT`, `POST`, `DELETE`, `OPTIONS`, `HEAD`, `PATCH` or `TRACE`. A missing `path` or an unrecognised `method` answers `FLK-0304`. A path and method the document does not declare answers `FLK-0902`. ## Step 3: Point a provider configuration at the document *** Call [Create a provider configuration](/en/reference/products/flowker/create-provider-configuration) with `kind` set to `external_openapi`. That kind references your uploaded document instead of a catalog provider. | Field | Required | Description | | - | - | - | | `kind` | Yes | `"external_openapi"`. You choose the kind when you create the configuration, and it stays the kind the configuration was created with. | | `providerId` | No | Omit it. This connection targets your own document, not a catalog provider. A read of the configuration then returns the reserved id `external.openapi`. | | `name` | Yes | A name for this connection, 1–100 characters. | | `config.openapi_schema_id` | Yes | The `id` from [Step 1](#step-1-upload-the-openapi-document). It must name a document in your tenant. | | `config.base_url` | No | The scheme, host and path prefix Flowker sends requests to. Omit it to use the first usable `servers` entry of the document. | | `config.auth` | No | A `{ type, config }` authentication block, in the same shape every provider configuration uses. See [Authentication](/en/products/flowker/integration-guide#authentication) for each type and its fields. Omit it for a service that needs no authentication. | | `config.headers` | No | Static HTTP headers for every request through this connection. It must be an object of valid, non-blank header names and string values; names must not collide case-insensitively. Values can use template references resolved at request time. | | `allowedHosts` | No | The hosts this configuration is allowed to call. Omit it, or send an empty list, to accept any public host. | | `allowedPrivateHosts` | No | Named private hosts this configuration is allowed to reach. Cloud metadata and link-local addresses stay blocked. | | `schemaBindings` | No | The stored documents this configuration binds to. See [Bind the document](#bind-the-document). | | `description` | No | Free text, up to 500 characters. | | `metadata` | No | Your own key-value pairs. | > [!WARNING] > > `config.headers` is stored with the provider configuration and can be returned by configuration reads. Never place API keys, tokens, cookies, or other secrets there. Put credentials in `config.auth`. ### Where the credential goes A read never returns the credential the authentication type declares. To rotate a secret later, send the new value in an update. To keep the current one, omit the field or send it blank while `auth.type` stays the same. See [Authentication](/en/products/flowker/integration-guide#authentication). ### Where the host allow-lists are set Both allow-lists belong to this create call, and to [Update a provider configuration](/en/reference/products/flowker/update-provider-configuration) afterwards. The `allowedHosts` list names the hosts every node that calls through this configuration may reach. Flowker checks the request URL and each redirect hop against it at run time. An entry with a leading dot matches subdomains, so `.acme-kyc.example.com` matches `api.acme-kyc.example.com`. Entries are host names only, with no IP literal, no wildcard and no port. `allowedPrivateHosts` is the companion list for a service that lives on a private network. It does not override `allowedHosts`: when `allowedHosts` is non-empty, it must also include the private host. A matching `allowedPrivateHosts` entry only lifts the private or loopback IP block. Cloud-metadata and link-local addresses remain blocked. ### Bind the document Add a `schemaBindings` entry for the document you referenced. Each entry names one stored document. Set `type` to `"openapi"` and `schemaId` to the same id you put in `config.openapi_schema_id`. The optional `operations` array scopes the stored binding. Flowker validates that array against the document when you save the configuration. That array does not verify what workflow nodes call, and it does not limit an `external_openapi` node at execution time. The node uses `config.openapi_schema_id`, `operation_path`, and `operation_method`. The binding makes the document's dependants visible. With it, [List resources referencing an OpenAPI schema](/en/reference/products/flowker/list-openapi-schema-references) reports this configuration, and a delete of the document is refused while the configuration is active. See [Removing a document](#removing-a-document). Flowker resolves every binding when you save. A `schemaId` that names no document in your tenant answers `FLK-0942`, and an `operations` entry the document does not declare answers `FLK-0943`, each naming the failing entry. A malformed entry answers `FLK-0293`. Malformed means an unknown `type`, a `schemaId` that is not a UUID, `operations` on a binding that is not `openapi`, or an operation with no path or method. > **Example request** > > ```json theme={null} > POST /v1/provider-configurations > > { > "name": "Acme KYC production", > "description": "Production KYC checks", > "kind": "external_openapi", > "config": { > "openapi_schema_id": "018f3e2a-1c4d-7b9e-a1b2-c3d4e5f6a7b8", > "base_url": "https://api.acme-kyc.example.com", > "auth": { > "type": "api_key", > "config": { > "key": "sk-live-xxx", > "header_name": "X-API-Key", > "location": "header" > } > } > }, > "allowedHosts": ["api.acme-kyc.example.com"], > "schemaBindings": [ > { > "type": "openapi", > "schemaId": "018f3e2a-1c4d-7b9e-a1b2-c3d4e5f6a7b8", > "operations": [ > { "path": "/v1/checks", "method": "POST" }, > { "path": "/v1/checks/{checkId}", "method": "GET" } > ] > } > ] > } > ``` > > The response returns the new configuration's `id`. Keep it. [Step 4](#step-4-address-an-operation-from-a-workflow-node) puts it in the `providerConfigId` of every node that calls this service. Flowker checks the configuration before it stores it. A `config` with no `openapi_schema_id`, or one whose value is not a UUID, answers `FLK-0946`. An id that names no document in your tenant answers `FLK-0947`. A `config.auth` block Flowker cannot read (an unknown type, or a type missing one of its required fields) answers `FLK-0948`. A malformed `config.headers` block answers `FLK-0955`. Flowker does not call your target API here. ## Step 4: Address an operation from a workflow node *** An executor node names one operation of the document with two fields in its `data`, alongside the `providerConfigId` of the configuration from Step 3. | Field | Required | Description | | - | - | - | | `providerConfigId` | Yes | The UUID of the provider configuration that points at the document. | | `operation_path` | To run | The operation path, exactly as the document writes it, including its `{...}` parameter templates. | | `operation_method` | To run | The operation's HTTP method. Matching is case-insensitive. | | `inputMapping` | No | Moves values from the workflow context into the request. Each `target` is a path in the operation's request body, or a parameter name. | | `outputMapping` | No | Moves values out of the response for later nodes to read. | Do not send `executorId` on such a node. Flowker resolves the provider configuration, recognises the kind, and fills the field in for you before it validates the workflow. The save path can persist a node missing either operation field, but execution then fails with `FLK-0950` before Flowker sends a request. Every other node field behaves as [Reference the provider configuration from a workflow node](/en/products/flowker/integration-guide#step-3-reference-the-provider-configuration-from-a-workflow-node) describes. ### How the request is assembled Flowker reads the operation out of the stored document at run time and builds the request from it: * **The destination** is `config.base_url` when the configuration sets it, otherwise the first usable `servers` entry of the document, joined with `operation_path`. * **A `path` parameter** takes its value from the node's resolved data first, and from the request body second. Every `path` parameter needs a value. * **A `query` or `header` parameter** resolves the same way. An optional parameter with no value is left out. A required parameter with no value fails the node before any request leaves Flowker, with `FLK-0954`. A static `config.headers` value or the configured authentication can satisfy a required header parameter. * **The request body** with the default `request_format.type` (`json`) is what your `inputMapping` builds. With `xml_converted`, Flowker serializes that mapped object as XML. With `xml_passthrough`, Flowker ignores the mapping and forwards the webhook trigger's original XML bytes. Write each `target` exactly as the operation's request schema names it. There is no wrapper object and no prefix to add. [Working with request and response data](/en/products/flowker/working-with-request-and-response-data) covers mappings and transformations in full. > **Example: a workflow that calls two operations of the document** > > ```json theme={null} > POST /v1/workflows > > { > "name": "kyc-check", > "description": "Opens a KYC check on the Acme API and records the result.", > "nodes": [ > { > "id": "kyc-received", > "type": "trigger", > "name": "KYC request received", > "position": { "x": 0, "y": 0 }, > "data": { > "triggerType": "webhook", > "path": "kyc/requested", > "method": "POST", > "input_contract": "open", > "format": "json" > } > }, > { > "id": "open-check", > "type": "executor", > "name": "Open KYC check", > "position": { "x": 200, "y": 0 }, > "data": { > "providerConfigId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", > "operation_path": "/v1/checks", > "operation_method": "POST", > "inputMapping": [ > { "source": "workflow.documentNumber", "target": "documentNumber" }, > { "source": "workflow.fullName", "target": "fullName" } > ], > "outputMapping": [ > { "source": "body.checkId", "target": "checkId" }, > { "source": "body.status", "target": "status" } > ] > } > }, > { > "id": "record-check", > "type": "action", > "name": "Record the check", > "position": { "x": 400, "y": 0 }, > "data": { > "actionType": "set_output", > "output": { > "checkId": "${open-check.checkId}", > "status": "${open-check.status}" > } > } > } > ], > "edges": [ > { "id": "e1", "source": "kyc-received", "target": "open-check" }, > { "id": "e2", "source": "open-check", "target": "record-check" } > ] > } > ``` > > The `open-check` node sends `POST https://api.acme-kyc.example.com/v1/checks` with the body its `inputMapping` built. Its `outputMapping` lifts two response fields, so the next node reads `${open-check.checkId}`. > > A node that calls `GET /v1/checks/{checkId}` instead reads the parameter from the same node scope. Map a value onto `checkId`, and Flowker substitutes it into the path. > [!TIP] > > A webhook trigger can validate the inbound payload against one operation of the same document. Set `input_contract` to `"openapi"` and give the trigger `openapi_schema_id`, `operation_path` and `operation_method`. See [Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger). ## Step 5: Run it and confirm it worked *** **1. Activate the workflow** Call [Activate a workflow](/en/reference/products/flowker/activate-workflow). Activation registers the webhook route and resolves what the trigger's contract references. **2. Run it** Call [Execute a workflow](/en/reference/products/flowker/execute-workflow) with a fresh `Idempotency-Key` header, or send a request to the webhook route. **3. Read the step results** A node that reached your service records the response under its own id. With an `outputMapping`, the mapped names sit directly under that id: `open-check.checkId`. With no `outputMapping`, the node's output keeps the response envelope, so the response body sits one level down, under `body`. ## Publishing a new version of your document *** A stored document does not change. To ship a revision, upload the file again under a new `version`. That gives you a second stored document with its own `id`. Uploading a new document does not change existing configurations. However, each executor node reads its provider configuration when it runs. Updating `config.openapi_schema_id` can change the document used by later nodes of an in-progress execution, so coordinate the cutover. Send the provider configuration's `config` with the new id through [Update a provider configuration](/en/reference/products/flowker/update-provider-configuration). The `config` field replaces the stored map rather than merging into it. Include any configured `base_url` and `auth` values that you need to retain in the same call. Flowker revalidates the new id against your tenant, and answers `FLK-0947` when it does not resolve. Check [List resources referencing an OpenAPI schema](/en/reference/products/flowker/list-openapi-schema-references) on the previous document before you retire it. The response is a display list, not a complete inventory: it returns up to 100 entries in each of its two groups. An active provider configuration beyond that display limit still blocks deletion. ## Spec versions for the services in the catalog *** Flowker's own catalog services resolve against a separate, shared registry of published specs. Three operations manage it. They never touch a document you uploaded in Step 1. | Operation | What it does | | - | - | | [List OpenAPI spec versions](/en/reference/products/flowker/list-openapi-spec-versions) | Lists the published versions of a service's spec in `versions`, and reports the one your tenant has pinned in `pinnedVersion`. `pinnedVersion` is empty when your tenant has pinned none. | | [Pin an OpenAPI spec version](/en/reference/products/flowker/pin-openapi-spec-version) | Selects the version your tenant resolves against for that service. It is idempotent: pinning again replaces the version in place. A request missing the service or the version answers `FLK-0801`. | | [Upload an OpenAPI spec version](/en/reference/products/flowker/upload-openapi-spec-version) | Publishes one version of a service's spec, for every tenant. Versions are immutable: a `service` and `version` pair that already exists answers `FLK-0803`. The caller needs the `create` permission on the `catalog` resource. | The pin is per tenant. Publishing a version changes nothing for a tenant until that tenant pins it. A new upload therefore never moves a running workflow onto a different spec. Flowker reads your pinned version when it reports that service's executor schemas in the catalog, so [Get a catalog executor](/en/reference/products/flowker/get-catalog-executor) describes the version you chose. ## Removing a document *** **1. Check what would break** [List resources referencing an OpenAPI schema](/en/reference/products/flowker/list-openapi-schema-references) returns two groups, `providerConfigurations` and `workflows`. Both are always present, each carries up to 100 entries, and each entry carries an `id`, a `name` and a `status`. It is a display list, not a complete inventory. An active provider configuration beyond the display limit still blocks the delete, while an inactive entry only warns. **2. Delete it** [Delete an OpenAPI schema](/en/reference/products/flowker/delete-openapi-schema) answers by what still references the document: | Outcome | What it means | | - | - | | `204 No Content` | Nothing referenced the document. It is gone. | | `200 OK` | Only inactive referrers held it: a draft or inactive workflow, or a disabled provider configuration. The document is gone, and the body shows its warnings under `warnings` and `providerConfigurationWarnings`; provider-configuration display details can be capped at 100. | | `409` with `FLK-0945` | An active provider configuration binds the document. Nothing is deleted, and the body shows the referring provider configurations and workflows; provider-configuration display details can be capped at 100. | | `409` with `FLK-0936` | An active workflow references the document from a webhook trigger. Nothing is deleted. | To clear a block, disable the provider configuration or deactivate the active workflow. Move an inactive workflow to `draft` only if you need to edit it. ## What goes wrong *** | Symptom | Cause | Fix | | - | - | - | | The upload is refused although the file opens in your editor. | The document declares an `openapi` version outside the 3.x series, or declares no operation. | Check the `openapi` field and the `paths` object, then upload again. | | The create call reports the schema id is unknown. | The id belongs to another tenant, or the document was deleted. | Call [List OpenAPI schemas](/en/reference/products/flowker/list-openapi-schemas) and take the id from the response. | | The workflow is refused with `FLK-0150`. | The node's `providerConfigId` names a configuration that does not exist, or one that is disabled. | Confirm the UUID on the node, and enable the configuration with [Enable provider configuration](/en/reference/products/flowker/enable-provider-configuration). | | The node fails without reaching your service. | The operation is absent from the document, or no base URL resolves. | Compare `operation_path` and `operation_method` with the `operations` list from Step 2, and set `config.base_url` when the document declares no `servers` entry. | | The service answers that a required field is missing. | A mapping `target` does not match the operation's request schema. | Read `inputSchema` from [Derive an operation schema](/en/reference/products/flowker/derive-openapi-operation-schema) and write each target exactly as it appears there. | | The service is never reached and the step reports a rejected URL. | `allowedHosts` does not cover the destination host. | Add the host to `allowedHosts` on the provider configuration. | | Error code | When | What it means | | - | - | - | | `FLK-0150` | Create or activate the workflow | The node's `providerConfigId` names a configuration that does not exist, or one that is not active. | | `FLK-0812` | Upload | A document with this `name` and `version` already exists. Choose another version. | | `FLK-0900` | Upload | The file does not parse as an OpenAPI 3.x document, or it declares no operation. | | `FLK-0901` | Upload | The file is over 8 MiB. | | `FLK-0811` | Read, derive, delete | The document id does not resolve for your tenant. | | `FLK-0902` | Derive an operation schema | The document declares no operation with that path and method. | | `FLK-0304` | Derive an operation schema | The `path` query parameter is missing, or `method` is missing or is not an HTTP method. | | `FLK-0946` | Create or update the provider configuration | `config.openapi_schema_id` is missing, or is not a UUID. | | `FLK-0947` | Create or update the provider configuration, and run time | The referenced document does not exist in your tenant. | | `FLK-0948` | Create or update the provider configuration | The `config.auth` block is malformed. | | `FLK-0293` | Create or update the provider configuration | A `schemaBindings` entry is malformed. | | `FLK-0942` | Create or update the provider configuration | A `schemaBindings` entry names a document that does not exist in your tenant. | | `FLK-0943` | Create or update the provider configuration | A `schemaBindings` entry restricts to an operation the document does not declare. | | `FLK-0949` | Run time | Neither `config.base_url` nor a usable `servers` entry resolves a base URL. The node fails without calling the service. | | `FLK-0950` | Run time | The bound operation is absent from the document, or a `path` parameter found no value. The node fails without calling the service. | | `FLK-0954` | Run time | A required `header` or `query` parameter found no value. The node fails without calling the service. | | `FLK-0955` | Create or update the provider configuration | The optional `config.headers` block is malformed. It must map valid, non-blank header names to string values without case-insensitive duplicates. | | `FLK-0945` | Delete the document | An active provider configuration binds it. | | `FLK-0936` | Delete the document | An active workflow references it from a webhook trigger. | | `FLK-0803` | Upload a spec version | That service and version pair is already published. Versions are immutable. | | `FLK-0801` | Pin a spec version | The request is missing the service or the version. | See the [Flowker error list](/en/reference/products/flowker/flowker-error-list) for every code. ## What's next *** > **[Working with request and response data](/en/products/flowker/working-with-request-and-response-data)** > > Map values into the operation's request body and read its response back out. > **[Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger)** > > Validate an inbound payload against one operation of the same document. > **[Integration guide](/en/products/flowker/integration-guide)** > > Set the authentication, the retries and the circuit breaker every provider configuration shares. > **[OpenAPI schemas API](/en/reference/products/flowker/list-openapi-schemas)** > > Explore the schema registry endpoints. # Activating and deactivating a workflow Source: https://docs.lerian.studio/en/products/flowker/console/activating-deactivating-workflow Control whether a workflow is live for execution by activating, deactivating, or moving it back to draft status as your needs change. You control whether a workflow is available for execution. You can activate a workflow, deactivate it, or move it back to draft status. ## Activating a workflow *** Activating a workflow makes it live and available for execution. You can activate only **Draft** workflows. To activate an **Inactive** workflow again, first move it back to Draft. To activate a workflow: **Step 1** On the **Workflows** page, find the workflow you want to activate. **Step 2** Click the three dots () on the **Actions** column and select **Activate**. **Step 3** The workflow status will change to **Active**. > [!NOTE] > > Make sure the workflow has a complete configuration before you activate it. Active workflows will respond to their configured triggers and start executions automatically. ## Deactivating a workflow *** Deactivating a workflow stops it from responding to triggers. It will not start new executions, but any in-progress executions will continue to completion. To deactivate a workflow: **Step 1** On the **Workflows** page, find the active workflow you want to deactivate. **Step 2** Click the three dots () on the **Actions** column and select **Deactivate**. **Step 3** The workflow status will change to **Inactive**. ## Moving a workflow to Draft *** Moving a workflow back to **Draft** status allows you to make significant changes before reactivating it. > [!NOTE] > > The **Move to Draft** option is only available for workflows in the **Inactive** state. Workflows in **Draft** or **Active** states won’t show this option in the Actions menu. To move a workflow to draft: **Step 1** On the **Workflows** page, find the workflow you want to move to draft. **Step 2** Click the three dots () on the **Actions** column and select **Move to Draft**. **Step 3** The workflow status will change to **Draft**. > [!TIP] > > To edit an active workflow, open it and click **Edit**. The Console moves it to Draft for you. Save your changes and activate it again. You can also **Clone** it to iterate without touching the live version. # Adding a provider Source: https://docs.lerian.studio/en/products/flowker/console/adding-a-provider Add a new provider in the Flowker Console so your workflows can connect to external systems. Set a name, choose a provider type, and configure it. To use external services in your workflows, you need to configure at least one provider. A provider is a connection to an external system that Flowker can interact with. To add a provider: **Step 1** Go to the **Providers** page and click the **+ New Provider** button. **Step 2** The **Add Provider** form will open. Fill in the fields: * **Name**: a descriptive name to identify this provider configuration (e.g., `production-payment-gateway`). * **Description** (optional): a description about the provider and what it does. * **How do you want to connect?** Choose the connection type: * **Connect a Lerian provider**: select a provider from the Lerian catalog. Fields for the connection appear after you select it. The platform provisions the credentials of the Midaz ledger, CRM, Fees and Identity providers. Tracer and Auth need their authentication settings. The **HTTP** provider requires **Allowed hosts**. * **Add your own API**: connect an external API from its OpenAPI spec. Select an uploaded OpenAPI spec, and optionally set a **Base URL** (leave it blank to use the spec's server URL) and the authentication settings. [Connecting your own API](/en/products/flowker/connecting-your-own-api) covers uploading the spec and calling its operations from a node. **Step 3** Review the configuration and click **Add Provider** to save the provider. > [!NOTE] > > The connection type is chosen at creation and cannot be changed later. Editing a provider keeps its original type. After creation, the new provider appears in the providers list with status `Active`. Workflow nodes can then reference it. # Canvas editor Source: https://docs.lerian.studio/en/products/flowker/console/canvas-editor Design workflows on the Flowker Canvas by dragging Trigger, Executor Call, Conditional, and Action nodes, then connecting and configuring each one. The **Workflow Canvas** is a visual drag-and-drop editor for designing workflows in Flowker. It is an interactive surface where you can add, connect, and configure nodes to define the execution flow of your automated processes. ## Accessing the Canvas *** The canvas opens when you create or open a workflow. Navigate to **Flowker → Workflows**, then create a new workflow or click the name of an existing one. A workflow that is not in Draft opens read-only until you click **Edit**. See [Editing a workflow](/en/products/flowker/console/editing-a-workflow). ## Canvas basics *** The canvas has the following interactions: * **Pan**: click and drag on the canvas background to move around. * **Zoom**: use the scroll wheel or pinch gesture to zoom in and out. * **Select**: click a node to select it. * **Configure**: double-click a node to open its configuration panel on the right side. * **Connect**: drag from a node's output handle to another node's input handle to create a connection. * **Delete**: select a node or connection and press Delete or Backspace. You can also click the trash icon on a node or the × on a connection. ## Node types *** Flowker workflows have four node types. ### Trigger Node The **Trigger Node** defines the event that starts the workflow. An active workflow must have at least one trigger node. A draft workflow may be empty or omit a trigger while you design it. Node-level save validation still applies. When you double-click a trigger node, the **Configure Trigger** panel opens with the following fields: | Field | Description | | - | - | | **Name** | A descriptive label for the trigger (e.g., "Webhook Trigger"). | | **Trigger** | A dropdown to select which trigger type to use. Options are loaded dynamically from the Flowker catalog. | After selecting a trigger type, additional configuration fields may appear based on the trigger's schema. For the webhook trigger, those fields are the path, the method, the input contract, the response mode, the response view and the accepted headers. See [Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger) for what each contract accepts. For the schedule trigger, they are the cron expression, the timezone, the enabled flag and the input data each run starts with. See [Running a workflow on a schedule](/en/products/flowker/running-a-workflow-on-a-schedule). | Trigger | Description | | - | - | | **Webhook** | The workflow starts when Flowker receives an external HTTP request at the configured path and method. | | **Schedule** | The workflow starts on a time-based schedule defined by a standard 5-field cron expression (minimum granularity of 1 minute), with an optional IANA timezone (defaults to UTC). | ### Executor Call Node **Executor Call Nodes** perform specific operations by calling executors registered in the Flowker catalog. Each executor is a pre-built integration with Lerian products or external services. When you double-click an executor call node, the **Configure Executor Call** panel opens. Its first two fields form a cascade: the provider configuration you pick first decides which executors the second field offers. | Field | Description | | - | - | | **Provider** | The first field, and the one that sets the connection. It lists the provider configurations saved on the [Providers](/en/products/flowker/console/providers-overview) page, each shown by its name with its catalog provider beside it. Picking one binds the step to that connection (its base URL and credentials) and scopes the **Executor** dropdown below to the same provider. When no provider configuration exists yet, the dropdown shows **No connections yet — create one in Flowker → Providers**. A workflow does not save while an executor call node has no provider configuration. | | **Executor** | The second field. It lists the catalog executors of the provider the selected configuration belongs to, and stays disabled until you pick a **Provider**, showing **Select a provider first**. When the selected configuration was built from your own OpenAPI spec, this field is replaced by **Operation**, which lists the operations of that spec. See [Connecting your own API](/en/products/flowker/connecting-your-own-api). | | **Name** | Auto-filled with the executor's category after selection, but editable. | After selecting an executor, additional configuration fields appear based on the executor's schema (e.g., data mapping parameters). Available executors include: | Executor | Description | | - | - | | **Create Account** | Creates a new account in Midaz. | | **Create Transaction** | Creates a new transaction in Midaz. | | **Get Account** | Retrieves account details from Midaz. | | **Get Account Balance** | Retrieves the balance for a specific account. | | **List Validations** | Lists transaction validations from Tracer. | | **Validate Transaction** | Validates a transaction through Tracer's risk rules. | > [!NOTE] > > Executors come from the Flowker catalog. The table is illustrative. Inspect the running catalog for the executors available to your deployment. If the configuration is invalid, an error message will appear in the panel. ### Conditional Node **Conditional Nodes** branch the workflow based on conditions evaluated at runtime. A conditional node holds a structured condition made of **cases**. Each case has one or more rules and routes the execution to its own outgoing path when it matches. The conditional node evaluates cases in order, and the first matching case wins. An optional **default** path catches executions that match no case. So a conditional node has one outgoing connection per case, plus the default path when enabled. When you double-click a conditional node, its configuration panel opens with the visual **condition builder**, where you define the cases and the rules inside each case (the value being tested, a comparison operator, and the value to compare against). > [!NOTE] > > Workflows saved with a legacy free-text condition expression keep the original **true**/**false** output paths. New conditionals use the condition builder. > [!TIP] > > Use descriptive case labels and node names (e.g., "High Value Check") to keep the workflow readable. ### Action Node **Action Nodes** perform a workflow-level action without calling an external service. The available action type is `set_output`, which sets the execution's final output. It is useful as the terminal step of a workflow, especially with [synchronous webhook responses](/en/products/flowker/integration-guide#synchronous-response-mode). Output values support `${...}` references resolved against the workflow context (for example, `${workflow.}` for the trigger payload, or `${execution.id}`). For workflows triggered by a synchronous webhook, a `set_output` node can also carry a `responseStatusCode` (integer, `200`–`599`) to override the HTTP status of the sync response. ## Configuring nodes *** To configure any node, double-click it on the canvas. A configuration panel slides in from the right side. Each node type has its own panel layout: * **Trigger**: Name field + trigger type dropdown + trigger-specific schema fields. * **Executor Call**: Provider dropdown + Executor dropdown + auto-filled name + executor-specific schema fields. * **Conditional**: Name field + the visual condition builder (cases and rules). * **Action**: Name field + the `set_output` output configuration. After filling in the configuration, click **Save & Close** to apply the changes and return to the canvas. ## Connections *** Connections define the order in which nodes execute. Each connection forms a directed edge from one node's output to another node's input. * A node can have multiple outgoing connections (e.g., conditional nodes have one per case, plus the default path when enabled). * A node can receive input from multiple sources. * Circular connections (loops) are not supported. > [!TIP] > > Keep your workflows readable by arranging nodes from left to right or top to bottom. Use descriptive node names to make it clear what each step does. # Cloning a workflow Source: https://docs.lerian.studio/en/products/flowker/console/cloning-a-workflow Clone a workflow in the Flowker Console to duplicate its nodes, connections, and configuration into a new Draft copy you can safely customize. Cloning creates a duplicate of an existing workflow, including all its nodes, connections, and configurations. The cloned workflow starts in **Draft** status so you can customize it before activating. To clone a workflow: **Step 1** On the **Workflows** page, find the workflow you want to clone. **Step 2** Click the three dots () on the **Actions** column and select **Clone**. **Step 3** The copy takes the original name followed by **(Copy)** and appears in the workflows list with **Draft** status. Workflow names are unique, so rename the copy before you clone the same workflow again. After cloning, you can [edit the workflow](/en/products/flowker/console/editing-a-workflow) to update its name, modify nodes, and adjust configurations as needed. > [!TIP] > > Cloning is useful when you want to create a variation of an existing workflow without modifying the original. For example, you can clone a workflow and adjust it for a different provider or data source. # Create a workflow from scratch Source: https://docs.lerian.studio/en/products/flowker/console/create-from-scratch Create a Flowker workflow from a blank canvas by adding trigger, executor, conditional, and action nodes, then connecting them to shape the flow. You can create a new workflow from scratch using the visual canvas editor. The workflow starts in **Draft** status. You must activate it before a trigger can start it. To create a workflow: **Step 1** Go to the **Workflows** page and click the **+ New Workflow** button. **Step 2** The **Workflow Templates** modal will open. In the left sidebar, select **Blank Workflow** and click the **Create Blank Workflow** button to start from an empty canvas. **Step 3** An empty canvas opens. Flowker creates the workflow when you save it. **Step 4** Enter a **name** for the workflow in the **Workflow name...** field. You cannot save without a name. **Step 5** Build your workflow by adding nodes to the canvas. You can add the following node types: * **Trigger**: defines the event that starts the workflow. * **Executor**: performs a specific operation (e.g., API call, data transformation). * **Conditional**: branches the workflow based on conditions organized in cases. * **Action**: performs a workflow-level action, such as setting the execution's final output (`set_output`). > [!TIP] > > To add a node, drag its card from the **Steps** panel to the canvas, double-click the card, or click the plus (+) that appears when you hover over it. **Step 6** Connect the nodes by dragging from one node's output to another node's input to define the execution flow. **Step 7** Configure each node by double-clicking it and filling in the required parameters in the side panel. **Step 8** Click **Save** to save your workflow. For a complete reference on canvas interactions, node configuration fields, and available triggers and executors, see [Workflow Canvas](/en/products/flowker/console/canvas-editor). The workflow will be created in **Draft** status. When your workflow is ready, you can [activate it](/en/products/flowker/console/activating-deactivating-workflow) to make it available for execution. # Creating Workflows Source: https://docs.lerian.studio/en/products/flowker/console/creating-workflows Start a new Flowker workflow from the + New Workflow button and choose Blank Workflow to open a fresh canvas. ## How it works *** When you click **+ New Workflow**, the **Workflow Templates** modal opens. Select **Blank Workflow** to get an empty canvas where you define every trigger, executor, and conditional node yourself. Every new workflow starts in **Draft** status. You can design, test, and iterate before [activating it](/en/products/flowker/console/activating-deactivating-workflow) for production use. ## Next step *** > **[Create from scratch](/en/products/flowker/console/create-from-scratch)** > > Start with a blank canvas and build your workflow node by node. # Deleting a provider Source: https://docs.lerian.studio/en/products/flowker/console/deleting-a-provider Delete an unused provider configuration from the Flowker Console using the Actions menu, and see what happens to workflows that still reference it. To remove a provider configuration from Lerian Console: **Step 1** On the **Providers** page, find the provider you want to delete. **Step 2** Click the three dots () on the **Actions** column and select **Delete**. **Step 3** A **Confirm Deletion** dialog will appear with the provider name. Confirm the deletion. > [!CAUTION] > > Deleting a provider configuration cannot be undone. Any workflows that reference this provider will fail if they attempt to use it. Make sure no active workflows depend on the provider before deleting it. > [!TIP] > > Before deleting a provider, check if any workflows reference it. You can update those workflows to use a different provider configuration first. # Deleting a workflow Source: https://docs.lerian.studio/en/products/flowker/console/deleting-a-workflow Delete a workflow from the Flowker Console using the Actions menu, confirm the removal, and understand how in-progress runs and history are handled. To remove a workflow from Lerian Console: **Step 1** On the **Workflows** page, find the workflow you want to delete. **Step 2** Click the three dots () on the **Actions** column and select **Delete**. **Step 3** A **Confirm Deletion** dialog will appear with the workflow name. Confirm the deletion. > [!CAUTION] > > Deleting a workflow cannot be undone. The deletion permanently removes all associated configuration, nodes, and connections. After deletion, Flowker attempts to mark pending or running executions as failed. This best-effort cleanup may not update every in-progress execution. Past execution records for the workflow will remain available in the **Executions** page. > [!NOTE] > > You can delete only **Draft** and **Inactive** workflows. Deactivate an **Active** workflow first. Otherwise, the Console shows **Active workflows cannot be deleted. Deactivate it first.** # Editing a provider Source: https://docs.lerian.studio/en/products/flowker/console/editing-a-provider Edit a provider in the Flowker Console to update its name, description, or connection parameters, and see how the changes apply to running workflows. You can update an existing provider configuration to change its connection parameters, credentials, or name. To edit a provider: **Step 1** On the **Providers** page, find the provider you want to edit. **Step 2** Click the three dots () on the **Actions** column and select **Details**. **Step 3** The provider panel will open with the current configuration. Update the fields as needed: * **Name**: the identifier for the provider. * **Description** (optional): a description about the provider and what it does. * **Provider-specific fields**: the connection parameters for the provider type (e.g., API URL, credentials). **Step 4** Click **Save** to apply your changes. > [!NOTE] > > Updating a provider configuration affects all workflows that reference it. Each executor call node reads the configuration when it executes. So runs already in progress pick up the new settings for nodes that haven't executed yet. > [!NOTE] > > You select the provider type at creation. You cannot change it when you edit the provider. To switch to a different provider, create a new configuration. # Editing a workflow Source: https://docs.lerian.studio/en/products/flowker/console/editing-a-workflow Edit a workflow in the Flowker Console to rename it, add or remove nodes, adjust connections, and reconfigure parameters in the canvas editor. You can edit an existing workflow to update its name, modify its nodes and connections, or reconfigure individual node parameters. You edit a workflow in **Draft** status. A Draft workflow opens ready to edit. An **Active** or **Inactive** workflow opens read-only, with an **Edit** button. To iterate without touching the live version, [clone](/en/products/flowker/console/cloning-a-workflow) it instead. To edit a workflow: **Step 1** On the **Workflows** page, click the workflow name, or click the three dots () on the **Actions** column and select **Details**. **Step 2** The visual canvas editor opens with the workflow's current configuration. If the workflow is not in Draft, click **Edit** and confirm in the **Edit workflow** dialog. The Console moves the workflow to Draft, and deactivates it first when it is Active. **Step 3** Make your changes: * **Update the name**: edit the **Workflow name...** field in the header. * **Add nodes**: drag nodes onto the canvas from the **Steps** panel. * **Remove nodes**: select a node and press Delete, or click its trash icon. * **Modify connections**: drag to create a connection, or click the × on a connection to remove it. * **Configure nodes**: double-click any node to open its configuration panel and update parameters. **Step 4** Click **Save** to apply your changes. When the workflow is ready to run, click **Activate**. > [!NOTE] > > If the workflow was Active and you leave the editor without saving, the **Your workflow is in Draft** dialog offers **Reactivate** or **Keep in Draft**. A Draft workflow does not run. > [!NOTE] > > Your changes take effect on future executions. They do not affect runs already in progress. > [!TIP] > > For more details about the canvas editor and its node types, see [Workflow Canvas](/en/products/flowker/console/canvas-editor). # Executions overview Source: https://docs.lerian.studio/en/products/flowker/console/executions-overview Use the Executions page in the Flowker Console to browse all workflow runs, filter by workflow or status, and track Pending, Running, Completed, or Failed runs. Executions are individual runs of your workflows. The Executions page lists every workflow execution. Use it to monitor progress, investigate failures, and review results. You manage executions in the [Lerian Console](/en/platform/console/about-lerian-console). ## Accessing the Executions page *** To open the **Executions** page, click **Executions** from the left-side menu on the **Flowker** page. The page displays all executions in a data table, with the following columns: * **Workflow**: the name of the workflow in this run (or `Deleted workflow` when the workflow no longer exists). * **Execution ID**: unique identifier of the execution. * **Status**: the current state of the execution. * **Started At**: the date and time of the trigger. * **Duration**: how long the execution took to complete. * **Actions**: action menu to view execution details or go to its workflow. Use the fields above the table to filter executions by status or by workflow name. The status filter searches all executions. The workflow name filter searches the executions on the current page. ## Execution statuses *** Each execution progresses through a lifecycle indicated by its status: | Status | Description | | - | - | | **Pending** | The execution has been triggered and is waiting to start processing. | | **Running** | The execution is currently in progress. | | **Completed** | The execution finished successfully. | | **Failed** | The execution encountered an error and did not complete successfully. | ## Related pages *** > **[View execution details](/en/products/flowker/console/viewing-execution-details)** # Flowker Dashboard Source: https://docs.lerian.studio/en/products/flowker/console/flowker-dashboard View an at-a-glance summary of workflow activity, key metrics, recent workflows, and execution history from the Flowker home page. The **Dashboard** is the home page of the Flowker module. It shows your workflow activity: key metrics, recent workflows, and recent execution history. ## Accessing the Dashboard *** Navigate to **Flowker → Dashboard** in the left sidebar, or select Flowker from the product launcher. The Dashboard opens by default. ## Stat cards *** A row of four summary cards appears at the top of the page: | Card | Description | | - | - | | **Total Workflows** | Total number of workflows, across all statuses. | | **Active Workflows** | Total number of workflows ready for execution. | | **Completed** | Number of executions that started in the last 24 hours and completed successfully. | | **Failed** | Number of executions that started in the last 24 hours and ended in failure. | ## Recent Workflows *** Below the stat cards, a list displays the five most recently created workflows. Each entry shows: * **Workflow name**: the name assigned to the workflow. * **Status**: the current lifecycle state of the workflow: `Draft`, `Active`, or `Inactive`. * **Last change**: how long ago the workflow last changed. Click any workflow to open it in the canvas editor. ## Recent Executions *** A second list displays the five most recent workflow executions. Each entry shows: * **Execution ID**: the unique identifier of the execution. * **Status**: the current state of the execution: `Pending`, `Running`, `Completed`, or `Failed`. * **Start time**: how long ago the execution started, or **Pending** before it starts. Click any execution to go to the **Executions** page, where you can open its detailed results. # Flowker module overview Source: https://docs.lerian.studio/en/products/flowker/console/flowker-module Design visual workflows, configure provider integrations, run automated processes, and monitor executions through the Flowker module. **Flowker** is a product module in the Lerian Console for workflow automation. It lets you design visual workflows, configure provider integrations, execute automated processes, and monitor execution history, all through a visual interface. Use the Console to create, edit, and manage workflows, configure providers, and monitor executions. > [!WARNING] > > Flowker is only available if you've acquired the Flowker product. [Get in touch with our team](https://lerian.studio/contact) to learn more. ## Accessing Flowker *** To access Flowker, select it from the product launcher in Lerian Console. ## Dashboard *** The **Dashboard** page provides an at-a-glance overview of your workflow activity. Use it to monitor key metrics, review recent workflows, and track recent executions. > **[Flowker Dashboard](/en/products/flowker/console/flowker-dashboard)** > > See a detailed walkthrough of the Dashboard page, including stat cards, recent workflows, and recent executions. ## Sidebar *** The left sidebar allows you to navigate between Flowker pages: **Dashboard**, **Workflows**, **Executions**, **Providers**, and [**Schema Registry**](/en/products/flowker/console/schema-registry). You can return to the product launcher by clicking **Back to products** in the sidebar. ## Available pages *** ### Workflows > **[Workflows](/en/products/flowker/console/workflows-overview)** > > Create, edit, and manage automated workflows using a visual canvas editor with configurable nodes and connections. > **[Workflow Canvas](/en/products/flowker/console/canvas-editor)** > > Learn about the visual canvas editor and its node types: triggers, executor calls, conditionals, and actions. ### Executions > **[Executions](/en/products/flowker/console/executions-overview)** > > View and monitor the execution history of your workflows, including status tracking and detailed results. ### Providers > **[Providers](/en/products/flowker/console/providers-overview)** > > Configure and manage provider integrations used by your workflows to connect to external services. # Managing workflows Source: https://docs.lerian.studio/en/products/flowker/console/managing-workflows Manage Flowker workflows from the Console: edit nodes, clone for reuse, control the Draft, Active, and Inactive lifecycle, or delete when no longer needed. Once a workflow exists, you can modify its structure, manage its lifecycle, duplicate it for reuse, or remove it entirely. All maintenance actions are available from the **Actions** menu () on each workflow row in the Workflows page. ## Workflow lifecycle *** Every workflow moves through a defined set of statuses: | Status | What it means | | - | - | | **Draft** | The workflow is being designed. It cannot be triggered or executed. | | **Active** | The workflow is live and responds to triggers. | | **Inactive** | The workflow has been paused. It won't respond to triggers until it is moved back to Draft and activated again. | You control these transitions through the **Activate**, **Deactivate**, and **Move to Draft** actions. See [Activating and deactivating a workflow](/en/products/flowker/console/activating-deactivating-workflow) for details. ## Available actions *** > **[Edit a workflow](/en/products/flowker/console/editing-a-workflow)** > > Modify the workflow's nodes, connections, and configuration in the visual canvas editor. > **[Clone a workflow](/en/products/flowker/console/cloning-a-workflow)** > > Create a duplicate of an existing workflow to use as a starting point for a new one. > **[Activate / Deactivate](/en/products/flowker/console/activating-deactivating-workflow)** > > Control whether the workflow is live and responsive to triggers, or paused. > **[Delete a workflow](/en/products/flowker/console/deleting-a-workflow)** > > Permanently remove a workflow you no longer need. # Providers overview Source: https://docs.lerian.studio/en/products/flowker/console/providers-overview Manage reusable provider configurations for external service integrations, including connection settings and credential references. Flowker provider configurations connect workflows to external services. A configuration stores or references the connection settings and credentials it needs. Internally credential-managed catalog providers can require no client-supplied credentials. You manage provider configurations in the [Lerian Console](/en/platform/console/about-lerian-console). ## What is a Provider? *** A **Provider** is a catalog integration type that groups related executors and declares a configuration schema. A **provider configuration** selects a catalog provider or references a tenant-provided OpenAPI document, then supplies or references connection settings for executor call nodes. This keeps credentials and endpoints out of individual workflows. ## Accessing the Providers page *** To open the **Providers** page, click **Providers** from the left-side menu on the **Flowker** page. The page lists your provider configurations in a data table, with the following columns: * **Name**: the provider configuration name. * **Provider**: the catalog provider, or **Your own API** for a connection to an OpenAPI document you uploaded. * **Type**: **Native** or **External** for a catalog provider, or **OpenAPI** for your own API. * **Schemas**: the number of XSD or OpenAPI schemas bound to the configuration. * **Status**: the configuration status: `Active` or `Disabled`. * **Last Modified**: the date of the last change to the configuration. * **Actions**: action menu. You can filter provider configurations by name, status, or type using the filter fields above the table. ## Provider Catalog *** Flowker includes a catalog of available provider types. When creating a `catalog` provider configuration, you select which external service type you want to connect to. The catalog provides the configuration schema and required fields for each provider type. ## Available actions menu *** For each provider configuration, the following actions are available in the action menu (): * **Details**: opens the provider configuration for viewing and editing. * **Enable / Disable**: toggles the configuration status. Workflow nodes that reference a disabled configuration fail at execution time until you enable it again. * **Delete**: permanently removes the provider configuration. ## Related pages *** > **[Add a provider](/en/products/flowker/console/adding-a-provider)** > **[Edit a provider](/en/products/flowker/console/editing-a-provider)** > **[Delete a provider](/en/products/flowker/console/deleting-a-provider)** # Schema Registry Source: https://docs.lerian.studio/en/products/flowker/console/schema-registry Upload, view, and delete the XSD schemas and OpenAPI documents that Flowker workflows use to validate and map data. The **Schema Registry** page stores the schemas that workflows use to validate and map data. Workflows validate XML payloads against XSD schemas. OpenAPI documents describe the external API operations that workflows call. ## Accessing the Schema Registry page *** To open the **Schema Registry** page, click **Schema Registry** in the left-side menu on the **Flowker** page. The page has two tabs: * **XSD**: the XSD schemas. * **External APIs**: the OpenAPI documents. To see the schemas in a tab, you need the `list` permission on the `xsd-schemas` resource for **XSD** or on the `openapi-schemas` resource for **External APIs**. To upload a schema, you need `create` on the same resource. To delete a schema, you need `delete`. ## XSD tab *** The **XSD** tab lists your XSD schemas, with the following columns: * **Name**: the schema name. * **Version**: the schema version. * **Imports**: the number of schemas that this schema imports. * **Content Hash**: the start of the hash of the file content. * **Used by**: the number of provider configurations and workflows that reference the schema. * **Created At**: the upload date. * **Actions**: action menu. ## External APIs tab *** The **External APIs** tab lists your OpenAPI documents, with the following columns: * **Name**: the document name. * **Version**: the document version. * **OpenAPI Version**: the OpenAPI version of the document. * **Operations**: the number of operations in the document. * **Used by**: the number of provider configurations and workflows that reference the document. * **Created At**: the upload date. * **Actions**: action menu. ## Uploading an XSD schema *** **Step 1** On the **XSD** tab, click **Upload**. **Step 2** The **Upload XSD Schema** form opens. Fill in the fields: * **Name**: the schema name (for example, `pain.001.001.09`). * **Version**: the schema version. * **Imports** (optional): the names of the schemas that this schema imports, separated by commas. * **XSD File**: a `.xsd` file no larger than 1 MiB. **Step 3** Click **Upload**. ## Uploading an OpenAPI document *** **Step 1** On the **External APIs** tab, click **Upload**. **Step 2** The **Upload OpenAPI Schema** form opens. Fill in the fields: * **Name**: the document name (for example, `orders-api`). * **Version**: the document version. * **OpenAPI File**: a `.json`, `.yaml`, or `.yml` file no larger than 8 MiB. **Step 3** Click **Upload**. To call the operations of an uploaded document from a workflow, see [Connecting your own API](/en/products/flowker/connecting-your-own-api). ## Viewing a schema *** Click the schema name, or select **Details** in the action menu (). The **Schema Details** panel opens. You cannot edit a schema in this panel. For an XSD schema, the panel shows the **Target Namespace** and the **Imports**. For an OpenAPI document, it shows the **OpenAPI Version** and a table of the document operations, with their **Path** and **Method**. The **Used by** section lists the provider configurations and workflows that reference the schema. Each one shows its status: **Active** or **Disabled**. ## Deleting a schema *** **Step 1** Click the three dots () on the **Actions** column and select **Delete**. **Step 2** The Console checks which provider configurations and workflows reference the schema. The next dialog depends on the result: * **No reference**: a confirmation dialog opens. Click **Confirm**. * **Only references that are not active**: the **Delete schema anyway?** dialog lists them. After you delete the schema, they point to a schema that does not exist. Click **Delete anyway**. * **One or more active references**: the **Schema in use** dialog lists them, and you cannot delete the schema. Deactivate the active references or point them to another schema, then try again. ## Related pages *** > **[Add a provider](/en/products/flowker/console/adding-a-provider)** > **[Connecting your own API](/en/products/flowker/connecting-your-own-api)** # Viewing execution details Source: https://docs.lerian.studio/en/products/flowker/console/viewing-execution-details Open the Execution Details panel in the Flowker Console to view summary fields and, after an execution completes or fails, its step results, output data, and errors. You can inspect the status of any workflow execution. After an execution completes or fails, you can also review its step results, outputs, and errors. ## Opening the execution detail *** To view execution details: **Step 1** On the **Executions** page, find the execution you want to inspect. **Step 2** Click the workflow name or select **View Details** from the action menu (). **Step 3** An **Execution Details** side panel opens with the full execution information. ## Execution detail panel *** The panel shows the summary fields, the execution error when there is one, the input as received, the step results, and the final output. ### Summary fields | Field | Description | | - | - | | **Workflow Name** | Name of the workflow that was executed. | | **Workflow ID** | Unique identifier of the workflow that was executed. | | **Execution ID** | Unique identifier for the execution. | | **Started At** | Timestamp when the execution was triggered. | | **Completed At** | Timestamp when the execution finished (if applicable). | | **Duration** | Total time elapsed from start to completion. | > [!TIP] > > The execution status appears as a tag next to the panel title. > [!NOTE] > > Step results are available only after an execution reaches **Completed** or **Failed**. Pending and running executions do not have results yet. ### Execution step results For a completed or failed execution, Flowker records results for the executed workflow steps. Trigger nodes start traversal and do not create a step result. Executor calls, conditionals, and actions do. Each step result includes: * **Step name**: the name recorded for the executed step. * **Status**: whether the step completed successfully or failed. * **Output data**: the data produced by the step. * **Error details**: if the step failed, the error message and relevant context. > [!TIP] > > Use the execution step results to trace through the workflow. When an execution fails, they help you identify which step encountered the error and its output. # Workflows overview Source: https://docs.lerian.studio/en/products/flowker/console/workflows-overview Browse the Workflows page in the Flowker Console to see every workflow, filter by name or status, and access lifecycle-aware actions. Each workflow defines an automated process composed of trigger, executor call, conditional, and action nodes connected in a visual canvas. You manage workflows in the [Lerian Console](/en/platform/console/about-lerian-console). ## Accessing the Workflows page *** To open the **Workflows** page, click **Workflows** from the left-side menu on the **Flowker** page. The page lists your workflows in a data table, with the following columns: * **Name**: the workflow name (and its description, when set). * **Status**: the current lifecycle state: `Draft`, `Active`, or `Inactive`. * **Created**: the workflow creation date. * **Last Updated**: the date of the last change to the workflow. * **Actions**: action menu. You can filter workflows by name or status using the filter fields above the table. ## Workflow statuses *** Each workflow has a lifecycle status that determines whether it can run: | Status | Description | | - | - | | **Draft** | The workflow is being designed and is not yet available for execution. | | **Active** | The workflow is live and can be triggered for execution. | | **Inactive** | The workflow has been deactivated and will not respond to triggers. | ## Available actions menu *** The action menu shows these actions, subject to your permissions: * **Details**: opens the workflow in the visual canvas editor (clicking the workflow name does the same). * **Schedule**: opens the schedule panel, showing the workflow's schedule status, upcoming runs, and missed runs pending review. See [Running a workflow on a schedule](/en/products/flowker/running-a-workflow-on-a-schedule). * **Execute**: triggers an immediate execution with an empty input. Only an **Active** workflow runs. * **Clone**: creates a duplicate of the workflow. * **Activate**: available only for a **Draft** workflow. * **Deactivate**: available only for an **Active** workflow. * **Move to Draft**: available only for an **Inactive** workflow. Returns it to draft status. * **Delete**: permanently removes a **Draft** or **Inactive** workflow. Deactivate an **Active** workflow first. ## Related pages *** > **[Create a workflow](/en/products/flowker/console/create-from-scratch)** > **[Edit a workflow](/en/products/flowker/console/editing-a-workflow)** > **[Workflow Canvas](/en/products/flowker/console/canvas-editor)** > **[Activate / Deactivate](/en/products/flowker/console/activating-deactivating-workflow)** > **[Clone a workflow](/en/products/flowker/console/cloning-a-workflow)** > **[Delete a workflow](/en/products/flowker/console/deleting-a-workflow)** # Core concepts Source: https://docs.lerian.studio/en/products/flowker/flowker-concepts Understand the building blocks of Flowker: workflows, nodes, edges, catalog, provider configurations, templates, executions, and dashboard. This page describes each Flowker concept. The last section shows how they connect. ## Workflows *** A workflow is the definition of a business process, the sequence of steps Flowker follows to complete an operation. Workflows go through a lifecycle: | Status | Description | | - | - | | `draft` | Created and editable. Not yet runnable. | | `active` | Ready to execute. Structure is locked. | | `inactive` | Deactivated. No new executions accepted. | To move a workflow between states, use the [activate](/en/reference/products/flowker/activate-workflow), [deactivate](/en/reference/products/flowker/deactivate-workflow), and [move to draft](/en/reference/products/flowker/move-workflow-to-draft) endpoints. ## Nodes and edges *** Nodes are the individual steps of a workflow, or **tasks** in business terms. Each node is a unit of work: receive an event, call a service, evaluate a condition, or perform an action. Flowker supports four node types: | Type | Purpose | When to use | | - | - | - | | `trigger` | Entry point for the workflow | Starts execution when an event occurs. | | `executor` | Calls an external service through a provider configuration | When you need to call an external API or integration. | | `conditional` | Branches execution based on conditions | When the next step depends on the result of a previous one. | | `action` | Performs a built-in action; the available type is `set_output`, which sets the execution's final output | For setting the workflow's final output without an external call. | **Edges** connect nodes and define the order of execution. Branching happens in the conditional node, not in the edge. The conditional node evaluates its condition and produces an outcome handle. Flowker then follows the single outgoing edge whose `sourceHandle` matches that handle. Every other node type follows all of its outgoing edges. ## Catalog *** The catalog is the registry of the built-in providers, executors, and triggers available in Flowker. Before configuring any integration, browse the catalog to see what's already available: * **Catalog executors** are the built-in components a workflow node invokes. They include the generic HTTP connector, and the operations of native providers such as the Midaz ledger and Tracer. * **Triggers** define the event types that can start a workflow (e.g., webhooks). Use these endpoints to explore what's available: * [List catalog executors](/en/reference/products/flowker/list-catalog-executors) and [List catalog triggers](/en/reference/products/flowker/list-catalog-triggers) to discover executor and trigger types. * [List catalog providers](/en/reference/products/flowker/list-catalog-providers) (`GET /v1/catalog/providers`) to list all available providers. * [Get catalog provider](/en/reference/products/flowker/get-catalog-provider) (`GET /v1/catalog/providers/{id}`) to get details for a specific provider. * [List executors by provider](/en/reference/products/flowker/list-executors-by-provider) (`GET /v1/catalog/providers/{id}/executors`) to list the executors available for a specific provider. ## Provider configurations *** A **provider configuration** is your connection to one live instance of an external service. It is the object a workflow node points at, and the object Flowker reads when that node runs. Flowker separates the type of service from your connection to it: | Concept | What it is | Nature | | - | - | - | | **Provider** | A type of external service (e.g., Midaz, Tracer, a generic HTTP endpoint). | Static (published in the catalog). | | **Provider configuration** | Your connection to one instance of that provider. | Dynamic (you create and manage these). | Each provider configuration holds: * **`config`**: the connection details for that instance, such as the base URL and authentication credentials. For the default `catalog` kind, Flowker validates this map against the catalog provider's JSON Schema. For `external_openapi`, it uses an external-OpenAPI configuration validator. A read never returns the credential the authentication type declares. * **`allowedHosts`**: the public hosts this configuration is allowed to call. * **`allowedPrivateHosts`**: named private hosts your operations team allows this configuration to reach. It lifts only Flowker's private or loopback restriction: if `allowedHosts` is non-empty, it must also include the host. Cloud metadata and link-local addresses stay blocked. * **`schemaBindings`**: the XSD or OpenAPI schemas bound to this configuration, each with an optional restriction to specific OpenAPI operations. A configuration also carries a **`kind`**. Supported values are `catalog` (the default), which connects to a catalog provider, and `external_openapi`, which connects to an OpenAPI document you uploaded yourself so a workflow node calls your own API's operations. See [Connecting your own API](/en/products/flowker/connecting-your-own-api). Provider configurations have two statuses: `active` (in use) and `disabled` (temporarily offline). Use [Disable provider configuration](/en/reference/products/flowker/disable-provider-configuration) to take a connection out of service and [Enable provider configuration](/en/reference/products/flowker/enable-provider-configuration) to bring it back. ### How a workflow reaches it Every executor node can carry a `providerConfigId`, the identifier of the provider configuration it calls through. At run time, Flowker builds each outgoing request from the base URL of that provider configuration plus the path on the node, and the node fails if the provider configuration is not `active`. Use the [Provider configurations](/en/reference/products/flowker/list-provider-configurations) endpoints to create, read, update, disable, enable, and delete your connections. ## Templates *** Workflow templates are pre-built workflow patterns published in the catalog. Each template describes a known integration pattern and the parameters that pattern expects. Each template has a parameter schema that defines what inputs it expects (e.g., which provider configuration to use, threshold values). When Flowker can retrieve active provider configurations, it enriches referenced parameter fields with selectable options. To inspect a template: 1. [List the catalog templates](/en/reference/products/flowker/list-catalog-templates). 2. [Get the template detail](/en/reference/products/flowker/get-catalog-template) to see its parameter schema. 3. [Validate a set of parameters](/en/reference/products/flowker/validate-catalog-template-params) against that schema. ## Executions *** An execution is a runtime instance of a workflow. A trigger normally starts a new execution. A retry that reuses an existing idempotency key returns the pre-existing execution instead of creating another. Each execution tracks: * `executionId`: unique identifier for this run. * `status`: current state (`pending`, `running`, `completed`, or `failed`). * `stepResults`: the output of each executed executor, conditional, or action node, in order. Trigger nodes start graph traversal and do not create execution-step records. * `finalOutput`: the final value persisted for the execution. When a `set_output` action produces an object, Flowker uses that object. Otherwise Flowker returns the accumulated workflow context. Use [Get execution status](/en/reference/products/flowker/get-execution-status) to monitor progress and [Get execution results](/en/reference/products/flowker/get-execution-results) to retrieve the full output. > [!NOTE] > > The status endpoint returns the execution record, including its current status. The dedicated results endpoint ([`GET /v1/executions/{id}/results`](/en/reference/products/flowker/get-execution-results)) returns `status`, `stepResults`, and `finalOutput` when present. A failed step can include `errorMessage`. This response has no top-level error-details field. ## Idempotency *** Direct execution requests require a non-empty `Idempotency-Key` string. Flowker does not enforce UUID format. The webhook header is optional. If Flowker receives a second request with the same `Idempotency-Key`, it returns the pre-existing execution instead of creating another. A direct replay includes `idempotencyReplayed: true` in the response. ``` Idempotency-Key: 7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b ``` Use a new key when you intentionally want a new execution. Reuse the same key only when retrying the exact same request. ## Dashboard *** The Dashboard API provides aggregated summaries of your workflows and executions. Use it whenever you need a high-level view of system health without querying individual executions. * [Workflow summary](/en/reference/products/flowker/get-dashboard-workflow-summary) returns totals and breakdowns by status (draft, active, inactive). * [Execution summary](/en/reference/products/flowker/get-dashboard-execution-summary) returns totals and breakdowns by status, with optional time range and status filters. Example response from [`GET /v1/dashboards/executions`](/en/reference/products/flowker/get-dashboard-execution-summary): ```json theme={null} { "total": 12847, "completed": 11903, "failed": 712, "pending": 130, "running": 102 } ``` Common use cases: * **Monitoring execution health**: track completion and failure rates over time to spot degradation early. * **Building status pages**: surface workflow throughput and success metrics in internal or client-facing dashboards. * **Alerting on failure rate spikes**: compare `failed / total` against a threshold to trigger alerts before issues cascade. ## How it all fits together *** Flowker's concepts build on each other in this order: 1. **Explore the catalog** to discover available providers, catalog executors, triggers, and templates. 2. **Create provider configurations** to connect Flowker to live instances of external services. 3. **Define workflows**: each executor node names a catalog executor and the provider configuration it calls through. 4. **Execute workflows** to run your business process and retrieve results. 5. **Monitor**: use the dashboard for operational summaries and the executions API for step-level detail. Follow the [Getting started guide](/en/products/flowker/flowker-getting-started) to run your first workflow end to end. # Getting started with Flowker Source: https://docs.lerian.studio/en/products/flowker/flowker-getting-started Learn how Flowker automates financial operations through real business scenarios, from payment orchestration to fraud prevention. Financial operations involve multiple steps, approvals, and external checks. Managing these manually leads to errors, delays, and compliance gaps. Flowker lets you define these processes once as workflows and execute them consistently, with step-level execution records and real-time visibility. ## How Flowker works *** You start by exploring what integrations are available, connect them to your environment, define a workflow that models your business process, and then execute it. Every execution leaves a record that is traceable and available for inspection through the API. **1. Explore the catalog** Browse the catalog to discover the providers, executors, and triggers available in your Flowker deployment. **2. Connect your providers** Create a provider configuration for each external service your workflows call: fraud engines, payment processors, KYC providers, and notification services. It identifies the connection a workflow node uses for that service. **3. Define your workflows** Model your business process as a workflow: define the steps (nodes), the order they run (edges), and the conditions that control branching. **4. Execute and monitor** Trigger workflows through the API, a webhook, or a schedule trigger. Read each execution's status and retrieve results when it completes. ## What you can automate *** Flowker is designed for multi-step financial processes that require reliability, traceability, and integration with external services. ### Payment orchestration **The challenge:** A payment involves multiple steps: validation, routing to the right provider, confirmation, and notification. When handled manually or through scattered integrations, failures go unnoticed and reconciliation becomes painful. **How Flowker solves it:** Define a workflow that coordinates the entire payment lifecycle: 1. **Trigger**: A new payment request arrives via API or webhook. 2. **Validate**: Check that the payment data is complete and the account has sufficient balance. 3. **Route**: Select the appropriate payment provider based on amount, currency, or rail (Pix, TED, SWIFT). 4. **Confirm**: Call the provider and wait for confirmation. 5. **Notify**: Send a confirmation event to the customer and update the ledger. For executor calls, you configure retry behavior per node. By default, `POST` and `PATCH` calls run once unless you opt the node into retries. Flowker does not deduplicate the provider side effect on retries: the execution `Idempotency-Key` deduplicates Flowker executions, not outbound provider requests. Opt a `POST` or `PATCH` node into retries only when the provider operation is idempotent. You can also opt in when the provider documents a stable business key and atomically enforces it. Send that key in a header as an idempotency key on every attempt. Otherwise, keep retries disabled for the node. Flowker records every executed executor, conditional, and action node for reconciliation and review. ### Anti-fraud validation **The challenge:** Fraud checks must happen before transaction processing. They involve external services, scoring logic, and different thresholds depending on the transaction type. Hard-coding this logic makes it hard to maintain. **How Flowker solves it:** Define a pre-transaction workflow that runs fraud checks before anything reaches the ledger: 1. **Trigger**: A new transaction arrives. 2. **Enrich**: Collect customer history, device data, and geolocation from internal systems. 3. **Score**: Call one or more fraud detection providers and collect risk scores. 4. **Decide**: Apply rules: if the score is above the threshold, block. If borderline, flag for review. If clear, approve. 5. **Act**: Proceed with the transaction or notify the compliance team. All decisions are traceable. You can adjust thresholds and add new providers without changing the core logic. ### Customer onboarding **The challenge:** Onboarding a new customer requires identity verification, document validation, AML/KYC checks, and account creation. These steps involve multiple external providers and regulatory requirements that vary by jurisdiction. **How Flowker solves it:** Define an onboarding workflow that automates the entire process: 1. **Trigger**: A new customer registration arrives. 2. **Verify identity**: Call a KYC provider to validate the customer's documents and identity. 3. **Check compliance**: Run AML screening against sanctions lists and PEP databases. 4. **Evaluate risk**: Apply internal risk rules based on the customer profile, jurisdiction, and transaction history. 5. **Create account**: If all checks pass, create the customer's account in the system. 6. **Notify**: Send a welcome email or notify the relationship manager if manual review is needed. Each step's result determines the next action. If a check fails, the workflow stops and records the step result that explains why. ### Manual approval workflows **The challenge:** Some operations (high-value transfers, credit limit changes, account closures) require human approval. But building approval flows with email notifications, timeouts, and escalation paths is complex and error-prone. **How Flowker solves it:** Define a workflow that calls an external approval service through an executor node: 1. **Trigger**: A high-value transfer request arrives. 2. **Evaluate**: Check the amount, customer risk profile, and internal policies. 3. **Request approval**: Call an external approval service (manager, compliance officer) through an executor node. 4. **Decide**: A conditional node evaluates the approval response. 5. **Act**: If approved, proceed with the transfer. If rejected, cancel and notify the requester. You can define multi-level approvals by chaining executor nodes that call your approval service at each level. ## Key capabilities *** * **Pre-ledger validation**: Put every check before the posting node in your graph and use conditional nodes with matching `sourceHandle` edges to route only approved outcomes to posting. * **Step-level execution records**: Execution results record status, duration, output, and error for each executed executor, conditional, or action node. Trigger nodes start graph traversal and do not create execution-step records. * **Provider integrations**: Connect to fraud engines, KYC providers, payment processors, and any HTTP-based service. * **Conditional branching**: A conditional node selects the outgoing edge whose `sourceHandle` matches its outcome, so you can route execution based on step results, amounts, risk scores, or any data field. * **Idempotent execution**: Reusing an `Idempotency-Key` returns the existing Flowker execution instead of creating another. Direct execution requires a non-empty key. The webhook header is optional. * **Execution monitoring**: Read execution status through the API while a workflow runs. Retrieve execution results, including step results, after it reaches a terminal state. > [!NOTE] > > With `PLUGIN_AUTH_ENABLED=true`, Flowker's management routes require a Bearer token and enforce resource/action permissions. Production deployments require this setting. The signed Streaming Hub ingress under `/v1/hub/webhooks/` uses the hub's signature instead of a Bearer token. Provider-specific credentials (for external services) are managed through [provider configurations](/en/reference/products/flowker/list-provider-configurations). ## What's next *** > **[Core concepts](/en/products/flowker/flowker-concepts)** > > Understand workflows, nodes, edges, and the execution model in detail. > **[What is Flowker?](/en/products/flowker/what-is-flowker)** > > Read about Flowker's architecture, features, and design philosophy. > **[API reference](/en/reference/products/flowker/create-workflow)** > > Explore the full API for workflows, executions, and configurations. # Flowker in Lerian Console Source: https://docs.lerian.studio/en/products/flowker/flowker-in-lerian-console See what the Flowker module in Lerian Console offers: visual canvas editor, provider management, and execution monitoring. The **Lerian Console** provides a visual interface for designing workflows, managing provider configurations, and inspecting executions. ## What you get *** The Flowker module in Lerian Console gives you: * A *visual canvas editor* for designing workflows with drag-and-drop nodes, triggers, conditionals, and connections * *Provider management* for configuring external integrations directly from the UI * *Execution monitoring* with status and step result inspection ## When to use the Console vs. the API *** The Console calls the same Flowker API for these flows. Choose the visual interface or the API based on your workflow: | Use the Console when | Use the API when | | :- | :- | | Designing workflows visually | Automating workflow creation in CI/CD | | Configuring provider integrations | Integrating Flowker into your own tooling | | Monitoring executions | Triggering workflows programmatically | | Inspecting step results | Building custom dashboards or alerts | Most teams use both: the Console for design and monitoring, the API for runtime execution and automation. ## Get started *** > **[Flowker module overview](/en/products/flowker/console/flowker-module)** > > Full walkthrough of the Flowker module in Lerian Console: accessing the module, navigating the sidebar, and understanding the available pages. > **[Dashboard](/en/products/flowker/console/flowker-dashboard)** > > Monitor workflow activity at a glance with stat cards, recent workflows, and execution history. > **[Managing workflows](/en/products/flowker/console/managing-workflows)** > > Create, edit, clone, activate, and delete workflows through the Console interface. > **[Workflow canvas](/en/products/flowker/console/canvas-editor)** > > Learn how to use the visual canvas editor: triggers, executor calls, conditionals, and connections. > **[Managing providers](/en/products/flowker/console/providers-overview)** > > Configure and manage the provider integrations used by your workflows. > [!NOTE] > > If Flowker is not enabled for your organization, an administrator can enable it in the Console under **Settings**, in **Products**. # Observability in Flowker Source: https://docs.lerian.studio/en/products/flowker/flowker-observability-guide Track Flowker workflow runs through their execution records and know what to send to Lerian support. Lerian operates and monitors the Flowker service. You follow your workflows through their execution records: each run has a status, step results, and a final output. ## Investigate a workflow run *** For an individual workflow run, use [Get execution results](/en/reference/products/flowker/get-execution-results) to see its status, step results, and final output when present. A failed step can include an `errorMessage`. This response has no top-level error-details field. ## How to interpret execution status *** Each workflow execution in Flowker has a status that tells you where it stands. | Status | Meaning | What to do | | - | - | - | | `pending` | The execution has been created but has not started. In the normal execution path, Flowker changes it to `running` synchronously before responding; do not treat it as a scheduler queue. | — | | `running` | Execution is in progress | Normal. Monitor for completion | | `completed` | The workflow ran to its end without an error | No action needed | | `failed` | Execution finished with an error | Check `stepResults` for a failed step. A failure before any step runs has no step error in this response. | > [!TIP] > > If you see a significant number of `failed` executions in a short period, collect their execution ids and error details before you escalate. ## When to contact Lerian support *** Contact Lerian support when: * `failed` executions keep growing and the cause is not clear from `stepResults` * Flowker does not process new executions Share the execution ids, the UTC timeframe, and the relevant API error payloads. # Integration guide Source: https://docs.lerian.studio/en/products/flowker/integration-guide Connect external services to Flowker through provider configurations. Set up authentication, map fields, and run workflows against real integrations. Flowker calls external services (such as fraud engines, payment processors, and KYC providers) through provider configurations. A provider configuration is your connection to one live instance of an external service. In this guide, you explore the catalog, create a provider configuration, and reference it from a workflow node. You then map fields between your data and the service, and learn how Flowker retries and protects those calls. ## Step 1: Explore the catalog *** The catalog is the registry of the providers, catalog executors, and triggers that ship with Flowker. **1. List available providers** Call the [List catalog providers](/en/reference/products/flowker/list-catalog-providers) endpoint to see the service types Flowker connects to. The catalog always includes the generic HTTP connector. Native providers such as `ledger` (Midaz) and `tracer` are synthesized from published OpenAPI specifications and appear only when the native schema registry is configured and synthesis succeeds. **2. List available catalog executors** Call the [List catalog executors](/en/reference/products/flowker/list-catalog-executors) endpoint to see the operations a workflow node can invoke. Use [List executors by provider](/en/reference/products/flowker/list-executors-by-provider) to narrow the list to one provider. **3. List available triggers** Call the [List catalog triggers](/en/reference/products/flowker/list-catalog-triggers) endpoint to see the built-in trigger types: webhooks and schedules. The Execute workflow API starts a workflow but is not a catalog trigger. **4. Pick what you need** Note the `providerId` and the catalog executor id that match your integration. You use the first in [Step 2](#step-2-create-a-provider-configuration) and the second in [Step 3](#step-3-reference-the-provider-configuration-from-a-workflow-node). > [!TIP] > > The catalog shows what Flowker can call. Provider configurations hold the base URL, the credentials, and the settings for each service instance you use. ## Step 2: Create a provider configuration *** Call [`POST /v1/provider-configurations`](/en/reference/products/flowker/create-provider-configuration) to define your connection to one instance of an external service. | Field | Required | Description | | - | - | - | | `name` | Yes | A name for this connection, 1–100 characters. | | `kind` | No | Which kind of connection this is. Omit it, or send `catalog`, for a connection to a catalog provider (the case this guide covers). Send `external_openapi` for a connection to an OpenAPI document you uploaded; see [Connecting your own API](/en/products/flowker/connecting-your-own-api). You choose the kind when you create the configuration. | | `providerId` | For `catalog` | The catalog provider this connection is an instance of, such as `ledger` or `http`. A configuration of kind `external_openapi` may omit it, and a read of it returns the reserved id `external.openapi`. | | `config` | Yes | The connection details for that instance, such as the base URL and the authentication credentials. Flowker validates this map against the provider's JSON Schema from the catalog and returns `422` when it does not match. | | `allowedHosts` | For `http` | The public hosts this configuration is allowed to call. The generic HTTP connector (`providerId: "http"`) requires at least one entry, and an empty list is rejected with `FLK-0323`. Native providers accept an empty list. An entry with a leading dot matches subdomains: `.kyc-provider.io` matches `api.kyc-provider.io`. Host names only: no IP literals, wildcards, or ports. The host in `config.base_url` must be covered by the list, otherwise the create is rejected with `FLK-0320`. | | `allowedPrivateHosts` | No | Named private hosts your operations team allows this configuration to reach. Cloud metadata and link-local addresses stay blocked. | | `schemaBindings` | No | The XSD or OpenAPI schemas bound to this configuration, each with an optional restriction to specific OpenAPI operations. | | `description` | No | Free text, up to 500 characters. | | `metadata` | No | Your own key-value pairs. | > [!NOTE] > > A `providerId` is a catalog identifier, and it does not always match the product name. The catalog registers Midaz as `ledger`. Always take the value from [List catalog providers](/en/reference/products/flowker/list-catalog-providers) rather than guessing it from the product name. > [!WARNING] > > The `providerId` on the configuration and the `executorId` on the node that uses it must belong to the same catalog provider. The generic HTTP connector uses `http` for both. Flowker rejects a workflow that pairs a configuration of one provider with an executor of another, with `FLK-0151`. The example below builds the connection this guide uses from here on: a fraud scoring service reached through the generic HTTP connector. > **Example request** > > ```json theme={null} > POST /v1/provider-configurations > > { > "name": "FraudShield Production", > "description": "Production fraud scoring service", > "providerId": "http", > "config": { > "base_url": "https://api.fraudshield.example.com", > "auth": { > "type": "api_key", > "config": { > "key": "sk-prod-xxx", > "header_name": "X-API-Key", > "location": "header" > } > } > }, > "allowedHosts": ["api.fraudshield.example.com"], > "metadata": { > "environment": "production" > } > } > ``` > > The response returns the new configuration's `id`. Keep it. [Step 3](#step-3-reference-the-provider-configuration-from-a-workflow-node) and [Step 4](#step-4-run-the-workflow) put it in the `providerConfigId` of the node that calls the service. ### Authentication The `config.auth` block holds the authentication the external service requires, as a `{ type, config }` pair. Use the method your service expects. | Type | Description | Config fields | | - | - | - | | `none` | No authentication. | — | | `api_key` | API key in header or query. | `key`, `header_name`, `location`, `query_param_name`, `prefix` | | `bearer` | Bearer token in Authorization header. | `token` | | `basic` | Username and password (Base64). | `username`, `password` | | `oidc_client_credentials` | OAuth 2.0 client credentials flow with automatic token management. | `issuer_url`, `client_id`, `client_secret`, `scopes` | | `oidc_user` | OAuth 2.0 resource owner password flow. | `issuer_url`, `client_id`, `username`, `password`, `client_secret`, `scopes` | | `oauth2_token_endpoint` | OAuth 2.0 client credentials against a token endpoint (no OIDC discovery). | `token_url`, `client_id`, `client_secret`, `scopes` | | `hmac` | Signs each request with a shared HMAC secret. | `secret`, `algorithm`, `encoding`, `header_name`, `signature_prefix`, `signing_string`, `timestamp_header` | A read never returns the credential the authentication type declares. Store credentials only in the fields the authentication type declares. To rotate a secret, send the new value in an update. To keep the current one, omit the field or send it blank. This works while `auth.type` stays the same. An update that changes `auth.type` must carry a value for each secret the new type requires and the previous one did not. Otherwise Flowker rejects it with `FLK-0952`. A change between two types that use the same secret, such as `oidc_user` to `oidc_client_credentials`, does not need that value again. > [!TIP] > > For OAuth 2.0 integrations, use `oidc_client_credentials`. Flowker handles token acquisition and renewal automatically. > **Example: OIDC client credentials** > > ```json theme={null} > { > "auth": { > "type": "oidc_client_credentials", > "config": { > "issuer_url": "https://auth.fraudshield.com/realms/fraudshield", > "client_id": "flowker-integration", > "client_secret": "secret-value", > "scopes": ["transactions:read", "transactions:score"] > } > } > } > ``` ### Enabling and disabling Provider configurations have two statuses: `active` (in use) and `disabled` (temporarily offline). A new provider configuration starts in `active` status. Use [Disable provider configuration](/en/reference/products/flowker/disable-provider-configuration) to take a connection out of service and [Enable provider configuration](/en/reference/products/flowker/enable-provider-configuration) to bring it back. See the [Provider configurations API](/en/reference/products/flowker/list-provider-configurations) for the full reference. ## Step 3: Reference the provider configuration from a workflow node *** Every executor node carries a `providerConfigId`, the identifier of the provider configuration it calls through. Flowker rejects a workflow whose executor node has no `providerConfigId`, and rejects a value that is not a UUID. At run time it builds each outgoing request from the base URL of that provider configuration plus the path on the node. The node fails if the provider configuration is not `active`. These are the fields an executor node sets in its `data` object when it calls through the generic HTTP connector: | Field | Required | Description | | - | - | - | | `executorId` | Yes | The catalog executor this node invokes, taken from [Step 1](#step-1-explore-the-catalog). Flowker rejects the workflow when the id is not in the catalog. A node that calls an uploaded OpenAPI document omits it (see below). | | `providerConfigId` | Yes | The UUID of the provider configuration this node calls through. | | `path` | No | The request path appended to the provider configuration's base URL. The destination host is always that base URL. A node cannot supply an absolute URL. | | `endpointName` | No | Used as the request path when the node sets no `path`. A node that carries both sends `path`. | | `method` | No | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS`. Defaults to `POST`. | | `headers` | No | Request headers, merged over the ones the provider configuration defines. A node header wins over a provider-configuration header of the same name. | | `query` | No | Query parameters appended to the URL. | | `auth` | No | A `{type, config}` authentication block for this node, in the same shape the provider configuration uses. When present it takes precedence over the authentication on the provider configuration. | | `body` | No | An explicit request body, resolved against the workflow context. When set, it is the only body source. Field mappings are not applied to it. | | `content_digest` | No | A SHA-256 pin for the assembled request body. Set a 64-character hexadecimal digest, or a value resolved from the workflow context. Flowker hashes the final bytes after assembling the body and sends the request only when they match. | | `config` | No | Fixed literal values that seed the request body. See [Field mapping and data transformation](#field-mapping-and-data-transformation). | | `inputMapping`, `outputMapping`, `transforms` | No | Field mappings and transformations. See [Field mapping and data transformation](#field-mapping-and-data-transformation). | | `timeout_seconds`, `retry`, `success_status_codes` | No | Per-node resilience settings. See [Retry and circuit breaker](#retry-and-circuit-breaker). | | `request_format` | No | An object whose `type` sets how Flowker serializes the request body: `json` (the default), `xml_converted`, or `xml_passthrough`. `xml_converted` also requires `root_element`. | A node that calls an operation of an uploaded OpenAPI document names it with `operation_path` and `operation_method` instead of an `executorId`. Flowker fills the `executorId` in for you from the provider configuration the node points at. [Connecting your own API](/en/products/flowker/connecting-your-own-api) walks that whole path. ### Validate a node configuration before you save Call the [Validate a node configuration](/en/reference/products/flowker/validate-executor-config) endpoint (`POST /v1/catalog/executors/{id}/validate`) to check a node's configuration against the catalog executor's JSON Schema. This performs **JSON Schema validation only**. It checks that your configuration object matches the structure the catalog executor expects (required fields, types, formats). It does not call the external service, so the first real round trip happens when a workflow runs the node. Pass `mappedTargets` to name the fields your node supplies through an `inputMapping` rather than a fixed value. Those fields count as satisfied, so a node that maps a required field from the trigger validates before you save it. ## Field mapping and data transformation *** Use field mappings and transformations when workflow data doesn't match the format an external service expects. Use them too when a service returns data in a shape the next step can't consume. You define field mappings and transformations inside the `data` object of executor nodes. Flowker applies input mappings before calling the external service, and output mappings after receiving the response. An input `target` is a path in the outgoing request body, written exactly as the external service expects it. There is no wrapper object and no prefix to add. An output `source` is a path into the response envelope, so response fields sit under `body`. > **Quick example: mapping workflow fields to an executor node** > > ```json theme={null} > { > "id": "executor-balance", > "type": "executor", > "name": "Check Balance", > "data": { > "executorId": "http", > "providerConfigId": "a1b2c3d4-e5f6-4789-a012-345678901234", > "path": "/accounts/balance", > "inputMapping": [ > { "source": "workflow.customerId", "target": "accountId" }, > { "source": "workflow.amount", "target": "minimumBalance" } > ], > "outputMapping": [ > { "source": "body.currentBalance", "target": "balance" }, > { "source": "body.accountStatus", "target": "status" } > ] > } > } > ``` > > Downstream nodes read the mapped output under this node's ID: `${executor-balance.balance}`. For complex integrations, you can also attach transformations to individual mapping entries (e.g., stripping characters, adding prefixes, changing case). You can define Kazaam operations for advanced JSON-to-JSON transformations. [Working with request and response data](/en/products/flowker/working-with-request-and-response-data) walks the whole path. It covers declaring the mappings, choosing what builds the request body, and reshaping values in flight. It also covers reading the response back out, and checking the assembled request before you call the service. ## Step 4: Run the workflow *** Reference the provider configuration in a workflow node of type `executor`. The example below creates a payment validation workflow on top of the FraudShield connection from [Step 2](#step-2-create-a-provider-configuration). When a payment arrives, Flowker calls the fraud check service, evaluates the risk score, and either approves or rejects the payment based on the result. The workflow has five nodes. A webhook **trigger** receives the payment, and an **executor** node calls the fraud check service. A **conditional** node evaluates the score, and there are two **action** nodes for the approve and reject outcomes. Edges connect them in sequence, with the conditional node branching to either path based on the score threshold. Use the [Create workflow](/en/reference/products/flowker/create-workflow) endpoint to define the workflow, then [Activate](/en/reference/products/flowker/activate-workflow) it, and finally [Execute](/en/reference/products/flowker/execute-workflow) it. > **Example: Create a payment validation workflow** > > ```json theme={null} > POST /v1/workflows > > { > "name": "payment-validation", > "description": "Validates a payment before processing.", > "nodes": [ > { > "id": "trigger-payment", > "type": "trigger", > "name": "Payment received", > "position": { "x": 0, "y": 0 }, > "data": { > "triggerType": "webhook", > "path": "payments/received", > "method": "POST", > "input_contract": "open", > "format": "json" > } > }, > { > "id": "check-fraud", > "type": "executor", > "name": "Fraud check", > "position": { "x": 200, "y": 0 }, > "data": { > "executorId": "http", > "providerConfigId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", > "path": "/score-transaction", > "method": "POST", > "inputMapping": [ > { "source": "workflow.transactionId", "target": "transactionId" }, > { "source": "workflow.amount", "target": "amount" } > ] > } > }, > { > "id": "evaluate-score", > "type": "conditional", > "name": "Score evaluation", > "position": { "x": 400, "y": 0 }, > "data": { > "condition": "check-fraud.body.score < 80" > } > }, > { > "id": "approve", > "type": "action", > "name": "Approve payment", > "position": { "x": 600, "y": -100 }, > "data": { > "actionType": "set_output", > "output": { "decision": "approved" } > } > }, > { > "id": "reject", > "type": "action", > "name": "Reject payment", > "position": { "x": 600, "y": 100 }, > "data": { > "actionType": "set_output", > "output": { "decision": "rejected" } > } > } > ], > "edges": [ > { "id": "e1", "source": "trigger-payment", "target": "check-fraud" }, > { "id": "e2", "source": "check-fraud", "target": "evaluate-score" }, > { "id": "e3", "source": "evaluate-score", "target": "approve", "sourceHandle": "true" }, > { "id": "e4", "source": "evaluate-score", "target": "reject", "sourceHandle": "false" } > ] > } > ``` > > The `check-fraud` node names `http`, the generic HTTP connector from the catalog. It also names the FraudShield configuration from Step 2, which holds the base URL and the credentials. Both sides name the same provider, so the workflow saves. Flowker sends the request to `https://api.fraudshield.example.com/score-transaction`. > > The node declares no `outputMapping`, so its output keeps the response envelope shape. The score therefore sits at `check-fraud.body.score`, which is what the `evaluate-score` condition reads. Add an `outputMapping` when you prefer a flatter name. See [Field mapping and data transformation](#field-mapping-and-data-transformation). > **Example: Execute the workflow** > > ```json theme={null} > POST /v1/workflows/{workflowId}/executions > Idempotency-Key: {unique-uuid} > > { > "inputData": { > "transactionId": "txn-98765", > "amount": 1500.00, > "currency": "BRL", > "customerId": "cust-12345" > } > } > ``` ## Triggering workflows *** You trigger workflow executions via the [Execute workflow](/en/reference/products/flowker/execute-workflow) endpoint: ``` POST /v1/workflows/:workflowId/executions ``` The request body contains the `inputData` for the execution. All fields are available to subsequent nodes via the `workflow` namespace (for example, `workflow.transactionId` or `workflow.amount`). Node outputs are available via the node's ID (for example, `check-fraud.body.score` for a node that declares no `outputMapping`). ### Idempotency Every Execute workflow request must include an `Idempotency-Key` header. A request without it fails with `400 Bad Request` (error `FLK-0509`). Generate a fresh UUID for each new execution, and reuse the same key only when retrying the identical request. ## Webhook triggers *** Webhooks are the primary way external systems trigger Flowker workflows. Instead of your system calling the executions API directly, you register a webhook path in a workflow. External services then send HTTP requests to that path. ### How it works 1. Add a trigger node of type `webhook` to your workflow with a `path` and a `method` in its `data`. Set `input_contract` explicitly for new nodes when you need `open`, `xsd`, or `openapi` validation. 2. When you activate the workflow, Flowker registers the path in its webhook registry. 3. External systems send requests to [`POST /v1/webhooks/{path}`](/en/reference/products/flowker/trigger-webhook) (or the method you configured). 4. Flowker resolves the path to the matching workflow and executes it. ### Defining a webhook trigger node The webhook trigger is a node with `type: "trigger"` and `triggerType: "webhook"` in its `data`, plus a `path`, a `method`, and an optional `input_contract`. [Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger) covers every field, the three `input_contract` modes and what each one requires, and carries a worked node for each mode. The trigger configuration follows a closed contract. Saving a workflow fails with `FLK-0934` when its webhook trigger omits `path` or `method`, or misses a field its selected `input_contract` mode requires. It also fails when the trigger names another mode's schema id or operation field. It fails too when the trigger carries a key or a value the schema does not accept. An `accepted_headers` name that is not a valid header name, repeats another name or is on the denylist fails with `FLK-0957` instead. The schema also declares the optional `response_mode`, `response_view`, and `accepted_headers` fields. See [Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger). ### Securing a webhook With `PLUGIN_AUTH_ENABLED=true`, requests to `/v1/webhooks/*` require a Bearer token and the `execute` permission on the `webhooks` resource. Production deployments require plugin authentication. Grant that permission to a machine-to-machine identity for each system you let call your webhooks, and manage the grant in Access Manager. This keeps webhook access under the same role and policy model as workflow management, rather than a credential attached to the path. ### Webhook metadata Flowker automatically injects a `_webhook` object into the execution's `inputData` with metadata about the incoming request: | Field | Description | | - | - | | `_webhook.method` | HTTP method used (e.g., `POST`). | | `_webhook.path` | The request path after the route prefix that received the call (`/v1/webhooks/`, or `/v1/hub/webhooks/` for the Streaming Hub ingress), as received. | | `_webhook.headers` | Request headers, filtered through a safe allowlist (`Content-Type`, `Accept`, `User-Agent`, `X-Request-Id`, `X-Forwarded-For`, `Idempotency-Key`) plus this trigger's optional `accepted_headers` and, for an `openapi` route, its operation's header parameters. The fixed denylist blocks `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`, `X-Auth-Token`, and `X-Webhook-Signature`. Other admitted headers are persisted verbatim; never admit secret-bearing names such as `X-Amz-Security-Token`. | | `_webhook.query` | Preserves the names of received query parameters. It preserves values only for `customerId`, `page`, `cursor`, `limit`, `offset`, and `sortOrder` (case-insensitive); every other value is stored as `[redacted]`. | | `_webhook.remote_ip` | IP address of the caller. | This metadata is available to all nodes in the workflow via the `workflow._webhook` namespace. ### Path rules and body size * Activating a workflow whose webhook path and method another active workflow holds fails with `FLK-0360`. * Webhook paths support nested segments (e.g., `payments/stripe/received`). * The request body maximum size is 1 MB. See the [Trigger a webhook](/en/reference/products/flowker/trigger-webhook) API reference for the complete endpoint documentation. ### Synchronous response mode By default, a webhook trigger responds with a `202` receipt as soon as the execution starts (the async mode). The caller must poll the execution status separately. Set `response_mode` to `"sync"` in the trigger node's `data` to have Flowker hold the HTTP connection open and return the execution's outcome directly in the response: | Field | Type | Required | Description | | - | - | - | - | | `response_mode` | string | No | `"async"` (default) returns a `202` receipt immediately. `"sync"` blocks (up to an internal cap) for the execution to reach a terminal state and returns the outcome in the response body. | | `response_view` | string | No | Shapes the sync response body. Only meaningful when `response_mode` is `"sync"`. See the table below. Defaults to `"full"`. | If the execution does not reach a terminal state before the internal wait cap elapses, Flowker falls back to the async mode's `202` receipt. That receipt carries a `Location` header pointing at the results endpoint. `response_view` selects the shape of the sync response body: | Value | Description | | - | - | | `full` (default) | The complete execution dump (`executionId`, `workflowId`, `status`, `stepResults`, `finalOutput`), in the same shape you would fetch from the execution results endpoint. | | `final_output` | Only the execution's `finalOutput` map, with no envelope wrapper. | | `receipt` | The lean execution receipt (`executionId`, `workflowId`, `status`, `startedAt`), in the same shape the async path returns. | | `passthrough` | Shapes the response by the terminal (last-executed) step's node type. For a terminal executor that captured a provider response, it returns the provider status (including `4xx`) and `Content-Type`. It relays at most 8 KiB of the captured body; longer bodies are truncated. JSON bodies are decoded and re-serialized before capture, so byte-for-byte relay is not guaranteed. If the terminal step is a `set_output` action with a configured output, the response is that node's business output, honoring its `responseStatusCode` override. When the execution failed, its last step captured no provider response, and no `set_output` node that ran set a `responseStatusCode`, the response is the `full` envelope at HTTP `200`. | A failed execution's `finalOutput` in the `final_output` view carries `status: "failed"` and `errorMessage`, and `errorClass` when Flowker could classify the failure, never a bare `{}`. Absent a `responseStatusCode` override (see below), the sync HTTP status stays `200` for `full`/`final_output`/`receipt` (it reports transport health, not business outcome). A valid `responseStatusCode` on a `set_output` node that runs overrides that status for those three views. An action node with `actionType: "set_output"` can carry an optional `responseStatusCode` (integer, `200`–`599`) to override the HTTP status a `sync` webhook response returns. An out-of-range or non-integer value fails at save time (`FLK-0122`). For `passthrough`, a terminal executor's relayed provider status wins over the override. A `set_output` terminal downstream of an executor shapes the response as its own output. Flowker never walks back to an earlier executor's response. On a failed execution the halting step is the terminal step, so Flowker relays a provider `4xx` that stopped the workflow as the real `4xx`. Values in a `set_output` node's output support `${...}` references resolved against the workflow context, including `${workflow.}` (trigger payload), `${execution.id}`, `${execution.startedAt}`, and `${execution.now}` (stamped at interpolation time). An unresolvable `${...}` reference fails the step (fail-closed). ## Error handling *** If a node fails, the execution stops and its status becomes `failed`. There is no automatic fallback. After the retries run out, the execution fails. Execution results report the execution `status` and `stepResults`. A failed step provides `stepNumber`, `nodeId`, `status`, and `errorMessage`, with `statusCode` and `errorClass` when available. The `output` field is optional. ## Retry and circuit breaker *** Flowker includes built-in resilience for executor calls. ### Retries When an executor call fails with a transient error (a network error, a timeout on the attempt, any `5xx` status, or status `408` or `429`), Flowker retries automatically. Retry behavior is configurable per node, in the executor node's `data`: | Setting | Default | Bounds | Description | | - | - | - | - | | `timeout_seconds` | 30 | 1–300 | Per-request timeout. | | `retry.max_attempts` | 3 (1 for unconfigured `POST` and `PATCH`) | 1–10 accepted; 1–5 effective | Accepts `1`–`10` in the node schema, but Flowker clamps the effective runtime attempt count to `1`–`5`. | | `retry.backoff_seconds` | 1 | 1–60 | First backoff ceiling; each wait is a random value between zero and the ceiling, which doubles per attempt. | | `success_status_codes` | `[200, 201, 202, 204]` | 100–599 | HTTP status codes treated as success. | Retries only apply when the operation is safe to repeat. By default, Flowker treats `POST` and `PATCH` calls as non-idempotent and does **not** retry them (a single attempt), while `GET`, `PUT`, `DELETE`, and other verbs retry normally. A `retry.max_attempts` greater than `1` opts that node into retries whatever the method is. A `retry.max_attempts` of `1` is not an opt-in. It sets a single attempt. **Non-retryable errors** short-circuit to a single attempt regardless of configuration. They are: circuit breaker open, context cancelled, configuration errors, and secret-resolution failures. They also include a request body over the configured size cap, a provider response body over the same cap, and non-transient `4xx` provider responses. That means any `4xx` except `408` and `429`. The retry applies per node execution. If all attempts fail, the step fails and the execution stops. ### Circuit breaker Flowker uses a circuit breaker so that repeated failing calls do not overwhelm external services: | Parameter | Value | | - | - | | Failure threshold | 20 consecutive failures opens the circuit | | Recovery timeout | 30 seconds before trying again (half-open state) | | Half-open requests | 1 request allowed to test if the service recovered | A provider `4xx` response does **not** count toward the threshold, except `408` and `429`: it reflects the request, not the health of the provider. When the circuit is open, executor calls fail immediately with `FLK-0507` instead of reaching the external service. This prevents cascading failures and gives the external service time to recover. ![Circuit breaker states](https://mintcdn.com/lerian-49cb71fc/Mmb3JaVhlcaSV8yn/images/en/d2/flowker-circuit-breaker.svg?fit=max\&auto=format\&n=Mmb3JaVhlcaSV8yn\&q=85\&s=60f59fb0d092e1f2ad73ea4b7628627e) Circuit breaker state transitions The circuit starts in the **Closed** state, where all requests pass through normally. After the circuit reaches the failure threshold, it transitions to **Open** and blocks all requests immediately. After 30 seconds, it moves to **Half-Open** and allows one test request. If that request succeeds, the circuit returns to Closed. If it fails, the circuit reopens for another 30-second cycle. > [!WARNING] > > The circuit breaker operates per provider configuration, scoped to your tenant. Failures against one connection do not affect another, and one tenant cannot open the circuit for another. Circuit breaker thresholds (failure count, recovery timeout) are global deployment defaults. You cannot customize them per connection. ## What's next *** > **[Core concepts](/en/products/flowker/flowker-concepts)** > > Understand workflows, nodes, edges, and executions. > **[Provider configurations API](/en/reference/products/flowker/list-provider-configurations)** > > Explore the provider configuration API. # Running a workflow on a schedule Source: https://docs.lerian.studio/en/products/flowker/running-a-workflow-on-a-schedule Start a Flowker workflow on a cadence, read the occurrences it plans to run, and decide what happens to an occurrence that could not run on time. A schedule trigger starts a workflow on a cadence you write as a cron expression. Flowker records the next firing as an occurrence. An occurrence that cannot run on time waits in a parked list until you run it or discard it. Use this page to write the trigger, confirm the cadence, and work the parked list. ## Before you start *** * A workflow in `draft` status. Only a draft workflow accepts an edit, so write the trigger before you activate it. See [Step 4: Run the workflow](/en/products/flowker/integration-guide#step-4-run-the-workflow) for the create-and-activate path. * The `read` permission on the `workflows` resource to list occurrences and counts, and `update` on the same resource to run or discard one. ## Step 1: Write the schedule trigger node *** Triggers come built in. You discover them in the catalog, and you never create one. [Get a catalog trigger](/en/reference/products/flowker/get-catalog-trigger) returns the schedule trigger's JSON Schema from the running instance: ```bash theme={null} curl -s https://your-flowker-host/v1/catalog/triggers/schedule \ -H "Authorization: Bearer " | jq -r '.schema' | jq . ``` The schedule trigger is a node with `type: "trigger"` and these fields in its `data`: | Field | When you set it | Value | | - | - | - | | `triggerType` | Always | `"schedule"`. | | `cron` | Always | The cadence, as a standard 5-field cron expression: `minute hour day-of-month month day-of-week`. | | `timezone` | Optional | The IANA identifier of the zone the cron fields belong to, such as `"America/Sao_Paulo"`. Defaults to `UTC`; an empty timezone is also evaluated as `UTC`. A name that is not an IANA zone fails with `FLK-0119` when you save or activate the workflow. | | `enabled` | Optional | `false` stops the schedule from firing while the workflow stays active. Defaults to `true`. | | `inputData` | Optional | An object that starts every run. The workflow reads it as `workflow.*`, the way a webhook run reads its request body. | ### What the cron expression accepts Five fields, separated by spaces. Each field takes `*`, a value, a list (`0,30`), a range (`9-17`), or a step (`*/15`, `9-17/2`). Day-of-week runs `0`–`7`, where both `0` and `7` mean Sunday. When you restrict day-of-month and day-of-week at the same time, the schedule fires on a day that matches either field. The expression `0 9 13 * 5` fires on the 13th and on every Friday. One minute is the finest cadence a 5-field expression can express. For anything faster, take the call in as it arrives with a [webhook trigger](/en/products/flowker/configuring-a-webhook-trigger). Flowker checks the expression when you save the workflow and again when you activate it, and answers `FLK-0117` when it does not hold. These forms do not hold: * A six-field expression, such as `*/30 * * * * *`. * A macro, such as `@daily` or `@every 5m`. * A named day or month, such as `MON`, `MON-FRI` or `sun`. * An `L` or `#` token, such as `0 9 L * *` or `0 9 * * 5#2`. * A value outside its field's range, such as `60` minutes, `24` hours, day-of-month `0` or `32`, month `13`, or day-of-week `8`. * A `/0` step. ### How the timezone works The cron fields are wall-clock time in the zone you name, and every time the API returns is UTC. A `0 9 * * *` schedule in `America/Sao_Paulo` reports `12:00Z`. A zone that observes daylight saving keeps the wall-clock hour across the change: the same expression in `America/New_York` reports `14:00Z` in winter and `13:00Z` in summer. ```json daily theme={null} { "id": "trigger-1", "type": "trigger", "name": "Daily settlement", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "schedule", "cron": "0 9 * * *", "timezone": "America/Sao_Paulo" } } ``` ```json every 15 minutes theme={null} { "id": "trigger-1", "type": "trigger", "name": "Poll for new statements", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "schedule", "cron": "*/15 * * * *" } } ``` ```json weekdays, paused theme={null} { "id": "trigger-1", "type": "trigger", "name": "Weekday reconciliation", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "schedule", "cron": "30 7 * * 1-5", "timezone": "America/Sao_Paulo", "enabled": false } } ``` ```json monthly theme={null} { "id": "trigger-1", "type": "trigger", "name": "Month-open batch", "position": { "x": 0, "y": 0 }, "data": { "triggerType": "schedule", "cron": "0 3 1 * *", "timezone": "UTC" } } ``` ## Step 2: Activate the workflow and read the cadence *** **1. Save the workflow** Send the node with the rest of your workflow to [Create a workflow](/en/reference/products/flowker/create-workflow), or to [Update a workflow](/en/reference/products/flowker/update-workflow) if the draft already exists. Flowker validates the cron here. **2. Check the cadence before you commit to it** [List upcoming scheduled occurrences](/en/reference/products/flowker/list-upcoming-scheduled-occurrences) computes the next firings straight from the trigger, so you can read them while the workflow is still a draft. ```bash theme={null} curl -s "https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/upcoming?limit=3" \ -H "Authorization: Bearer " | jq . ``` ```json theme={null} { "occurrences": [ { "scheduledFor": "2026-08-01T12:00:00Z" }, { "scheduledFor": "2026-08-02T12:00:00Z" }, { "scheduledFor": "2026-08-03T12:00:00Z" } ] } ``` `limit` takes `1` to `50` and defaults to `10`. A value outside that range answers `FLK-0304`. **3. Activate it** Call [Activate a workflow](/en/reference/products/flowker/activate-workflow). The leader-gated producer sweeps every 60 seconds. After a successful sweep, Flowker records and queues the next occurrence for its slot. ```bash theme={null} curl -s -X POST https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/activate \ -H "Authorization: Bearer " | jq . ``` Flowker records only the next firing, never a calendar of future firings. When that occurrence runs, the engine records the firing after it, so the cadence carries itself forward one occurrence at a time. ## Step 3: See which occurrences did not run *** An occurrence **parks** when the engine reaches it more than a minute after its slot and nobody has asked for that slot to run. Common causes are a service that was down, a queue that was behind, or a process that restarted. Flowker never runs a parked occurrence on its own. It holds it for your decision, in the `pending-review` state. An occurrence also parks when the queue gives up on it before its run starts. A run that starts and then fails is recorded as failed. A parked occurrence stays available to the run and discard endpoints. Flowker never back-fills a slot that passed. So the parked list holds the firings Flowker had already recorded and could not run. It does not hold one entry for every slot that went by during an outage. The cadence itself resumes from the next future slot. An occurrence is **skipped** when the engine took it up and closed it without running the workflow. The scheduler never runs a skipped occurrence again. A skipped occurrence carries a `skipReason`, and the run and discard endpoints do not accept it: | `skipReason` | What happened | | - | - | | `active-run` | Another run of this workflow was still going. A schedule runs one occurrence at a time, so this firing stood down. | | `workflow-gone` | The workflow was deleted, or it was not active when the occurrence reached the engine. | | `execution-duplicate` | The work for that slot had already run, so the engine did not run it twice. | The two classes do not split on timing, with one exception. A late slot you never asked to run lands in the parked list, never in the skipped list. After you ask for a parked slot to run, its age stops holding it back. The engine then takes it up like any other occurrence, and all three reasons above can close it. So a `scheduledFor` far in the past is normal in the skipped list, and it does not mean the slot fired on time. [Step 4](#step-4-run-or-discard-a-parked-occurrence) covers what your own run can end in. Three reads cover the whole picture: **1. List the parked occurrences of one workflow** [List parked scheduled occurrences](/en/reference/products/flowker/list-parked-scheduled-occurrences) returns them oldest first. ```bash theme={null} curl -s https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed \ -H "Authorization: Bearer " | jq . ``` ```json theme={null} { "occurrences": [ { "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30", "workflowId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", "status": "pending-review", "cronExpr": "0 9 * * *", "timezone": "America/Sao_Paulo", "scheduledFor": "2026-07-29T12:00:00Z", "attempts": 0, "createdAt": "2026-07-29T11:00:04Z", "updatedAt": "2026-07-30T08:12:41Z" } ] } ``` `scheduledFor` is the slot the occurrence stands for, and `status` is what decides whether you can act on it. This route takes no `limit` and no page cursor, and it returns at most 100 occurrences. Plan a bulk recovery around that: work the rows you get, then read the list again. The count below reports the total in `pending-review`. The parked list can hold more rows than that count, because it also includes `missed` occurrences. The `missed` state is short-lived: a late slot holds it while the engine parks the slot as `pending-review`. The run and discard endpoints do not accept that state, so read the list again and act once the row shows `pending-review`. Discarding the whole list also covers every occurrence pending review, not only the 100 a single read shows you. **2. List the skipped occurrences** [List skipped scheduled occurrences](/en/reference/products/flowker/list-skipped-scheduled-occurrences) returns them oldest first, each with its `skipReason`. When supplied, `limit` takes `1` to `50`. When omitted, it defaults to `100`. ```bash theme={null} curl -s "https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/skipped?limit=10" \ -H "Authorization: Bearer " | jq '.occurrences[] | {scheduledFor, skipReason}' ``` ```json theme={null} { "scheduledFor": "2026-07-28T12:00:00Z", "skipReason": "active-run" } ``` Repeated `active-run` skips mean the workflow takes longer than the gap between two firings. Widen the cadence, or make the workflow finish faster. **3. Count what is waiting across every workflow** [Count pending-review occurrences](/en/reference/products/flowker/count-pending-review-occurrences) answers for the whole tenant in one call, which is what you poll for a review badge. The map is sparse: a workflow with nothing waiting is absent from it. ```bash theme={null} curl -s https://your-flowker-host/v1/workflows/schedule/pending-review-counts \ -H "Authorization: Bearer " | jq . ``` ```json theme={null} { "counts": { "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a": 2 } } ``` Add `?workflowIds=,` to scope the counts to the workflows you care about. Both lists answer `200` with an empty `occurrences` array for a workflow id your tenant does not own, so an empty list means "nothing to review here". Flowker exposes the underlying schedule data through its upcoming, parked, and pending-review-count APIs. Consult the Console documentation for UI guidance. ## Step 4: Run or discard a parked occurrence *** Run and discard act on an occurrence whose `status` is `pending-review`. Any other state answers `FLK-0755`, which also makes a repeated call safe: the second call answers an error instead of acting twice. Your own run puts the occurrence in one of those other states: it leaves `pending-review` straight away. A run you ask for leaves the parked list at once, and the age of the slot no longer holds it back. It does not promise that the workflow runs: * **The workflow runs.** The execution appears in [List executions](/en/reference/products/flowker/list-executions) for that workflow. * **The engine closes the occurrence as skipped.** It leaves the parked list for the skipped list with one of the three reasons above. An `active-run` reason means another run of the workflow was still in progress. A `workflow-gone` reason means the workflow was not active when your run reached the engine. An `execution-duplicate` reason means the work for that slot had already run. After your run, the scheduler never runs a skipped occurrence again, and the run and discard endpoints do not accept it. Read both lists before you conclude anything about a slot you tried to recover. The skipped row may record the refusal of your recovery, not of the original firing. Keep the workflow active while you work the list. A run reloads the workflow, and a workflow that is not active closes the occurrence as a `workflow-gone` skip instead of running it. **1. Run one occurrence** [Run a parked occurrence](/en/reference/products/flowker/run-parked-occurrence) moves it to `queued` and enqueues a force-run on the same execution path a scheduled firing uses. The delayed-task forwarder checks every second. Actual start time depends on worker availability and queue capacity. ```bash theme={null} curl -s -X POST https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30/run \ -H "Authorization: Bearer " | jq '{id, status}' ``` ```json theme={null} { "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30", "status": "queued" } ``` The run covers that one slot. It does not shift the cadence: the next firing stays the one the engine already planned. **2. Discard one occurrence** [Discard a parked occurrence](/en/reference/products/flowker/discard-parked-occurrence) moves it to `discarded`, which is terminal. The occurrence never executes and leaves the parked list. ```bash theme={null} curl -s -X POST https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30/discard \ -H "Authorization: Bearer " | jq '{id, status}' ``` ```json theme={null} { "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30", "status": "discarded" } ``` **3. Clear the whole parked list of one workflow** [Discard all parked occurrences](/en/reference/products/flowker/discard-all-parked-occurrences) discards, in a single write, every occurrence of that workflow pending review, and reports how many it moved. ```bash theme={null} curl -s -X POST https://your-flowker-host/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/discard-all \ -H "Authorization: Bearer " | jq . ``` ```json theme={null} { "discarded": 7 } ``` With nothing pending review it answers `200` with `"discarded": 0`, so a repeat call is safe. > [!WARNING] > > A discard cannot be undone, and it never runs anything. Read the parked list before you clear it. > > Discarding all parked occurrences covers exactly the occurrences of that one workflow, in your tenant, pending review. It leaves the schedule itself running and leaves the upcoming occurrences alone. It does not touch another workflow's occurrences, or occurrences that already ran, failed, were skipped, or were discarded earlier. ## Confirm it worked *** * The cadence is healthy when [List upcoming scheduled occurrences](/en/reference/products/flowker/list-upcoming-scheduled-occurrences) returns future slots and the parked list stays short. * A run worked when the occurrence has left the parked list and the execution shows up in [List executions](/en/reference/products/flowker/list-executions) for that workflow. * A discard worked when the occurrence has left the parked list and the pending-review count for that workflow has dropped. ## Change or pause a cadence *** Only a draft workflow accepts an edit, so a cadence change is four calls: 1. [Deactivate the workflow](/en/reference/products/flowker/deactivate-workflow). Flowker stops recording new occurrences for it. 2. [Move it to draft](/en/reference/products/flowker/move-workflow-to-draft). 3. [Update the workflow](/en/reference/products/flowker/update-workflow) with the new `cron`, `timezone`, or `enabled` value. 4. [Activate it](/en/reference/products/flowker/activate-workflow). The leader-gated producer sweeps every 60 seconds. After a successful sweep, Flowker records and queues the next occurrence from the new cadence. Flowker does not back-fill the slots that passed while the workflow was inactive. The parked occurrences survive all four steps. They stay listed and stay actionable once the workflow is active again. > [!NOTE] > > A parked occurrence Flowker recorded before the change remains pending review. Read the parked list after a cadence change, and clear anything you no longer want. ## When something goes wrong *** | Code | When it happens | What to do | | - | - | - | | `FLK-0117` | You save or activate the workflow. | The cron is not a standard 5-field expression. Compare it against [What the cron expression accepts](#what-the-cron-expression-accepts). | | `FLK-0119` | You save or activate the workflow. | The `timezone` is not an IANA zone name. Use an identifier such as `America/Sao_Paulo`. | | `FLK-0118` | You list the upcoming occurrences. | The workflow carries no schedule trigger, or its cron is not an expression Flowker can compute. Check the trigger node's `triggerType` and `cron`. | | `FLK-0100` | You list the upcoming occurrences. | No workflow in your tenant has that id. | | `FLK-0002` | Any of these calls. | A path id is not a valid UUID. | | `FLK-0304` | You list upcoming or skipped occurrences. | `limit` is outside `1`–`50`. | | `FLK-0755` | You run or discard an occurrence. | The occurrence is not pending review. Your own run may have moved it on already, or it may have run, been skipped, or been discarded. Read the parked list and the skipped list before you call again. | | `FLK-0760` | You run or discard an occurrence. | No occurrence of that workflow has that id. Take the id from the parked list of the same workflow. | | `FLK-0762` | You run an occurrence. | A run of that occurrence is already in flight. Read the parked list and the skipped list before you call again. | | `FLK-0763` | Any of these calls. | The scheduler is temporarily unavailable for your tenant. Listing and discarding may still work. If it persists, contact Lerian support. | Two failures answer no error code: * **The upcoming list shows occurrences, but nothing ever runs.** Confirm that the trigger node has `enabled` set to `true` and that the workflow is active. If both hold and nothing runs, contact Lerian support with the workflow id. * **Flowker records nothing new for an active workflow.** Check `enabled` on the trigger node: `false` keeps the workflow active and its schedule quiet. ## What's next *** > **[Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger)** > > Start the same workflow from an inbound HTTP call instead of a cadence. > **[Workflow design guide](/en/products/flowker/workflow-design-guide)** > > Build the rest of the graph the trigger enters. # Security Source: https://docs.lerian.studio/en/products/flowker/security Protect Flowker workflows, data, and integrations with Access Manager authentication, per-connection credentials, and TLS on every API call. Flowker protects your workflows, data, and integrations through Access Manager authentication and per-connection credential management. Lerian serves the API over TLS. This page covers the security model as implemented in the current release. ## Platform authentication *** Flowker delegates platform authentication to **Access Manager**. Every request to a protected API route must carry a Bearer token (OIDC JWT), and each protected route enforces a per-resource, per-action permission. This is how role- and policy-based authorization works. ```bash theme={null} curl -X GET https://your-flowker-instance/v1/workflows \ -H "Authorization: Bearer " ``` Invalid or missing credentials return `401 Unauthorized`. ## Provider authentication *** When Flowker calls an external service, it authenticates with the credentials on the provider configuration that the node calls through. Your platform credentials and your provider credentials stay separate. **Supported authentication types:** | Type | Description | Use case | | - | - | - | | `none` | No authentication | Internal services behind a VPN or service mesh | | `api_key` | API Key sent as header or query parameter | Third-party APIs with key-based access | | `bearer` | Bearer token in the `Authorization` header | Services using static or pre-generated tokens | | `basic` | HTTP Basic authentication (username:password) | Legacy systems or internal APIs | | `oidc_client_credentials` | OAuth 2.0 client credentials flow | Machine-to-machine integrations with identity providers | | `oidc_user` | OAuth 2.0 user token flow | Integrations that act on behalf of a specific user | | `oauth2_token_endpoint` | OAuth 2.0 client credentials against a token endpoint (no OIDC discovery) | OAuth2-style providers without OIDC discovery metadata | | `hmac` | Request signing with a shared HMAC secret | Providers that verify a request signature header | The `config.auth` block on the provider configuration holds the authentication the external service requires, as a `{ type, config }` pair. Flowker applies it to every call a node makes through that connection. A read never returns the credential the authentication type declares. Store credentials only in the fields the authentication type declares. To rotate a secret, send the new value in an update. To keep the current one, omit the field or send it blank. This works while `auth.type` stays the same. An update that changes `auth.type` must carry a value for each secret the new type requires and the previous one did not. Otherwise, Flowker rejects it with `FLK-0952`. A change between two types that use the same secret, such as `oidc_user` to `oidc_client_credentials`, does not need that value again. ```json theme={null} { "config": { "auth": { "type": "bearer", "config": { "token": "eyJhbGciOiJSUzI1NiIs..." } } } } ``` > [!NOTE] > > For OIDC flows (`oidc_client_credentials` and `oidc_user`), Flowker handles token acquisition and refresh automatically. For `oidc_client_credentials`, provide the issuer URL, client ID, and client secret. For `oidc_user`, provide the issuer URL, client ID, username, and password. `client_secret` is optional for public clients. ## Network security *** **TLS:** * Lerian terminates TLS for Flowker API traffic. * Use `https://` base URLs for external calls. Flowker accepts a URI for the generic HTTP provider's `base_url`. It does not restrict it to HTTPS. * Transmit credentials and sensitive payloads only over encrypted links. **CORS configuration:** Flowker applies these CORS rules: * Only allowed origins can call the API from a browser. To add an origin, contact Lerian support * Cross-origin requests cannot carry credentials (`AllowCredentials` stays off) * Preflight responses tell the browser to cache them for one hour ## Resilience *** Flowker protects against cascading failures from external services using circuit breaker and retry patterns. **Circuit breaker:** When an external service fails repeatedly, the circuit breaker opens and stops sending requests. This prevents your workflows from hanging on an unresponsive provider. * Transitions through `closed` → `open` → `half-open` states * Each circuit applies to one provider configuration and one tenant, so failures against one connection do not affect another * The half-open state allows a limited number of test requests before fully closing **Retries:** Flowker resolves the retry budget for each node with the first two rules. The failure class then decides whether that budget is spent: 1. **Node opt-in.** A `retry.max_attempts` greater than `1` turns retries on whatever the method is. The value is the total attempt count, and the platform caps it at 5. A `retry.max_attempts` of `1` is not an opt-in. It sets a single attempt. 2. **HTTP method.** With no opt-in, Flowker treats `POST` and `PATCH` as non-idempotent and gives them a single attempt. Every other verb retries, with 3 total attempts by default. This includes `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE`. 3. **Failure class.** The budget is spent only on a transient failure: a network error, a timeout on the attempt, any `5xx` status, or status `408` or `429`. Every other `4xx` fails on the first attempt however high the budget is. An open circuit and a cancelled execution also stop the loop. A request body over the configured size cap and a response body over the same cap stop it too. Backoff is exponential with full jitter. Each wait is a random value between zero and a ceiling. The ceiling starts at 1 second and doubles on each attempt. `retry.backoff_seconds` sets the first ceiling, between 1 and 60. The random wait prevents many executions from retrying the same service at the same moment. ## What's next *** > **[Integration guide](/en/products/flowker/integration-guide)** > > Learn how to create provider configurations and connect external services. > **[Observability](/en/products/flowker/flowker-observability-guide)** > > Track workflow runs through their execution records. # Use cases Source: https://docs.lerian.studio/en/products/flowker/use-cases Common financial scenarios where Flowker orchestrates validation, provider calls, and data routing. Flowker targets the operations of financial institutions. This page walks through five common scenarios for structuring workflows. The controls and outcomes depend on the external systems and integration enforcement you configure. ## Transaction validation *** ### The problem When validation happens **after** a transaction is written to the ledger, the damage is already done. Invalid writes create compliance violations, trigger costly rollbacks, and erode data integrity. Remediation is expensive, time-consuming, and often incomplete. ### How Flowker solves it Use a Flowker workflow to orchestrate KYC, AML, fraud-scoring, and balance-validation calls before a ledger-posting step. You define the sequence in the order your compliance policies require. Your ledger integration must enforce that no other path can post without the required checks. If a validation node fails, the workflow execution fails. Execution records capture the configured processing steps and their results. ### Business outcome * **Configurable validation path**: place the checks required for your operation before its posting step * **Execution records**: inspect configured processing steps and their results * **Integration-owned posting control**: enforce the required checks at the ledger boundary ## Customer onboarding (KYC/AML) *** ### The problem Customer onboarding in financial services involves identity verification, document checks, AML screening, and risk scoring, often spread across multiple systems, teams, and manual handoffs. It can be difficult to see where a customer is in the process or which checks have completed. ### How Flowker solves it Flowker lets you define the entire onboarding sequence as a single workflow: identity verification, then document check, then AML screening, then risk scoring. Each step calls the appropriate external provider automatically. Conditional logic routes customers to enhanced verification when risk scores exceed your thresholds. The workflow tracks every step, so you always know exactly where each customer is in the process. ### Business outcome * **Automated sequencing**: structure the configured checks and handoffs in one workflow * **Standardized process**: every customer goes through the same validated steps, regardless of which team handles them * **Evidence-ready**: the execution record shows exactly what checks ran, when, and with what results ## Payment orchestration *** ### The problem Payment routing involves multiple providers, each with different APIs, authentication methods, and response formats. Building and maintaining custom integrations for each provider consumes engineering resources and creates fragile, hard-to-change payment flows. ### How Flowker solves it Flowker coordinates multi-step payment flows as a single workflow: fraud check, then balance verification, then payment gateway call, then confirmation. You configure each service instance once as a provider configuration that holds its base URL and credentials. Every workflow node that calls it carries its own field mappings. If a provider call fails on a transient error, Flowker retries it automatically with exponential backoff. A `POST` or `PATCH` call runs once unless the node opts into retries. Circuit breakers prevent cascading failures when a provider goes down. ### Business outcome * **Consistent payment processing**: every payment follows the same validated path * **Automatic failure recovery**: Flowker retries transient errors without manual intervention * **Provider resilience**: circuit breakers isolate failing providers so the rest of your operations continue ## Fraud detection workflows *** ### The problem In many systems, fraud checks come after the transaction is already in progress, or worse, after it's committed. By the time a fraudulent transaction is flagged, the financial and reputational damage may already be done. ### How Flowker solves it Use Flowker to run fraud scoring before the posting or action step that you configure. Conditional branching lets you define different paths based on risk level. Low-risk transactions can continue. Medium-risk transactions can trigger additional verification. High-risk transactions can route to a rejection path. The integration that posts or acts on the transaction must enforce that its required Flowker checks ran. A workflow definition alone cannot block another system path. ### Business outcome * **Risk decisions in the configured flow**: place fraud scoring before the posting or action step * **Risk-based routing**: different risk levels trigger different responses automatically * **Traceable workflow checks**: inspect the configured score, branch, and execution record ## Provider integration acceleration *** ### The problem Integrating an external provider involves its API contract, authentication, error handling, security review, and testing. The effort varies by provider and by the controls your integration requires. ### How Flowker solves it Flowker separates the connection from the workflow logic. You create a provider configuration (base URL, credentials, and the hosts it may call) and reference it from workflow nodes. The mappings that reshape data for that service live on the node. You can update settings of an existing provider configuration. A migration that changes provider, executor, or provider-configuration ID can also require updates to draft workflow nodes. Flowker validates catalog provider configurations against the provider JSON Schema. `external_openapi` configurations use dedicated external OpenAPI and configuration validation. ### Business outcome * **Centralized configuration and mappings**: keep connection settings and node mappings in their respective workflow surfaces * **Reusable across workflows**: configure a provider once, use it everywhere * **Provider migration control**: update settings directly and update draft nodes when their provider or executor reference changes ## What's next *** > **[Getting started](/en/products/flowker/flowker-getting-started)** > > Set up Flowker and run your first workflow end to end. > **[Integration guide](/en/products/flowker/integration-guide)** > > Learn how to create provider configurations for your workflows. # What is Flowker? Source: https://docs.lerian.studio/en/products/flowker/what-is-flowker Orchestrate financial workflows with Flowker. Coordinate anti-fraud checks, external approval systems, payment routing, and onboarding with step-level execution records. **Flowker** is Lerian's workflow orchestration platform for financial operations. It coordinates the steps around a transaction: anti-fraud checks, calls to external approval systems, payment routing, customer onboarding, and more. Instead of building custom logic for each approval flow or validation step, you define workflows once and Flowker executes them consistently, with step-level execution records and real-time visibility. ## Why use Flowker? *** Financial operations involve multiple steps, approvals, and external checks. Managing these manually leads to errors, delays, and compliance gaps. Flowker centralizes this orchestration so every step stays traceable and repeatable. For the full business case, see [Why Flowker?](/en/products/flowker/why-flowker). ## Core concepts *** Flowker works with workflows and nodes: * **Workflows**: The overall process, a graph of nodes and edges that defines what happens for a given operation. For example, "process a Pix payment" or "onboard a new customer". Workflows can branch, and retry policy can retry eligible executor-call failures. * **Nodes**: The individual elements in a workflow, such as a trigger, an external-service call, a condition, or an action. The workflow graph determines routing. Conditional nodes select an outgoing edge from the execution context, while other node types follow their outgoing edges after completing successfully. ## What makes Flowker different? *** ### Purpose-built for the financial domain Flowker is modeled for the transactional ecosystem. Its architecture reflects the operational and regulatory realities of banking, payments, Pix, and financial orchestration. ### Pre-ledger validation Use Flowker to orchestrate validation calls before a ledger-posting step. Your ledger integration must enforce that no other path can post without the required checks. ### Integrations with APIs and webhooks Flowker receives webhooks and calls external services through provider configurations. You define input and output expectations, then configure the provider connection and the nodes that call it. ### Step-level execution records Each executed executor, conditional, or action node records its status, duration, output, and error message. Trigger nodes start graph traversal and do not create execution-step records. This gives you a step-by-step view of what each execution did through the API. ### Enterprise-grade security Token-based authentication through Access Manager, with per-resource, per-action authorization. Lerian serves the API over TLS. ## Features by area *** ### Workflow orchestration * Run nodes according to the workflow graph and selected conditional branches. * Apply conditional logic based on HTTP status codes and response fields. * Persist workflow execution state for review. * Manage versions with full lifecycle control, including draft, activation, deactivation, and clone-based iteration. ### Integration and connectivity * Send HTTP requests (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). * Receive and process webhooks from external systems. * Connect to multiple financial service providers. ### Workflow configuration * Define workflows as graphs of nodes and edges. * Validate workflow definitions before activation. * Map and transform data between executor calls. ### Authentication and security * Token-based authentication through Access Manager, with per-resource, per-action authorization. * Structured logs. Lerian encrypts stored data and secrets at rest. ### Observability Lerian monitors Flowker's API and service operations. Use execution records to investigate workflow behavior. ## Common use cases *** ### Anti-fraud operations * Send transaction and account data to external fraud prevention services. * Collect and process responses from multiple providers, and apply configurable rules to decide whether to proceed. * Every step is recorded with its status and result. ### Manual approvals * Submit a request to, or query, an external approval system with executor nodes. * Route on the decision that the external system returns. * Flowker does not have a built-in pause. Model asynchronous human review with an external system and a later workflow trigger or execution. ### Payment orchestration * Coordinate multi-step payment flows that span multiple providers or rails. * Flowker can retry eligible executor-call failures. Reconciliation and posting controls belong to the systems you integrate. ### Customer onboarding * Automate KYC and AML processes, integrate with credit bureaus and validation services, and set up conditional onboarding rules based on risk profiles. ## API architecture *** Flowker’s API has three main categories: **Authoring**, **Runtime**, and **Administrative**. * **Authoring routes**: create, update, and manage workflow definitions and versions. * **Runtime routes**: start and monitor workflow executions. * **Administrative routes**: manage provider configurations and system health. ## Current status *** For additional information about specific integrations or implementation details, [contact us](https://lerian.studio/contact). > [!TIP] > > [Explore the Flowker API](/en/reference/products/flowker/create-workflow). Browse endpoints for workflows, executions, and provider configurations. # Why Flowker? Source: https://docs.lerian.studio/en/products/flowker/why-flowker How Flowker centralizes provider configuration, workflow routing, and execution records for financial operations. Financial institutions need to integrate providers, validate transactions, and preserve evidence for compliance. Flowker centralizes workflow routing, provider configuration, and execution records. The controls and outcomes still depend on the systems and integration boundaries you configure. ## The problem *** Financial services teams face these challenges: * **Variable provider-integration effort**: each provider has its own API contract, authentication, error handling, security review, and test scope * **Scattered validation evidence**: validation data is spread across multiple systems, making it difficult to reconstruct what happened during audits * **Compliance gaps**: when validation happens after the ledger write, invalid transactions create regulatory exposure * **Provider migration complexity**: changing provider, executor, or provider-configuration reference can require updates to draft workflow nodes * **Costly rollbacks**: invalid ledger writes require expensive manual remediation and damage data integrity ## How Flowker changes this *** Flowker provides one orchestration platform for workflow routing, provider configuration, mappings, and execution records. | | Before Flowker | With Flowker | | - | - | - | | **Provider integration** | Provider-specific integration work | Centralize provider configuration and node mappings | | **Step traceability** | Partial, scattered across systems | Every executed processing node recorded automatically | | **Provider flexibility** | Changes can require integration work | Update settings; update draft nodes when references change | | **Validation model** | Depends on the integration boundary | Orchestrate checks before a configured posting step; enforce at the ledger boundary | | **Failure handling** | Custom retry logic per integration | Built-in retries and circuit breakers | | **Workflow visibility** | Limited, requires log aggregation | Full execution tracking with step-level detail | ## For compliance teams *** Compliance teams need to prove that every transaction was validated correctly. Flowker gives compliance teams: * **Step-level traceability**: every execution records each executed processing node with its status, duration, output, and error message * **Queryable execution history**: filter executions by workflow and status, then read the step detail of any execution through the API * **Integration-owned posting control**: use Flowker to orchestrate checks before a ledger-posting step, then enforce them at the ledger boundary > [!NOTE] > > Flowker records execution steps automatically. You do not need to enable or configure anything. ## For engineering teams *** Engineering teams need to integrate providers quickly, handle failures gracefully, and stop rebuilding the same integration plumbing for every new service. Flowker gives engineering teams: * **Configure once, reuse everywhere**: set up a service instance's base URL and credentials once as a provider configuration, then reference it from any workflow node * **Built-in resilience**: circuit breakers prevent cascading failures when providers go down. Retries with exponential backoff handle transient errors. A `POST` or `PATCH` call runs once unless the node opts into retries * **Configuration and mappings**: centralize connection settings and node mappings in their workflow surfaces * **Configuration validation**: catalog configurations use the provider JSON Schema. `external_openapi` configurations use dedicated external OpenAPI and configuration validation * **Field mapping and data transformation**: map workflow fields to provider-specific formats without writing transformation code ## For product teams *** Product teams need to ship faster, respond to market changes, and measure what's working, without waiting months for engineering to build or modify validation flows. Flowker gives product teams: * **Faster time to market**: new validation workflows can be defined and deployed without waiting for custom integration work * **Flexibility without code changes**: add, remove, or reorder validation steps by updating workflow definitions, not by requesting engineering sprints * **Observable and measurable operations**: execution records and deployment telemetry provide evidence for workflow investigation ## What's next *** > **[Use cases](/en/products/flowker/use-cases)** > > See how Flowker applies to transaction validation, onboarding, payments, and more. > **[Getting started](/en/products/flowker/flowker-getting-started)** > > Set up Flowker and run your first workflow end to end. # Workflow Design Guide Source: https://docs.lerian.studio/en/products/flowker/workflow-design-guide Design Flowker workflows: node types, edges, real-world patterns, status transitions, and best practices for reliable orchestration. This guide walks through node types, edges, real-world patterns, status transitions, technical limits, and best practices. ## Node types *** Every workflow consists of nodes. Each node has a `type` that defines how Flowker processes it at runtime. ### trigger A trigger node is an execution entry point. Draft workflows can be incomplete, but activation requires at least one trigger node. The examples on this page show node topology only. A real trigger node also carries a `triggerType` and that trigger's configuration in its `data`. See [Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger) or [Running a workflow on a schedule](/en/products/flowker/running-a-workflow-on-a-schedule). ```json theme={null} { "id": "node-trigger", "type": "trigger", "name": "Payment Received" } ``` ### executor Calls an external service through a provider configuration. This node is the main integration point for fraud engines, payment providers, notification services, and other external systems. The examples on this page show node topology only. A real executor node also carries a `providerConfigId` in its `data`, plus an `executorId` naming the catalog executor it invokes. A node that calls an operation of an uploaded OpenAPI document omits the `executorId` and carries `operation_path` and `operation_method` instead. See the [Integration guide](/en/products/flowker/integration-guide). ```json theme={null} { "id": "node-fraud-check", "type": "executor", "name": "Check Fraud Score" } ``` ### conditional Evaluates a condition against the execution context and routes to different branches based on the result. Use conditional nodes to implement branching logic, for example, routing to an approval path when risk is high, or continuing directly when it is low. The condition lives in the node's `data.condition`. A free-text expression evaluates to a boolean and produces the output handle `true` or `false`. Each outgoing edge declares which handle it follows via `sourceHandle`. Conditional nodes built in the Console use a structured, case-based condition where each case routes to its own output handle. See the [Canvas editor](/en/products/flowker/console/canvas-editor). ```json theme={null} { "id": "node-risk-decision", "type": "conditional", "name": "Evaluate Risk Score", "data": { "condition": "node-fraud-check.body.riskScore < 70" } } ``` ### action Represents a synchronous internal `set_output` operation. It can write an interpolated output value and, optionally, override the synchronous HTTP response status. It does not provide a built-in pause, generic event emission, or a generic state-change operation. ```json theme={null} { "id": "node-record-approval", "type": "action", "name": "Record Approval Decision" } ``` ## Edges *** Edges connect nodes and define execution paths. Each edge includes the following fields: | Field | Description | | - | - | | `id` | Unique identifier for the edge. | | `source` | ID of the origin node. | | `target` | ID of the destination node. | | `sourceHandle` | Output handle of the source node this edge follows. Required to route out of a conditional node: it must match the branch outcome (`true` or `false` for a free-text condition). | | `condition` | Legacy free-text field kept for backward compatibility. Flowker does not evaluate it for routing. Leave it empty for new workflows. | | `label` | Human-readable label used for visualization and debugging. | ### Example edge ```json theme={null} { "id": "edge-approved", "source": "node-risk-decision", "target": "node-process-payment", "sourceHandle": "true", "label": "Approved" } ``` Routing depends on the source node's type. A conditional node evaluates its `data.condition` and follows the single outgoing edge whose `sourceHandle` matches the branch outcome. If no edge matches, that branch ends. Every other node type follows all of its outgoing edges when it completes successfully. ## Status transitions *** Workflows follow a well-defined lifecycle. ![Workflow status transition diagram showing three states: draft, active, and inactive. An arrow labeled 'activate' points from draft to active. An arrow labeled 'deactivate' points from active to inactive. An arrow labeled 'draft' points from inactive back to draft.](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/flowker-workflow-status-transitions.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=a046e93f9e4a2cbf44f5a32b69254b06) * **draft**: the initial state. You can add nodes, edit edges, and change configuration only in `draft` status. * **active**: a workflow you activated. Flowker can execute it. It accepts no modification while active. * **inactive**: a workflow you deactivated. Flowker can no longer execute it, but you can move it back to `draft` for editing. ### Rules * You can activate only a `draft` workflow (transition: `draft → active`). * You can deactivate only an `active` workflow (transition: `active → inactive`). * You can move only an `inactive` workflow back to draft (transition: `inactive → draft`). * Attempting an invalid transition returns error `FLK-0102`. * Attempting to modify a workflow that is not in `draft` returns error `FLK-0103`. ### Moving an inactive workflow back to draft If you deactivated a workflow and want to edit it again, move it back to `draft` by calling [`POST /v1/workflows/{id}/draft`](/en/reference/products/flowker/move-workflow-to-draft). This makes the workflow editable without needing to clone it. Use this when you deactivated a workflow by mistake, or when you want to iterate on an existing workflow instead of creating a copy. > [!NOTE] > > You can move only inactive workflows to draft. To modify an active workflow while it keeps running, use the clone approach described below. ### Iterating safely with clone To modify an active workflow, clone it first. Cloning creates a new `draft` from any status, copying all nodes and edges. You can then update it without impacting the current version. Use this approach for production versioning. ## Technical limits *** | Limit | Value | Error code | | - | - | - | | Maximum nodes per workflow | 100 | `FLK-0113` | | Maximum edges per workflow | 200 | `FLK-0114` | | Maximum execution input payload | 1 MB | `FLK-0506` | Workflows with more than \~50 nodes usually indicate that the flow should be split into smaller, composable workflows. ## Common patterns *** ### Sequential The simplest pattern. Nodes execute in a linear sequence. Use this when each step depends on the previous one and needs no branching. ![Sequential workflow pattern: a trigger node connects to a first executor node, which connects to a second executor node, which connects to a third executor node. All connections are single directed arrows forming a straight line.](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/flowker-pattern-sequential.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=340149b4d91ac1ceb12d735624e90861) **Example: Payment orchestration** ```json theme={null} { "nodes": [ { "id": "n1", "type": "trigger", "name": "Payment Initiated" }, { "id": "n2", "type": "executor", "name": "Validate Payment Data" }, { "id": "n3", "type": "executor", "name": "Route to Provider" }, { "id": "n4", "type": "executor", "name": "Send Confirmation Notification" } ], "edges": [ { "id": "e1", "source": "n1", "target": "n2", "label": "Start" }, { "id": "e2", "source": "n2", "target": "n3", "label": "Valid" }, { "id": "e3", "source": "n3", "target": "n4", "label": "Routed" } ] } ``` ### Conditional branching A `conditional` node evaluates its condition and routes execution accordingly. The branch outcome selects the outgoing edge the node follows, matched by `sourceHandle`. ![Conditional branching workflow pattern: a trigger node connects to an executor node, which connects to a conditional node. The conditional node has two outgoing arrows: one labeled 'Path A' pointing to a first executor node, and one labeled 'Path B' pointing to a second executor node.](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/flowker-pattern-conditional.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=bf118a79f905ff81a2994ea171eef3bf) **Example: Anti-fraud check** ```json theme={null} { "nodes": [ { "id": "n1", "type": "trigger", "name": "Transaction Received" }, { "id": "n2", "type": "executor", "name": "Get Fraud Score" }, { "id": "n3", "type": "conditional", "name": "Evaluate Score", "data": { "condition": "n2.body.fraudScore < 70" } }, { "id": "n4", "type": "executor", "name": "Approve Transaction" }, { "id": "n5", "type": "executor", "name": "Reject Transaction" } ], "edges": [ { "id": "e1", "source": "n1", "target": "n2", "label": "Start" }, { "id": "e2", "source": "n2", "target": "n3", "label": "Score received" }, { "id": "e3", "source": "n3", "target": "n4", "sourceHandle": "true", "label": "Approved" }, { "id": "e4", "source": "n3", "target": "n5", "sourceHandle": "false", "label": "Rejected" } ] } ``` ## Real-world examples *** ### Anti-fraud check A transaction arrives, an executor node gets the fraud score, and a conditional node routes execution to approval or rejection. ```json theme={null} { "nodes": [ { "id": "n1", "type": "trigger", "name": "Transaction Received" }, { "id": "n2", "type": "executor", "name": "Get Fraud Score" }, { "id": "n3", "type": "conditional", "name": "Evaluate Fraud Score", "data": { "condition": "n2.body.fraudScore < 70" } }, { "id": "n4", "type": "executor", "name": "Approve Transaction" }, { "id": "n5", "type": "executor", "name": "Reject and Notify" } ], "edges": [ { "id": "e1", "source": "n1", "target": "n2", "label": "Start" }, { "id": "e2", "source": "n2", "target": "n3", "label": "Score received" }, { "id": "e3", "source": "n3", "target": "n4", "sourceHandle": "true", "label": "Low risk" }, { "id": "e4", "source": "n3", "target": "n5", "sourceHandle": "false", "label": "High risk" } ] } ``` ### Payment orchestration A linear flow that validates incoming payment data, routes it to the appropriate provider, and sends a confirmation. ```json theme={null} { "nodes": [ { "id": "n1", "type": "trigger", "name": "Payment Initiated" }, { "id": "n2", "type": "executor", "name": "Validate Payment Data" }, { "id": "n3", "type": "executor", "name": "Route to Payment Provider" }, { "id": "n4", "type": "executor", "name": "Send Confirmation" } ], "edges": [ { "id": "e1", "source": "n1", "target": "n2", "label": "Start" }, { "id": "e2", "source": "n2", "target": "n3", "label": "Valid" }, { "id": "e3", "source": "n3", "target": "n4", "label": "Payment routed" } ] } ``` ### KYC onboarding Use a workflow to submit a document check to an external approval system. Flowker does not have a built-in pause: for asynchronous human review, start a later, separate workflow execution after your approval system publishes its decision. ### Manual approval flow An executor node submits the request for review. An executor retrieves the review decision from the external system. A conditional node then routes to either the approved or rejected path. Flowker executions run straight through. Flowker has no built-in pause step, so a human decision must come from an external system the workflow queries. ```json theme={null} { "nodes": [ { "id": "n1", "type": "trigger", "name": "Request Submitted" }, { "id": "n2", "type": "executor", "name": "Submit for Review" }, { "id": "n3", "type": "executor", "name": "Get Approval Decision" }, { "id": "n4", "type": "conditional", "name": "Decision Received", "data": { "condition": "n3.body.decision == 'approved'" } }, { "id": "n5", "type": "executor", "name": "Process Approved Request" }, { "id": "n6", "type": "executor", "name": "Notify Rejection" } ], "edges": [ { "id": "e1", "source": "n1", "target": "n2", "label": "Start" }, { "id": "e2", "source": "n2", "target": "n3", "label": "Submitted" }, { "id": "e3", "source": "n3", "target": "n4", "label": "Decision received" }, { "id": "e4", "source": "n4", "target": "n5", "sourceHandle": "true", "label": "Approved" }, { "id": "e5", "source": "n4", "target": "n6", "sourceHandle": "false", "label": "Rejected" } ] } ``` ## Best practices *** ### Node naming conventions Use descriptive, action-oriented names that communicate what the node does, not what type it is. * **correct**: `Validate Payment Data`, `Get Fraud Score`, `Notify Customer`, `Get Approval Decision` * **wrong**: `executor1`, `conditional node`, `node3` Good names make workflows readable without opening the node configuration. They also appear in execution records and traces. ### Condition expressions Flowker evaluates free-text conditions on conditional nodes against the execution context at runtime. A node without an output mapping stores its response envelope, so a condition reads a response field under `body`. Keep conditions simple and explicit: * Use direct field comparisons: `.body.status == 'approved'` * Use numeric comparisons: `.body.riskScore < 70` * Use boolean fields: `.body.reviewRequired == true` * Combine with `AND` / `OR` when needed: `.body.score < 70 AND .body.verified == true` Avoid complex expressions. They make the workflow hard to read and debug. If logic is non-trivial, give the `conditional` node a clear name that encapsulates the decision. A missing condition fails the execution, and so does a condition that fails to evaluate at runtime. Always test conditions before activating a workflow. ### Error handling strategies Design workflows to handle failure explicitly: * Add rejection paths from `conditional` nodes for every decision point that can fail. * Use separate `executor` nodes for retry logic or fallback providers. * Name error paths clearly (e.g., `Reject and Notify`, `Fallback to Manual Review`) so execution records are self-explanatory. ### Avoiding cycles Flowker uses a DFS-based cycle guard at runtime. When the cycle guard finds a cycle during execution, the workflow fails with `FLK-0508`. Cycles are not caught at design time, so validate your edge structure before activating. Rules to prevent cycles: * Edges must always point forward in the flow, never back to a previously executed node. * Review the graph visually before activating any workflow with branching or merging paths. * If you need a retry or a loop, model it as a separate workflow invocation, not a back-edge in the current graph. ### Versioning via clone Never edit an active workflow directly. Instead: **Step 1** Clone the workflow (creates a new `draft` with all nodes and edges copied). **Step 2** Make your changes in the draft. **Step 3** Validate or preview the draft. **Step 4** Activate the new version. A webhook method and path belong to one active workflow at a time: for a webhook-triggered workflow, deactivate the old version first, or give the new version its own path. **Step 5** Deactivate the old version if you no longer need it. This preserves the execution history of the active version and gives you a clean rollback path if the new version has issues. ## Error reference *** The following error codes are relevant to workflow design and execution: | Code | Description | | - | - | | `FLK-0102` | Invalid status transition | | `FLK-0103` | Workflow cannot be modified (not in draft status) | | `FLK-0113` | Too many nodes (maximum is 100) | | `FLK-0114` | Too many edges (maximum is 200) | | `FLK-0506` | Execution input payload too large (maximum is 1 MB) | | `FLK-0508` | Cycle detected during workflow execution | # Working with request and response data Source: https://docs.lerian.studio/en/products/flowker/working-with-request-and-response-data Move values into an executor node's request and out of its response. Declare the mappings, reshape values in flight, and check the assembled request before you call the service. An executor node sends data to an external service and receives data back. The service names its own fields, and your workflow names its own. Mappings are how you move values between the two. They are a list of source-to-target entries on the node. Flowker applies them to the request before the call and to the response after it. You need this whenever the shape your workflow carries is not the shape the service accepts. One example is a document that must travel without punctuation. Another is an amount that belongs under a nested object, or a score that a later node reads under a short name. ## Before you start *** * A provider configuration for the service, and an executor node that references it. See [Reference the provider configuration from a workflow node](/en/products/flowker/integration-guide#step-3-reference-the-provider-configuration-from-a-workflow-node). * The field names the service expects. When the service's OpenAPI document is in the registry, [Derive an operation schema](/en/reference/products/flowker/derive-openapi-operation-schema) returns `inputSchema` (the operation's request body) and `outputSchema` (its success response). Both give you the field names you write as mapping targets and sources. * A workflow in `draft` status. To edit an active workflow, [deactivate it](/en/reference/products/flowker/deactivate-workflow) and [move it to draft](/en/reference/products/flowker/move-workflow-to-draft), then activate it again. > [!NOTE] > > Do not send `executorId` on a node that calls an operation of an uploaded OpenAPI document. Such a node names the operation with `operation_path` and `operation_method`. Flowker fills the `executorId` in for you, from the provider configuration the node points at, before it validates the workflow. It does this when you create the workflow and when you update it. [Connecting your own API](/en/products/flowker/connecting-your-own-api) walks that whole path. The nodes on this page name `http`, the generic HTTP connector, which does need an explicit `executorId`. ## Step 1: Know what a mapping can read *** Every mapping reads from the workflow context, one JSON object that grows as the execution advances: | Path | What it holds | | - | - | | `workflow` | The trigger payload: the `inputData` of an execution request, or the body a webhook route received. A webhook route also adds `workflow._webhook` with the call's metadata. See [Webhook metadata](/en/products/flowker/integration-guide#webhook-metadata). | | `execution.id` | The execution identifier. | | `execution.startedAt` | When the execution started, in UTC. | | `` | The output of each executor node that has completed, under that node's id. | Address a value by its path from one of those top-level keys: | Source | What it selects | | - | - | | `workflow.customer.document` | A nested field of the trigger payload. | | `workflow.items[0].sku` | One element of an array. | | `workflow.items[*].sku` | The same field across every element, as an array. | | `workflow` | The whole trigger payload as an object. | | `score-transaction.body.score` | A field of the `score-transaction` node's output. | > [!NOTE] > > A mapping `source` is a plain path. Do not wrap it in `${...}`. Inside a mapping Flowker reads a `${...}` string as a literal path name that selects nothing. ## Step 2: Declare the input mapping *** Input mappings live in an `inputMapping` array inside the executor node's `data` object. Each entry moves one value into the outgoing request body. | Field | Type | Required | Description | | - | - | - | - | | `source` | string | Yes | The path in the workflow context to read. | | `target` | string | Yes | The path in the outgoing request body to write. A dotted target creates the nested object (`payment.amount` sends `{"payment":{"amount":…}}`). | | `transformation` | object | No | A change applied to the value after it lands on the target. See [Step 4](#step-4-reshape-a-value-on-its-way-through). | | `required` | boolean | No | Whether the source path must exist. Defaults to `false`. Applies to the whole node. See below. | The mapped result is the request body. Write each `target` exactly as the service expects to receive it: there is no wrapper object and no prefix to add. > **Example: map the trigger payload into a fraud check** > > ```json theme={null} > { > "id": "score-transaction", > "type": "executor", > "name": "Score transaction", > "position": { "x": 200, "y": 0 }, > "data": { > "executorId": "http", > "providerConfigId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", > "method": "POST", > "path": "/score-transaction", > "inputMapping": [ > { "source": "workflow.transactionId", "target": "reference" }, > { "source": "workflow.amount", "target": "payment.amount" }, > { "source": "workflow.customer.document", "target": "payment.document" } > ] > } > } > ``` > > With a trigger payload of `{"transactionId":"txn-98765","amount":1500.00,"customer":{"document":"12345678900"}}`, the service receives: > > ```json theme={null} > { > "reference": "txn-98765", > "payment": { "amount": 1500, "document": "12345678900" } > } > ``` ### When a source selects nothing A source path that is absent from the context is not an error. Flowker still writes the target, with the value `null`, and the request goes out. Set `required: true` when the node must not call the service without a value. Flowker then checks every source path on that node before it builds the request, and fails the step when any of them is absent. The execution fails and the step reports `input transformation failed`. > [!NOTE] > > `required` applies to the node, not to the one entry that carries it. If any entry in a node's `inputMapping` sets `required: true`, every source path in that array must resolve. To keep some fields optional, leave `required` off across the whole node. Give each `target` one entry. When two entries write the same target, the last one wins. ## Step 3: Decide what builds the request body *** A node has three sources for a request body. Only `data.body` is exclusive: when present, it is the whole body. Without it, Flowker composes the body from the other two sources, with the mapping overlay written over the `config` literals: **1. data.body** An explicit body template wins outright. Flowker resolves its `${...}` references against the workflow context and sends the result. While `data.body` is present, `inputMapping`, `transforms`, and `config` contribute nothing to the body. Every `${...}` reference here must resolve. One that does not fails the node with `FLK-0143`, before any call goes out (the opposite of a mapping source, which resolves to `null`). Use `data.body` when a missing value must stop the workflow, and a mapping when the request must go out regardless. **2. inputMapping or transforms** Otherwise Flowker builds an overlay from `inputMapping`. When `inputMapping` is empty it builds the overlay from `transforms` instead. The two are mutually exclusive: a node with at least one `inputMapping` entry never runs its `transforms` on the input. **3. config literals** Literal values in `data.config` seed the body. With an overlay present, the literals are the base and the overlay wins on any key both set. One node can therefore combine fixed values with mapped ones. With no overlay, the literals are the body on their own. Transport never becomes body content. Before `config` can seed the body, Flowker removes these names from it: `method`, `path`, `url`, `endpointName`, `query`, `headers`, `auth`, `retry`, `timeout`, `timeout_seconds`, `request_format`, `success_status_codes`, `content_digest`, `allowedHosts`, and `allowedPrivateHosts`. A node that keeps transport in `config` by mistake therefore sends none of it to the destination. An `auth` block placed there can never ship as request content. The node reads its own transport from the top of its `data` object: `path`, `endpointName`, `method`, `headers`, `query`, `auth`, `timeout_seconds`, `retry`, `success_status_codes`, `request_format`, and `content_digest`. The outbound host allow-lists are not among them: you set `allowedHosts` and `allowedPrivateHosts` on the provider configuration, where each applies to every node that calls through it. > **Example: fixed values plus mapped ones** > > ```json theme={null} > { > "data": { > "executorId": "http", > "providerConfigId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", > "method": "POST", > "path": "/score-transaction", > "config": { > "channel": "web", > "payment": { "currency": "BRL" } > }, > "inputMapping": [ > { "source": "workflow.transactionId", "target": "reference" }, > { "source": "workflow.amount", "target": "payment.amount" } > ] > } > } > ``` > > The literals and the mapped values merge, and nesting merges with nesting: > > ```json theme={null} > { > "channel": "web", > "reference": "txn-98765", > "payment": { "currency": "BRL", "amount": 1500 } > } > ``` ## Step 4: Reshape a value on its way through *** When the service needs a value in a different form, attach a `transformation` to the mapping entry. It applies to the value after it lands on the target. | Type | What it does | Config | | - | - | - | | `remove_characters` | Removes the given characters from the text. | `characters`: the characters to remove. | | `add_prefix` | Puts text in front of the value. | `prefix`: the text to add. | | `add_suffix` | Puts text after the value. | `suffix`: the text to add. | | `to_uppercase` | Converts the text to uppercase. | — | | `to_lowercase` | Converts the text to lowercase. | — | Two rules to write them by: * They act on text. A value that is not text reaches the target unchanged. * The `prefix` and `suffix` values each need at least one character, and a single space counts. The `characters` value needs at least one character that is not a space, a tab, or a line break. A value that does not meet this fails the step at run time. > **Example: normalize a document and stamp a reference** > > ```json theme={null} > { > "inputMapping": [ > { > "source": "workflow.customer.document", > "target": "payer.document", > "transformation": { > "type": "remove_characters", > "config": { "characters": ".-/" } > } > }, > { > "source": "workflow.customer.name", > "target": "payer.name", > "transformation": { "type": "to_uppercase" } > }, > { > "source": "workflow.transactionId", > "target": "payer.reference", > "transformation": { > "type": "add_prefix", > "config": { "prefix": "BR-" } > } > } > ] > } > ``` > > From `{"customer":{"document":"123.456.789-00","name":"ada lovelace"},"transactionId":"txn-98765"}`, the node sends: > > ```json theme={null} > { > "payer": { > "document": "12345678900", > "name": "ADA LOVELACE", > "reference": "BR-txn-98765" > } > } > ``` ### Whole-document transforms For work that entry-by-entry mapping does not express (combining two fields, choosing the first value that is present, filling a default), declare a `transforms` array instead. Each operation reads the whole workflow context and writes the whole overlay. Flowker accepts `shift` (move or rename), `concat` (join values), `coalesce` (first value present), `default` (fill a missing key), `extract` (lift a subtree to the root), `delete` (drop a key), `timestamp`, `uuid`, and `pass`. The five transformation types above are available here too. As operations they take the target path in the spec as `path`. > **Example: shift and a default in one node** > > ```json theme={null} > { > "data": { > "executorId": "http", > "providerConfigId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a", > "transforms": [ > { > "operation": "shift", > "spec": { > "reference": "workflow.transactionId", > "payment.amount": "workflow.amount" > } > }, > { "operation": "default", "spec": { "channel": "web" } } > ] > } > } > ``` > > The node sends: > > ```json theme={null} > { > "reference": "txn-98765", > "payment": { "amount": 1500 }, > "channel": "web" > } > ``` > > `shift`, `concat`, `coalesce`, `extract`, `delete`, and `timestamp` accept `require: true`, which demands that every path the `spec` names exists, the same way `required` works on a mapping entry. > [!NOTE] > > `transforms` runs only when the node has no `inputMapping`. Use one or the other in a given node, never both. ## Step 5: Read the response back out *** Output mappings extract fields from the response and store them in the workflow context under the node's id, so later nodes read short, stable names. Declare them in an `outputMapping` array in the node's `data`, with the same four entry fields as an input mapping. An output `source` is a path into the response envelope, not into the response body: | Path | What it holds | | - | - | | `status` | The HTTP status code. | | `status_text` | The HTTP status line, such as `200 OK`. | | `url` | The URL Flowker requested, with the query string it assembled. | | `headers` | The response headers, as a key-value object under their canonical names, such as `Content-Type`. A header the service sent more than once arrives as one comma-separated value. | | `body` | The response body, parsed when the response is `application/json`, a type whose name ends in `+json`, or an XML type (`application/xml`, `text/xml`, or a type whose name ends in `+xml`). | | `raw_body` | The response exactly as it arrived, as text. Present for an XML response. | | `body_format` | Set to `xml` when an XML body was decoded. | | `body_parse_error` | Present in place of `body` when an XML body could not be decoded, so the workflow can branch on it. | Response fields therefore sit under `body`: ```json theme={null} { "outputMapping": [ { "source": "body.score", "target": "score" }, { "source": "body.decision", "target": "decision" }, { "source": "status", "target": "httpStatus" } ] } ``` On a node with the id `score-transaction`, that stores: ```json theme={null} { "score": 42, "decision": "review", "httpStatus": 200 } ``` A downstream mapping then reads `score-transaction.score` and `score-transaction.httpStatus`, and a downstream `data.body` reads `${score-transaction.score}`. A node that declares no `outputMapping` stores the whole envelope under its id instead, and downstream nodes read the envelope path directly: `score-transaction.body.score`. Add an output mapping when you want the shorter name. Skip it when the envelope path is clear enough. An output source that selects nothing behaves like an input one. Flowker stores the target as `null` unless an entry on that node sets `required: true`. ## Step 6: Check the mapping before you call the service *** [Preview an executor request](/en/reference/products/flowker/preview-executor-request) uses the mapping, transformation, and request-assembly paths without making a service-network call. It does not read the vault or fetch an authentication token. The preview masks with `***` the credential the authentication type declares where it appears as a value. Any other value, including a free-form authentication parameter, can appear as Flowker would send it. Send the node, the provider configuration it targets, and a sample payload. The provider configuration goes in the request itself, so preview does not need a saved provider configuration: ```json theme={null} POST /v1/workflows/preview-request { "node": { "executorId": "http", "method": "POST", "path": "/score-transaction", "inputMapping": [ { "source": "workflow.customer.document", "target": "payer.document", "transformation": { "type": "remove_characters", "config": { "characters": ".-/" } } }, { "source": "workflow.customer.name", "target": "payer.name", "transformation": { "type": "to_uppercase" } }, { "source": "workflow.transactionId", "target": "payer.reference", "transformation": { "type": "add_prefix", "config": { "prefix": "BR-" } } } ] }, "providerConfig": { "providerId": "http", "config": { "base_url": "https://api.fraudshield.example.com" }, "allowedHosts": ["api.fraudshield.example.com"] }, "sampleInput": { "transactionId": "txn-98765", "customer": { "document": "123.456.789-00", "name": "ada lovelace" } } } ``` Your `sampleInput` becomes the trigger payload, so the mapping sources read it as `workflow.*`, exactly as they will at run time. The response is the assembled request: ```json theme={null} { "method": "POST", "url": "https://api.fraudshield.example.com/score-transaction", "headers": { "Content-Type": "application/json" }, "body": "{\"payer\":{\"document\":\"12345678900\",\"name\":\"ADA LOVELACE\",\"reference\":\"BR-txn-98765\"}}", "curl": "curl -X POST -H 'Content-Type: application/json' --data '{\"payer\":{\"document\":\"12345678900\",\"name\":\"ADA LOVELACE\",\"reference\":\"BR-txn-98765\"}}' 'https://api.fraudshield.example.com/score-transaction'", "unresolved": [] } ``` Each transformation resolved: the punctuation is gone from the document, the name is uppercase, and the reference carries its prefix. No call reached the service. Read it in this order: **1. Check the URL** It is the provider configuration's base URL plus the node's `path`. A path you did not expect is a node field to fix, not a mapping. **2. Check the body against the field names the service expects** Every `target` should appear where the service wants it. A field carrying `null` is a `source` path that selects nothing. **3. Check `unresolved`** It lists the `${...}` references in the node's headers, query, and URL that the preview could not resolve. Flowker leaves them literal in the rendered request. An unresolved reference in `data.body` fails the preview with `FLK-0143` instead. To check a node's fixed fields against the catalog executor's schema as well, call [Validate a node configuration](/en/reference/products/flowker/validate-executor-config). Send the node's configuration in `config` and, in `mappedTargets`, the target paths your `inputMapping` supplies. Flowker counts those as satisfied, so a node that maps a required field from the trigger passes the check. ## What goes wrong *** | Symptom | Cause | Fix | | - | - | - | | The service receives a field set to `null`. | The `source` path is absent from the workflow context. | Compare the path with a preview's rendered body. Set `required: true` on the node when it must not run without the value. | | The service receives a nested object it did not ask for. | The `target` carries a prefix. | Write the target exactly as the service expects it. A target of `executor.accountId` sends an `executor` object. | | The request body is empty. | The node has no `data.body`, no mapping and no `transforms`, and its `config` holds only transport names. | Add the values as `config` literals or as mapping entries. | | The mappings appear to be ignored. | The node also carries `data.body`, which is the only body source while it is present. | Remove `data.body` to let the mappings build the body. | | `transforms` appears to be ignored. | The node has at least one `inputMapping` entry. | Remove the `inputMapping` entries, or move the logic into them. | | A downstream node reads nothing. | The reference does not name the producing node. | Read `.` in a mapping, or `${.}` in `data.body`. There is no shared top-level namespace. | | An output mapping stores `null`. | The `source` omits the envelope prefix. | Map `body.score`, not `score`. | | Error code | When | What it means | | - | - | - | | `FLK-0140` | Create, update, or activate | The node's `inputMapping` does not form a valid specification (most often, an unknown `transformation.type`). | | `FLK-0141` | Create, update, or activate | The same, for the node's `outputMapping`. | | `FLK-0142` | Create, update, or activate | The same, for the node's `transforms`. | | `FLK-0143` | Run time | A `${...}` reference in the node's `data.body` does not resolve against the workflow context. The node fails without calling the service. | See the [Flowker error list](/en/reference/products/flowker/flowker-error-list) for every code. ## What's next *** > **[Integration guide](/en/products/flowker/integration-guide)** > > Create the provider configuration a node calls through, and set its authentication. > **[Configuring a webhook trigger](/en/products/flowker/configuring-a-webhook-trigger)** > > Choose the payload contract that fills the `workflow` namespace your mappings read. # Accounting and accrual runs Source: https://docs.lerian.studio/en/products/lender/accounting-and-accrual-runs How Lender books to the ledger: accounting profiles and posting rules, durable posting intents, accrual runs, and journal references. Money events, such as a disbursement, a repayment, or an interest accrual, reach the ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.). Lender's accounting layer is how they get there. A product declares its posting rules once. From then on, each of those events books automatically and traceably. ## Accounting profiles and posting rules *** An **accounting profile** binds a product version to the ledger. It defines the **posting rules**: the double-entry (Every financial movement is recorded as at least two operations: a debit from one account and a credit to another, so the system always balances.) legs that fire for each kind of financial event. It also defines the ledger organization and ledger the resulting transactions book into. You create a profile per product: `POST /api/v1/loan-products/{id}/accounting-profiles` Lender validates the posting rules when you create the profile. A product therefore cannot go live with legs that would not balance. ## Posting intents and the relay *** Lender does not call the ledger inline. When a financial event happens, Lender persists a durable **posting intent**. That write happens in the same database transaction that changes domain state. A relay then posts the balanced transaction to Midaz asynchronously. These properties make bookings reliable: * **The intent commits with the state change**: both land in one database transaction, through a transactional outbox. * **Idempotent**: each posting carries a deterministic key, so retries collapse to one ledger transaction. * **Fails closed**: if routing cannot resolve a non-empty ledger target, Lender refuses the post and does not write it to the wrong place. [Lender in the platform](/en/products/lender/lender-in-the-platform) describes the full path. ## Accrual runs *** An **accrual run** recognizes interest and the other time-based amounts. One run closes the last civil month that had fully ended before its business date, for every loan it selects. It also produces the postings for that month. `POST /api/v1/accrual-runs` starts a run. Lender selects the candidate loan accounts itself, recognizes interest per account, and writes one posting intent per recognition. > [!TIP] > > Recognition is idempotent per loan account, per accounting month, and per amount kind. A run started twice for the same month recognizes interest once. ## Retrying a run *** A recognition is durable the moment the run commits. The ledger booking is not. When a posting never arrives, retry the run instead of starting a new one. `POST /api/v1/accrual-runs/{id}/retry` names the items to re-send, and takes the same `X-Idempotency` header the run itself takes. It is a second attempt on the same run, so Lender records no second run. Lender rebuilds the posting behind every named item the ledger has not acknowledged, and enqueues it under the deterministic key the original run used. The relay delivers it, and the ledger treats a duplicate as a duplicate rather than as a second booking. Lender leaves alone the items the ledger already acknowledged. The answer names, item by item, what it could not re-send. It also names why: the posting had already been delivered, it was quarantined, the contract behind it is gone, or the product is misconfigured. > [!WARNING] > > A retry refuses with `LENDER-0405` when the posting it rebuilds differs from the one already enqueued under that key. The product's accounting profile changed after the run, so re-sending would book the month against legs nobody chose for it. Reconcile the enqueued posting first, because repeating the request reproduces the same refusal. ## Journal references *** Each accrual run records a **journal reference**, the run's own accounting identifier. The reference record also stores the correlation id that Lender derived for the run. The journal reference id identifies one run exactly. A read by correlation id returns the most recent run that shares that mode, business date, and product scope. A run emits one posting per loan account, per accounting month, per amount kind. Lender therefore records the tie back to the ledger per posting, not per run. Once the ledger acknowledges a posting, Lender stores a receipt under that posting's own key. A read by that key answers with the ledger transaction that booked it. A run-level reference carries no ledger transaction of its own: it names the run, and the receipts name the bookings. | Operation | Purpose | | - | - | | `GET /api/v1/journal-references` | Look up a journal reference by correlation id. | | `GET /api/v1/journal-references/{id}` | Read a single journal reference. | ## Next steps *** > **[Jurisdictions](/en/products/lender/jurisdictions)** > > See how the active jurisdiction shapes disclosures and endpoints. > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > Layer CET, IOF, and PDD staging on top of the accounting model. # Assign receivables Source: https://docs.lerian.studio/en/products/lender/assignment-of-receivables Manage a receivables-assignment journey: register a fund, assess a pool, approve a set, exchange files, and record the external facts that complete the cession. An **assignment of receivables** (*cessão*) transfers a selected set of receivables to a fund. Lender keeps the set, its eligibility decision, its documents, and the externally observed lifecycle facts together. An operator can therefore work the journey without treating a file exchange as proof of a completed transfer. This guide describes the operational flow. The [Lender REST API](/en/products/lender/lender-rest-api) and its generated reference define the request and response contracts. ## Before you start *** * Use a Lender identity authorized for the assignment operations. The service derives the tenant from that validated identity. You do not supply a tenant in a request. * Register the receiving fund before creating a set. The registration holds its administrator details, cession rate, eligibility limits, document mode, and the layout used for its offer file. * Treat each external fact as evidence. Sending a file, receiving a remessa, signing a term, paying, transferring, and accepting are separate facts with separate controls. ## The assignment flow *** **1. Register the fund** Create or update the assignment-fund registration. Its configuration supplies the cession rate and the concentration and repeated-cut rules used when Lender evaluates a pool. The receiving fund's document and registry settings also determine the later document and acceptance paths. **2. Review eligible receivables** Read the fund-scoped candidate pool. Lender evaluates each candidate against the fund configuration and the assignment context, returning an eligibility verdict rather than silently dropping ineligible receivables. When it cannot read a required source, it reports that condition instead of treating the constraint as satisfied. **3. Assemble and score a draft set** Create a draft from the receivables you selected. Lender freezes the set's composition and the fund settings it needs for that journey. Score the draft against the fund rule before approval. An unapproved set can be re-scored. **4. Approve the set** Approval re-reads the relevant receivable state before it records the decision. A receivable that changed since assembly can leave the set, or Lender can reprice it. Review the approval result, and do not assume the draft survives unchanged. **5. Call off a set that cannot continue** `POST /api/v1/assignment/sets/{setId}/discard` cancels an unsold composition and releases its receivables back to the assignable pool. It preserves the set, its evidence, and its audit trail. It does not undo a sale. Lender refuses the call when the set is accepted, partly sold or paid, or has a transfer lodged at the registry. **6. Exchange the fund files** Download the offer file only after approval, then record that you sent it. Ingest the fund administrator's remessa against that same set. Lender reconciles the file with the offered composition and records the outcome. **7. Record the completion facts** Record the endorsement term and the externally observed payment, transfer, and acceptance facts as they occur. Lender enforces the lifecycle transition for each fact. Lender removes an accepted receivable from the active holding path only when the required acceptance processing succeeds. ## Documents and exceptions *** Lender can produce a receivable dossier from the facts it holds. The first materialization freezes the evidence snapshot used for that dossier, so a later retrieval represents the same recorded evidence. The journey can also surface exception cases. Those cases include a repeated cut, a remessa conflict or divergence, a receivable that cannot be valued, or a derecognition that cannot be posted. Resolve those cases through the assignment exception flow. Do not substitute a lifecycle fact for the missing resolution. ## Operational guardrails *** * **A set is not a completed cession.** Draft, eligibility, approval, offer delivery, remessa, term, payment, transfer, and acceptance are distinct lifecycle steps. * **Fund changes do not rewrite a set already in progress.** Lender freezes the relevant fund configuration at set assembly. * **Approval is intentionally defensive.** It validates the current receivable state before a fund receives an offer. * **Acceptance is money-path sensitive.** If Lender cannot complete its required processing, it refuses the acceptance instead of marking the receivable as sold. ## Next steps *** > **[Lender REST API](/en/products/lender/lender-rest-api)** > > Start from the service API and the generated operation reference. > **[Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs)** > > See how Lender keeps durable accounting work separate from inline requests. # Brazil regulatory pack Source: https://docs.lerian.studio/en/products/lender/brazil-regulatory-pack Explore Lender's Brazilian profile: CET disclosure, IOF tax preview, capitalization consent, PDD staging, prepayment quotes, and business calendar rules. The Brazilian **profile** carries the regulated behavior of the loans Lender books in Brazil. You reach everything here through the BR-prefixed endpoints (`/api/v1/br/...`). Everything here applies to loans originated under the Brazil jurisdiction. Portuguese domain terms stay as-is, with a gloss (see [Core concepts](/en/products/lender/core-concepts) for the full list). ## CET (Custo Efetivo Total) *** **CET** (*Custo Efetivo Total*, total effective cost of credit) is the all-in cost disclosed to the borrower (interest plus taxes and fees). Lender persists the CET disclosure at origination and exposes it per loan account: `GET /api/v1/br/loan-accounts/{id}/cet-disclosure` ## IOF and tax preview *** **IOF** (*Imposto sobre Operações Financeiras*) is the federal tax on credit operations. The Brazilian profile pins the IOF rates, so a preview and the disbursement that follows it compute the same amounts. Preview the daily and the additional IOF component, and the net amount the borrower receives, before you originate: `POST /api/v1/br/tax/preview` At disbursement, send `jurisdictionExtensions` with `disbursedAt`, `loanProductVersionId`, and `requestId`, the call's `X-Idempotency` value. Lender computes the IOF from them and accepts the disbursement only when the net equals gross minus that IOF, minus a retained origination fee. ## Capitalization consent *** Brazilian compound-interest (*juros compostos*) capitalization requires the borrower's explicit consent. Lender records that consent as an integrity-checked ceremony. The client submits the clause text and its hash. The server canonicalizes the text, recomputes the hash, and rejects any mismatch. The recorded consent is therefore provably the clause the borrower saw. `POST /api/v1/br/loan-applications/{id}/capitalization-consents` ## PDD staging *** **PDD** (*Provisão para Devedores Duvidosos*) is loan-loss provisioning. Each loan account sits in a PDD stage. Lender records each stage change as a transition, not as a silent overwrite. | Operation | Purpose | | - | - | | `GET /api/v1/br/loan-accounts/{id}/pdd-stage` | Read the current PDD stage. | | `POST /api/v1/br/loan-accounts/{id}/pdd-stage/transitions` | Apply a PDD stage transition. | The number of days overdue drives staging. The later stages also suspend interest accrual, so the [accrual run](/en/products/lender/accounting-and-accrual-runs) skips those accounts. ### The nightly promotion sweep Stages also advance on their own. A daily sweep reads the overdue book for the business day, measures each account against the Res. CMN 4.966 provisioning ladder in force, and applies the same transition operation above wherever the stage should change. A loan that ages past a boundary is therefore restaged without anyone calling the API. The sweep is on by default, and it runs at 05:00 UTC until you give it another schedule. Every deployment runs it against its own book. The sweep reads the portfolio snapshot for the business day, so it builds that snapshot itself when nothing else did, and fails that tenant's sweep when it cannot. A book nobody measured and a book with nothing overdue look identical from the outside, and the sweep refuses to report the first as the second. Set `JOBS_PDD_PROMOTION_ENABLED=false` to turn it off. Stages then change only when you call the transition operation. On a default deployment this sweep is also the only producer of the portfolio snapshot, because the dedicated snapshot job ships off. Turn the sweep off and you have to enable `JOBS_PORTFOLIO_SNAPSHOT_ENABLED` in its place, or the portfolio dashboard and the delinquent-loan register refuse every business date with `503`. ### Qualitative problem assets Res. CMN 4.966 art. 3 paragraphs 1 and 2 also make an asset a problem asset for a qualitative reason, with nothing overdue. Record that judgment as a declaration on the loan account. | Operation | Purpose | | - | - | | `POST /api/v1/br/loan-accounts/{id}/problem-asset-assessments` | Declare that the contract is, or is not, a problem asset under one article. Requires `X-Idempotency`. | | `GET /api/v1/br/loan-accounts/{id}/problem-asset-assessments` | List the declarations, newest first. | | `PUT /api/v1/br/loan-product-versions/{id}/problem-asset-reassessment-horizon` | Set how many days a characterisation can stand on a contract of this product version before somebody must re-assess it. | | `GET /api/v1/br/problem-asset-reassessment-overdue` | List the contracts whose characterisation has outlived that horizon. | A declaration needs `trigger` (the article, for example `art_3_p2_iii`), `source`, `evidenceReference`, `evaluatedOn`, and `characterizesProblemAsset`. Declarations are immutable. To withdraw a characterisation, declare the same `trigger` again with `characterizesProblemAsset: false` and its own evidence. The nightly promotion sweep reads the live declarations. A characterised contract takes the more severe of its arrears stage and the problem-asset floor of the ladder in force. It can therefore enter a PDD stage with nothing overdue. Passing the re-assessment horizon changes no stage and no provision. The contract stays on the re-assessment worklist until somebody declares again, with either verdict. ## Credit operation descriptor *** The **credit descriptor** is the SCR-style credit-operation profile for a loan account, the regulated description of the operation. `GET /api/v1/br/loan-accounts/{id}/credit-descriptor` ## Prepayment quote *** A Brazilian loan account settles early against a **prepayment quote**: create the quote first, then prepay with its identifier. The quote is valid for 24 hours and accepts one settlement, and the amount you settle must equal the quote's net settlement amount. The BR prepayment route mirrors the generic one and takes the same `X-Idempotency` key. | Operation | Purpose | | - | - | | `POST /api/v1/br/loan-accounts/{id}/prepayment-quote` | Create a prepayment quote (partial or full). | | `POST /api/v1/br/loan-accounts/{id}/prepayments` | Prepay the loan account (BR alias). | ## CDC caps *** Brazilian consumer-credit (CDC) products can carry regulatory caps, for example a late-fee ceiling. Lender enforces caps by rejecting a charge that would violate them, rather than silently clamping the amount. The contract and its audit trail then stay explicit about each refusal. ## Business calendar *** Brazilian date math (accruals, schedule dates, settlement) uses a Brazilian business calendar. That calendar has ANBIMA holidays and an Actual/252 day-count convention. Installment and accrual dates therefore land on business days. ## BR products *** You create and version Brazilian products through jurisdiction-aware aliases that accept the BR extensions: * `POST /api/v1/br/loan-products`, `GET /api/v1/br/loan-products`, `GET /api/v1/br/loan-products/{id}` * `POST /api/v1/br/loan-products/{id}/versions`, `POST /api/v1/br/loan-products/{id}/activate`, `POST /api/v1/br/loan-products/{id}/promote` ## Next steps *** > **[Consignado privado](/en/products/lender/consignado-privado)** > > The Brazilian journey: payroll-deducted lending end to end. # Configuration and deploy Source: https://docs.lerian.studio/en/products/lender/configuration-and-deploy How Lender is packaged and run: the container shape, its dependencies, a curated environment-variable surface, and the ledger posting path. Lender is a single Go service, packaged as a container and configured entirely through environment variables. This page is the operator's reference: what it runs as, what it depends on, the settings that matter, and how tenancy and ledger routing behave. ## The container *** Lender builds from a multi-stage Dockerfile (a Go Alpine builder producing a static binary, copied into a distroless nonroot runtime) and listens on port `8080`. It ships as a container image you deploy into your Kubernetes (A platform for running and managing software applications at scale. It automatically handles deployment, scaling, and recovery across multiple servers.) cluster with the platform's standard [lifecycle tooling](/en/platform/lifecycle-management). This page does not assume any particular packaging beyond the image. Operational probes: | Endpoint | Purpose | | - | - | | `GET /health` | Liveness. | | `GET /readyz` | Dependency readiness. | | `GET /version` | Build and version metadata. | ## Dependencies *** | Dependency | Role | | - | - | | **PostgreSQL 17** | Primary data store, with primary/replica support. | | **Valkey 8** (Redis-compatible) | Cache, rate limiting, and the M2M-credential cache. | | **RabbitMQ** | Optional, and off by default. It is not the transport for Lender's event catalog. | | **RedPanda** | Kafka-compatible event backbone for lifecycle facts, published through Lerian's streaming library and transactional outbox. Required: the service does not start without it. | | **Midaz** | The double-entry ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.), reached through the official SDK. Lender authenticates via [Access Manager](/en/platform/access-manager) with machine-to-machine (M2M) credentials. You keep them in your secret vault (see Ledger M2M custody below). | | **Caradhras** | Identity backend behind Access Manager. Route authorization runs through Lerian's auth library. | | **Systemplane** | Runtime configuration plane, mounted under `/api/v1/systemplane`. | ## Configuration surface *** Configuration is entirely environment-driven. The keys below shape behavior. They are not the full list. | Group | Key | What it controls | | - | - | - | | App | `ENV_NAME`, `LOG_LEVEL`, `DEPLOYMENT_MODE`, `SERVER_ADDRESS` | Environment name, log verbosity, deployment mode, and listen address. | | Tenancy | `DEFAULT_TENANT_ID` | The tenant the deployment serves. | | Postgres | `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_NAME`, `POSTGRES_SSLMODE` | Primary database connection. Migration directories are fixed in the service image, not configurable. | | Cache | `REDIS_HOST`, `REDIS_PASSWORD`, `M2M_CREDENTIAL_CACHE_TTL_SEC` | Valkey connection and how long M2M credentials are cached. | | Messaging | `RABBITMQ_ENABLED`, `RABBITMQ_HOST`, `OUTBOX_DISPATCH_INTERVAL_SEC` | Separate RabbitMQ messaging path and dispatcher cadence. These variables do not configure lifecycle event topics. The PostgreSQL outbox is always initialized; it has no enable flag. | | Streaming | `STREAMING_ENABLED`, `STREAMING_BROKERS`, `STREAMING_TLS_ENABLED`, `STREAMING_TLS_CA_CERT` | The event backbone, Kafka-compatible broker addresses, TLS, and an optional base64-encoded PEM CA certificate for private broker trust. Startup requires `STREAMING_ENABLED=true` and at least one broker. Invalid base64 or decoded non-PEM content prevents startup. | | Auth | `PLUGIN_AUTH_ENABLED`, `AUTH_M2M_INVERSION_ENABLED` | Route authorization, on by default, and the required M2M-inversion gate for BYOC. | | Proxy security | `TLS_TERMINATED_UPSTREAM`, `TRUST_PROXY_ENABLED`, `TRUSTED_PROXIES` | Upstream TLS termination and the restrictive trusted-proxy list it requires in hardened deployments. | | Consignado | `CONSIGNADO_ENABLED` | Defaults to `true`. It gates the inbound `consignado.averbacao.rejected` handling. When you disable it, also keep `CONSUMER_CONSIGNADO_AVERBACAO_REJECTED_ENABLED` off; startup rejects that contradictory configuration. Existing-contract servicing and the booking and payment paths configure independently. See [Consignado privado](/en/products/lender/consignado-privado). | | Ledger M2M custody | `M2M_SECRETS_BACKEND`, `M2M_VAULT_MOUNT`, `AWS_REGION` | Where the ledger M2M credentials are kept on your own deployment. On Lerian Cloud, Lerian manages them. | | Ledger accounts | `MIDAZ_LEDGER_ACCOUNT_PROVISIONING_ENABLED` | Defaults to `true`. Before a relay posts, Lender ensures the non-external account aliases from the accounting profile exist; disable it only when you pre-provision the chart of accounts. It never creates assets. | | Observability | `ENABLE_TELEMETRY`, `OTEL_EXPORTER_OTLP_ENDPOINT` | Telemetry toggle and OTLP collector endpoint. | | Limits | `RATE_LIMIT_ENABLED` | Request rate limiting. Always on when `ENV_NAME=production`. | | Docs | `SWAGGER_ENABLED` | Serve the OpenAPI document and API reference UI. Always off when `ENV_NAME=production`. | ## Tenant and ledger defaults *** Lender serves one tenant (`DEFAULT_TENANT_ID`) with one database schema, and uses a construction-time ledger organization and ledger as the posting default. ## Ledger posting and fails-closed routing *** A booking reaches the ledger through a durable posting intent and an asynchronous relay. The full path is in [Lender in the platform](/en/products/lender/lender-in-the-platform). Routing fails closed. A posting books into the ledger organization and ledger resolved from the accounting profile. A posting that resolves no profile target falls back to the env default. If neither resolves a non-empty target, Lender refuses to post rather than book into an empty or wrong ledger. ## Next steps *** > **[Lender in the platform](/en/products/lender/lender-in-the-platform)** > > The posting path, event catalog, and tenant isolation in detail. # Consignado privado Source: https://docs.lerian.studio/en/products/lender/consignado-privado Payroll-deducted lending in Brazil: the vocabulary, where the commercial terms come from, the collateral model, and the averbação lifecycle topics. **Consignado privado** is payroll-deducted private-sector lending. The paying employer withholds each installment at source, from the borrower's salary. It is a full Brazilian bounded context on top of the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). It has its own vocabulary, its own collateral model, and its own lifecycle topics. ## The vocabulary *** | Term | Gloss | | - | - | | **Consignado** | Payroll-deducted credit. Repayments are withheld from salary at source. | | **Averbação** | Registration of the payroll deduction with the paying entity, so installments are withheld each period. | | **Margem** | The consignable margin, the portion of salary available for deduction. | | **Vínculo** | The employment link between borrower and employer that the loan is deducted against. | | **Competência** | The payroll period (a `YYYYMM` reference) an installment is deducted in. | | **CCB** | *Cédula de Crédito Bancário*, the bank credit instrument for the loan. | ## Where the commercial terms come from *** Your credit engine prices a consignado loan. The rate, the annual rate, the CET, the IOF and the installment plan are contracted facts Lender records, never values Lender computes. Lender binds those terms to a loan product version. Money and rate fields cross the wire as decimal strings (never floats), consistent with the ledger's money model. ## Optional collateral (garantias) *** A consignado contract can carry FGTS and rescission-benefit collateral alongside the payroll deduction. A contract either declares collateral and carries at least one of the three amounts, or declares none and carries none. | Field | Meaning | | - | - | | `valorSaldoDisponivelGarantiaFgts` | FGTS balance amount pledged (from *consultar-saldo-fgts*). | | `valorMultaRescisoriaGarantiaFgts` | FGTS rescission-penalty amount pledged. | | `percVerbaRescisoriaGarantia` | Fraction of severance-verbas pledged, capped at `0.35`. | The two FGTS fields are money amounts (decimal strings, scale 2). The verbas fraction uses a decimal string (scale 8). Lender validates each field. Collateral is off-balance: FGTS balances stay in CAIXA custody and never post to the ledger. Lender tracks the pledge as a domain record on the contract, not as a Midaz posting. ## The rail lifecycle *** The Dataprev rail combines authenticated HTTP commands with asynchronous business facts. The Consignado HTTP API admits averbação. The gateway does not consume an averbação command from the Lender stream. * Lender commands use `lerian.streaming.lender.commands`. * Consignado facts use `lerian.streaming.consignado-gw`. * Event identity comes from source-qualified CloudEvent headers, not from a per-event topic. | Event key | Direction | Runtime behavior | | - | - | - | | `consignado.redirecionamento.requested` | out | The gateway executes the command when an adapter is wired and returns a named refusal otherwise. | | `consignado.exclusao.requested` | out | The gateway resolves the same idempotent exclusion service as HTTP; when the runtime is unavailable it returns a named error. | | `consignado.contract.registered` / `consignado.disbursement.confirmed` | in | Booking and proof-of-payment facts. | | `consignado.averbacao.rejected` / `consignado.employment_status.reported` | in | Rejection and employment-status facts. | | `consignado.redirecionamento.confirmed` / `consignado.redirecionamento.rejected` | in | Redirection outcomes. | | `consignado.exclusao.confirmed` / `consignado.exclusao.rejected` | in | Exclusion outcomes. | | `consignado.reconciliation.received` | in | Payroll bookkeeping, CEF repasse, portal CSV, or guarantee-recovery reconciliation. | | `consignado.contract_correction.available` / `consignado.disbursement_correction.available` | in | The gateway publishes authenticated pointers to corrected CCB or payment resources; Lender retrieves and verifies their SHA-256 digests. | | `consignado.portabilidade.efetivada` / `consignado.portabilidade.rejeitada` | in | When the outcome consumer is enabled and the deployed gateway manifest declares the event, origin-side portability exclusion outcomes close servicing after effectuation or preserve the published refusal. | Use the [Consignado API reference](/en/reference/rails/consignado/fetch-consignado-worker-margin) for rail commands and queries. The producer and consumer contracts live in [Lender events](/en/products/lender/lender-events) and [Consignado events](/en/reference/events/consignado). ## Contracting gate *** `CONSIGNADO_ENABLED` defaults to `true`. It gates the inbound averbação-rejected handling. Servicing for existing contracts remains available. When you disable it, also keep `CONSUMER_CONSIGNADO_AVERBACAO_REJECTED_ENABLED` off. Startup rejects that contradictory configuration. The booking and payment flows configure independently. ## Optional: reconciliation with Matcher *** This integration is configuration-gated and requires Matcher to use `STREAMING_CLOUDEVENTS_SOURCE=matcher`. Lender then consumes `match_run.completed` from `lerian.streaming.matcher` and routes `ce-source: matcher`, `ce-type: studio.lerian.matcher.match_run.completed`, `ce-resourcetype: match_run`, and `ce-eventtype: completed`. If Matcher uses another source, inspect its streaming manifest and align both sides before enabling the integration. The handler translates the verdict into a PDD stage transition and the corresponding ledger posting intent. ## Next steps *** > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > CET, IOF, capitalization consent, PDD staging, and the rest of the BR profile. > **[Lender events](/en/products/lender/lender-events)** > > The wire contract, the topics Lender publishes, and how a handler stays safe. # Core concepts Source: https://docs.lerian.studio/en/products/lender/core-concepts Learn Lender's vocabulary: loan products and versions, accounting profiles, applications, loan accounts, schedules, accrual runs, and Brazil-specific terms. Learn these terms once and every guide, event, and API operation reads the same way. The concepts group by domain: the product catalog, origination, servicing, accounting, and the Brazil extensions. ## Product catalog *** ### Loan product A **loan product** is the template a loan originates from. It is the named credit offering (a CDC line, a consignado line) that carries its terms, charges, and rate configuration. ### Product version A product's terms change over time, so a loan product owns **immutable product versions**. Each version freezes the terms as they stood, and every originated contract traces back to the exact version it used at origination. You **activate** the product to pin one version as its current version. Existing loans keep the version they started on. ### Charge A **charge** is a fee or cost component attached to a product (and inherited by the loans originated from it), for example an origination fee. A charge lives on the product and applies to the schedule. ### Floating rate A product can reference a **floating-rate table** rather than a fixed rate. Lender resolves the applicable rate from the table at the moments the schedule requires it. ### Accounting profile An **accounting profile** binds a product version to the ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.). It declares the posting rules: which double-entry (Every financial movement is recorded as at least two operations: a debit from one account and a credit to another, so the system always balances.) legs fire for each accounting event (disbursement, prepayment, accrual). It also declares which ledger organization and ledger the transaction books into. Without an accounting profile, a product cannot be activated. ## Origination *** ### Loan application A **loan application** is a request to originate a loan against a product version. It moves through an explicit lifecycle: | Action | Meaning | | - | - | | **Submit** | Create the application in a pending state. | | **Approve** | Accept the application; a decision record is kept. | | **Reject** | Decline the application, with a decision record. | | **Withdraw** | Retract an application that is pending approval or approved. | | **Disburse** | Record the funds delivered; the disbursement facts and the resulting loan account are kept. | ### Schedule preview **Schedule preview** computes the amortisation schedule for a prospective loan without originating anything. You preview the installments and disclosures before an application (or before approval) commits to them. ## Servicing *** ### Loan account A **loan account** is an active, disbursed loan, the servicing view of a contract. It carries the live schedule, the transactions posted against it, its charges, and its audit history. ### Schedule and installment The **schedule** is the set of **installments** a loan account owes, in order. Each installment carries its due date and its principal, interest, and charge components. ### Repayment and prepayment A **repayment** records money received against the schedule. Lender allocates it across the outstanding installments, oldest first. A **prepayment** settles ahead of schedule, in whole or in part. You can **preview a repayment** to see the allocation before recording it. ### Reschedule A **reschedule** rewrites the remaining schedule of a loan account, for example after a renegotiation. ### Reversal and replay Corrections are never destructive. A **reversal** undoes a recorded transaction with a compensating entry. A **replay** re-derives account state from its event history. Both preserve a consistent, auditable timeline rather than editing the past. ## Accounting *** ### Posting intent A **posting intent** is the durable record of a financial event that must book to the ledger. Lender persists the intent as part of the same transaction that changes domain state. It then hands the intent to the ledger relay. The intent lives in the database, so it survives a restart. ### Accrual run An **accrual run** computes interest (and other time-based amounts) accrued over a period across the book, producing the postings that recognize it. Each recognition covers one loan account, one month, and one amount kind. A repeated run does not accrue twice. ### Journal reference A **journal reference** is an accrual run's own accounting identifier. It is a separate value from the run's correlation id, which the reference record also stores. The journal reference id identifies one run exactly. A read by correlation id returns the most recent run that shares it. ## Jurisdictions *** ### Jurisdiction and profile A **jurisdiction** is a market whose credit rules Lender knows about. Each jurisdiction supplies a **profile**: the rules, disclosures, and calendar that market requires. Lender ships a **generic** base profile, which books no credit, and a **Brazil (BR)** profile. The active jurisdiction shapes which disclosures and endpoints apply. See [Jurisdictions](/en/products/lender/jurisdictions). ## Brazil terms *** The Brazilian profile adds regulated concepts. The Portuguese terms stay as-is, with a gloss. For the use of each term, see the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). | Term | Gloss | | - | - | | **CET** | *Custo Efetivo Total*, the total effective cost of credit disclosed to the borrower, covering interest, taxes, and fees. | | **IOF** | *Imposto sobre Operações Financeiras*, a federal tax on credit operations, previewable before origination. | | **Capitalization consent** | The borrower's recorded consent to compound-interest (*juros compostos*) capitalization on the contract. | | **PDD stage** | *Provisão para Devedores Duvidosos*, the loan-loss provisioning stage a loan account sits in, with tracked transitions. | | **Credit descriptor** | The SCR-style credit-operation descriptor for a loan account (the regulated operation profile). | | **Prepayment quote** | A quote for settling a loan account early. A Brazilian prepayment settles against one, within the quote's 24-hour validity. | | **CCB** | *Cédula de Crédito Bancário*, the bank credit instrument for a consignado contract. | | **Averbação** | Registration of the payroll deduction with the paying entity, so installments are withheld at source. | ## Next steps *** > **[Lender in the platform](/en/products/lender/lender-in-the-platform)** > > See how these concepts book to the ledger and emit events. > **[Define a loan product](/en/products/lender/define-a-loan-product)** > > Put the product-catalog concepts to work. # Define a loan product Source: https://docs.lerian.studio/en/products/lender/define-a-loan-product Create a loan product, freeze its terms in an immutable version, attach charges and rates, bind an accounting profile, and activate it for origination. A loan product is the template every loan originates from. Defining one is a small sequence: create the product, version its terms, describe how it books to the ledger, then activate the product. You originate each loan against a **product version**, and activation needs that version to carry an accounting profile. ## Before you start *** * You need a Lender bearer token (a JWT issued by your Access Manager). Every operation is authenticated and tenant-scoped. * Decide which [jurisdiction](/en/products/lender/jurisdictions) the product belongs to. Credit books under Brazil. The generic base, `XX`, books no credit. Brazilian products carry extra fields covered in the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). ## Steps *** **1. Create the product** `POST /api/v1/loan-products` registers the named product. This is the catalog entry the versions hang off. (Brazil has an alias, `POST /api/v1/br/loan-products`, that accepts the BR extensions.) **2. Create a version** `POST /api/v1/loan-products/{id}/versions` freezes a set of terms (rate configuration, term, and the schedule shape) as an **immutable version**. Every loan traces back to the version it used at origination. You never edit a version, you publish a new one. A version must declare its rate band and the borrower kinds it admits, or it originates nothing. **3. Attach charges** `POST /api/v1/loan-products/{id}/charges` applies a charge template (an origination fee, a service charge) that the loans originated from this product inherit. **4. Bind an accounting profile** `POST /api/v1/loan-products/{id}/accounting-profiles` declares how the product books to the ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.). The profile carries the posting rules (which double-entry legs fire for each event) and the ledger organization and ledger they book into. Activation refuses a version that has no profile. See [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs). **5. Activate the product** `POST /api/v1/loan-products/{id}/activate` sets the product to active and pins the version you name as its current version. Until activation, the product originates nothing. A Brazilian version also needs its carteira, `normPortfolioClass`, before it activates. Each application names the version it uses at origination, so existing loans keep the version they started on. ## Promote a new version *** Activation pins the first version. Promotion pins every version after it. It repoints a product that is already live at a different current version, without taking the product down and putting it back up. `POST /api/v1/loan-products/{id}/promote` names the version to pin. Brazil has an alias, `POST /api/v1/br/loan-products/{id}/promote`, that behaves identically. Promotion carries the same two readiness checks activation carries: an accounting profile is present, and the version is complete for its jurisdiction. It carries them because it is the same act. A second route without them would be a quiet way around the first. | Refusal | What it means | | - | - | | `LENDER-0101` (`409`) | The product is not active, so there is no current version to repoint. Activate it instead, naming the version to pin. | | `LENDER-0102` (`422`) | The version named is not a version of this product. Read the product's own versions and name one of them. | > [!WARNING] > > **Promotion changes no contract and no application.** Every loan already written keeps the version it was born on. Its schedule, pricing, and charges read from that version for the life of the loan. Every application still in flight keeps its version too. Only new applications read the current version. ## Inspecting products *** | Operation | Purpose | | - | - | | `GET /api/v1/loan-products` | List loan products. | | `GET /api/v1/loan-products/{id}` | Read a single product, including the version pinned as current. | | `GET /api/v1/loan-products/{id}/floating-rates` | Read the product's floating-rate table, if it references one. | > [!TIP] > > A floating-rate product resolves its applicable rate from the table when the schedule needs it, rather than pinning a fixed rate at origination. ## What happens downstream *** Creating and activating products emits `loan_product.created`, `loan_product_version.created`, `loan_product.activated`, `accounting_profile.configured`, and `loan_charge.applied` on the event backbone (see [Lender in the platform](/en/products/lender/lender-in-the-platform)). ## Next steps *** > **[Originate a loan](/en/products/lender/originate-a-loan)** > > Take an active product and originate a loan against it. # How accrual works Source: https://docs.lerian.studio/en/products/lender/how-accrual-works What an accrual run does: the accounting month it recognizes, the amount it computes, what the run produces, and where the resulting posting goes. Lender does not recognize interest continuously. It recognizes interest in an **accrual run**. One run covers one business date, and for every loan it selects it recognizes exactly one accounting month. ## One closed civil month, the same one for every loan *** A loan's accounting month is its **competência**, and the calendar anchors it, not the disbursement date. A run recognizes the last civil month that had fully ended before its **business date**. It recognizes that same month for every loan it selects. A run on any day of April closes March. The mapping is total. Every business date names exactly one competência, and that competência is always a month that is over. No run can recognize a month still in progress. The service sets the month a run closes, whatever day you start the run. A loan disbursed mid-month is not left out of its first month. Interest spreads across the days of each schedule interval, and a competência collects whatever falls inside it. A loan disbursed on the 12th therefore recognizes that first part-month inside the month it was contracted in. ## What a run does *** `POST /api/v1/accrual-runs` takes the business date and the run **mode**, and, optionally, up to 100 loan product ids that scope the run. Both `businessDate` and `mode` are required. `mode` labels the run for your own reporting, with one reservation. The `heartbeat` mode belongs to the scheduled driver, and Lender refuses a call that sends it. **1. Lender selects the loans** You do not send a list. Lender reads its own book and takes each loan account in a disbursed or active application, within the products you scoped. A loan account that already settled drops out. One run covers the whole book. It walks the candidates page by page until they run out, so no contract misses a competência because the book grew. **2. Lender recognizes the interest** For each loan, Lender solves the effective interest rate of the contractual schedule and takes the line for that competência. Recognition works on the contractual cash flows. What the borrower paid does not change it. **3. Lender writes the recognition and the posting intent** One database transaction stores the run, one item per recognition, and the balanced posting intent behind each item. Either all of it is durable, or none of it is. **4. The relay delivers the posting** After the run commits, the outbox relay posts the balanced transaction to Midaz, and Midaz books it. ## The amount Lender recognizes *** Recognition follows the effective interest method. The amortized cost starts at the principal the schedule amortizes, less the origination fee. Withholding taxes stay outside it: in Brazil, IOF is a pass-through and never enters the amortized cost. Lender solves the rate from the schedule itself, so the recognized interest reproduces the contract instead of a separate rate you maintain. Where the jurisdiction taxes interest revenue, the run recognizes that tax too. The tax is a second amount on the same loan and the same competência, with its own balanced posting. Interest and tax never share a transaction. ## Once per loan, per month, per amount *** A recognition is unique on three things: the loan account, the competência, and the kind of amount. A second run for the same business date recognizes nothing new for a loan already recognized. It does not double the interest and it does not enqueue a second posting. That uniqueness is the money-path guarantee. It also makes a run safe to repeat after an interruption. ## What a run produces *** A run answers with: * The run identifier and the status of the run. * A **journal reference**: the accounting identifier of the run. * The **correlation id** Lender derives from the mode, the business date, and the products in scope. Behind that answer, the run stores one item for each recognition: the loan account, the competência, the kind of amount, and the amount. Those items are what Lender builds the postings from. Use the journal reference id to tie the run to your own accounting records: it identifies one run exactly. The reference record also stores the correlation id. Lender derives that id from the mode, the business date, and the products in scope. A read by correlation id therefore returns the most recent run that shares it. [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs) covers the two operations that read it back. ## The run does not write the ledger entry *** A run recognizes interest and enqueues a posting intent. It does not call Midaz, and it does not wait for a booking. The relay posts afterwards, and the ledger books the transaction. A successful run means the recognition and its intent are durable, not that the ledger already shows the entry. A posting that never reached the ledger is not lost work. Retry the run. Lender rebuilds the intent behind every item the ledger has not acknowledged and enqueues it again under the same deterministic key. As a result, the relay delivers it, and a duplicate stays a duplicate. [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs) covers what a retry answers back. Configure two things before a posting can book: * Give the product version's accounting profile a rule for the `accrual` event, with balanced legs. Where the jurisdiction taxes interest revenue, add the optional `accrual_tax` rule as well, so the tax has legs of its own. * Lender always initializes the outbox. Configure the connection to the ledger so the relay can deliver the durable intent. See [Configuration and deploy](/en/products/lender/configuration-and-deploy). Configure the profile before the first run. Without an accrual rule, Lender has no legs to build a posting from, and the run leaves that loan out. ## Loans a run passes over *** A selected loan can still recognize nothing: * The jurisdiction suspends its accrual. In Brazil, the two deepest stages of the provisioning ladder suspend accrual (see [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)). A suspended contract still books its retained origination fee; the interest is what it withholds. * An earlier run already decided that competência for it. * Its interest for that competência is zero. None of these fails the run. A run recognizes what it can and reports what it recognized. ## Running accrual on a schedule *** The run also has a scheduled driver inside the service, and it is on by default. It fires every day at 06:00 UTC until you give it another cron expression. Daily does not mean a competência a day. Every fire closes the same month, the first one that succeeds decides it, and every later fire that month finds the work already done. The daily cadence is a retry. A service that is down on the 1st closes the month on the 2nd. A once-a-month expression would have lost that competência for good. The scheduled path and the API path use the same code. A run from cron and a run from a call behave identically. ## Next steps *** > **[Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs)** > > Accounting profiles, posting rules, and the journal-reference operations. > **[Lender architecture](/en/products/lender/lender-architecture)** > > The domains, the jurisdiction seam, and the outbox that carries money out. > **[Define a loan product](/en/products/lender/define-a-loan-product)** > > Products, versions, and the accounting profile a run depends on. > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > Provisioning stages, taxes, and the disclosures the Brazilian profile adds. # How origination works Source: https://docs.lerian.studio/en/products/lender/how-origination-works The path one loan application takes: what it binds to, the four lifecycle decisions, the single transaction that disburses it, and the loan account it leaves behind. Origination turns a request for credit into a live loan account. It has four decisions and one transaction that does everything at once. This page follows one application from submitted to disbursed. > [!NOTE] > > Jurisdiction `XX` books no credit. Lender refuses its products, versions, and applications with 422. Read the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack) for what the `BR` profile adds to this path. ## What an application binds to *** An application binds to a **product version**, never to a product on its own. The version pins the currency, the rate terms, and the accrual basis, so a contract always traces back to the terms it was created under. A later version does not change a loan that already exists. The product must be active, and the version must be the one the product currently originates on. Activation requires an **accounting profile** on the version. The disbursement builds its posting from the profile's legs. See [Define a loan product](/en/products/lender/define-a-loan-product). ## Preview, if you want it *** Schedule preview computes the installments and the cost disclosure for prospective terms. When it records the result as an offer, it returns the offer's identifier. It creates no application. Use it to show a borrower what the loan looks like before anyone commits. This step is optional. ## Submit *** The submit call creates the application in `pending_approval`. It carries the product version, the borrower and the borrower kind, the requested principal, the requested **monthly** interest rate, the number of installments, and an expected disbursement date. Lender accepts up to 600 installments. The requested rate must fall inside the version's rate band. The body does not carry the **assigned officer**. Lender takes it from the authenticated subject of the submit call. Later decisions are checked against that officer, so submit with the identity that will also approve and disburse. ## Decide *** Exactly one decision resolves a pending application. | Decision | Result | Who may act | | - | - | - | | Approve | `approved`, with the approved amount and a decision timestamp | The jurisdiction's approval policy. Under `BR`, the assigned officer. | | Reject | `rejected` | The assigned officer. | | Withdraw | `withdrawn` | The borrower, or the assigned officer. | An approved application can still be withdrawn. `rejected` and `withdrawn` are final, and nothing moves out of them. The approved amount moves no money. It bounds every disbursement that follows. ## Disburse *** Disbursement moves money, so it carries the most guards. Three things belong in the request: * An `X-Idempotency` header. Lender requires it on this operation. * The **loan account identifier**. It is a UUID you choose, and Lender does not mint one for you. That identifier addresses the schedule, the transactions, the charges, and the audit trail. * The gross requested amount and the net delivered amount. Lender checks five rules. It checks the first four before it writes anything. It checks the balance rule inside the disbursement transaction, so a failure there rolls the whole disbursement back. | Guard | Rule | | - | - | | Amount | A single disbursement must release the approved amount exactly. A tranche declares its `trancheNumber`. | | Total | Gross across every tranche must stay within the approved amount. | | Chronology | The disbursement must not predate the application's creation or the approval decision. | | Identity | The loan account identifier must be the same on every tranche. | | Balance | Net must equal gross minus the withholdings the jurisdiction computes, minus the retained origination fee. | Under `BR` the withholding is IOF. Quote it before you disburse with the tax preview in the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). ## One transaction, four results *** A disbursement is a single database transaction. Four things happen inside it. **1. The application becomes disbursed** Lender appends a disbursement event that records the amounts, the date, and the actor who disbursed. **2. Lender writes the schedule** Lender computes a Price (French) amortisation schedule over the cumulative disbursed principal. It stores the result as schedule version 1, with the change reason `origination`. **3. The jurisdiction pipeline runs** Under `BR` the pipeline computes the IOF withholding. **4. Lender enqueues the posting intent** Lender writes a balanced posting intent to the outbox: principal debited at gross, cash credited at net, one credit for each withholding, and one credit for a retained origination fee. All four commit together, or all four roll back together. There is no half-disbursed loan. If the schedule cannot be written, or the posting does not balance, the application stays `approved`. ## More than one tranche *** You can disburse an approved application more than once. The status stays `disbursed`, Lender appends another disbursement event, and Lender writes a **new schedule version** over the cumulative principal. The new version supersedes the previous one on read. The total across tranches still cannot exceed the approved amount, and every tranche uses the same loan account identifier. ## Two callers, one application *** Every lifecycle write states the status it expects to find. When two callers decide the same application at the same time, one wins and the other receives `409 Conflict` without changing anything. A repeated disburse that does not match the first one is refused the same way. ## The state model *** | From | Decision | To | | - | - | - | | `pending_approval` | approve | `approved` | | `pending_approval` | reject | `rejected` | | `pending_approval` or `approved` | withdraw | `withdrawn` | | `approved` or `disbursed` | disburse | `disbursed` | ## What you have at the end *** * An application that reads `disbursed`, with one disbursement event per tranche. * A loan account under the identifier you supplied, carrying its schedule, its transactions, its charges, and its audit events. * A posting intent on the way to the ledger. That booking is asynchronous, so it lands shortly after the request returns rather than during it. * One lifecycle event on the event backbone for each transition. Continue in [Service a loan](/en/products/lender/service-a-loan). ## Next steps *** > **[Service a loan](/en/products/lender/service-a-loan)** > > Record repayments, prepay, reschedule, and correct an active loan account. > **[Lender architecture](/en/products/lender/lender-architecture)** > > The domains, the jurisdiction seam, and the outbox that carries money out. > **[Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs)** > > Posting rules, accrual runs, and the journal reference that ties a booking back. > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > IOF, CET, capitalization consent, and the rest of the Brazilian profile. # Jurisdictions Source: https://docs.lerian.studio/en/products/lender/jurisdictions How Lender stays jurisdiction-aware: the profile registry, the generic base, the Brazil profile, and the discovery endpoints that expose them. Credit rules differ by market. Lender is **jurisdiction-aware**: it carries a registry of jurisdiction **profiles**, each supplying the rules, disclosures, and calendar its market needs. The active jurisdiction shapes which behavior applies. ## The profile model *** * **Generic (XX)**: the base profile. It books no credit: Lender refuses to create a product, a version, or an application under it. A tenant with no jurisdiction binding is refused with 403. * **Brazil (BR)**: the profile that books credit. It adds CET disclosure, IOF preview, capitalization consent, PDD staging, the credit-operation descriptor, prepayment quotes, a Brazilian business calendar, and the consignado privado journey. You reach Brazilian behavior through BR-prefixed endpoints (`/api/v1/br/...`). Several of those endpoints are jurisdiction-aware aliases of the generic operations. See the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack) and [Consignado privado](/en/products/lender/consignado-privado). ## Discovery endpoints *** The compiled-in profiles are discoverable at runtime, so a client can learn which jurisdictions a deployment supports: | Operation | Purpose | | - | - | | `GET /api/v1/jurisdictions` | List the registered jurisdiction profiles. Each one reports `booksCredit`. | | `GET /api/v1/jurisdictions/{code}` | Read one jurisdiction profile by its code. | > [!NOTE] > > Profiles ship with the service, not as runtime configuration. Discovery tells you what a given deployment can do. You cannot edit a profile through the API. ## Next steps *** > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > Everything the Brazilian profile adds. # Lender architecture Source: https://docs.lerian.studio/en/products/lender/lender-architecture The parts Lender is built from: eight domains, the jurisdiction profile that supplies every market rule, the request pipeline, and the outbox that carries money to the ledger. Lender is one service. Inside it, eight domains own the credit journey, a jurisdiction **profile** supplies every market-specific rule, and money reaches the ledger through a durable queue instead of an inline call. ## Eight domains, one service *** | Domain | What it owns | Where it stops | | - | - | - | | **Products** | Loan products, product versions, charge templates, floating-rate tables. | A version never changes. New terms are a new version. | | **Origination** | The application lifecycle, and the schedule that disbursement produces. | It records the intent to book. It does not write to the ledger. | | **Servicing** | The active loan account: schedule versions, repayments, prepayments, reschedules, corrections. | It never edits history. A correction is a new transaction. | | **Accounting** | Accounting profiles, posting rules, posting intents, accrual runs, journal references. | It builds balanced postings. The relay delivers them. | | **Audit** | The trail of what happened to a loan account. | Append-only. Nothing rewrites an event. | | **Assignment** | Funds, sets of receivables assigned to them, their documents, and the cession facts. | It records external facts as evidence of each step. | | **Issuance** | The credit instruments that evidence an application. | An instrument evidences an application that already exists. | | **Portfolio** | The portfolio dashboard and the delinquency reads. | It reads. It does not change a loan account. | Each domain owns its own tables and its own operations. The **loan account identifier** is the key that joins them. Lender does not mint that identifier. You supply it when you disburse, and every domain addresses the loan by it from then on. ## The jurisdiction seam *** Credit rules differ by market, so no domain names a market. Everything market-specific arrives through a jurisdiction profile, which supplies a fixed set of capabilities: * The tax engine that computes withholdings at disbursement and revenue taxes on accrual. * The holiday calendar and the day-count convention. * The effective-cost method behind the cost disclosure. * The caps registry that holds regulated rate and fee limits. * The disclosures the market requires. * The disbursement pipeline that runs inside the disbursement transaction. * The product validator and the schedule-preview extension. * The actor policies that decide who may approve and who may disburse. Profiles are compiled into the service rather than configured at runtime. Two ship: **BR** for Brazil, and **XX**, the generic base, which books no credit. The jurisdictions table in the database is a projection of what the service carries, so no API call creates a jurisdiction. See [Jurisdictions](/en/products/lender/jurisdictions). ## How a request resolves *** Every request passes the same three gates before a handler runs. **1. Authentication** Lender expects a bearer JWT. The two jurisdiction-discovery reads are the only public operations. **2. Tenant resolution** The tenant comes from the validated identity. It is never a header, a body field, or a path parameter, so a caller cannot select a tenant. **3. Jurisdiction resolution** Lender reads the tenant's jurisdiction binding and puts the matching profile in the request context. The lookup is cached for five minutes, so a rebinding takes effect within that window. Bind every tenant to a jurisdiction before it sends traffic. Lender refuses a request from an unbound tenant. ## Money leaves through the outbox *** Lender never posts to the ledger during your request. The path has four properties: 1. A money event, such as a disbursement, a repayment, or an interest accrual, writes a **posting intent** in the same database transaction as the business row. A crash between the two is not possible. 2. A dispatcher reads the intent from the outbox and relays it to Midaz. 3. Each intent carries a deterministic idempotency key, so a retried relay collapses onto one ledger transaction. 4. Routing fails closed. A posting books into the organization and ledger that the accounting profile resolves, and a single-tenant deployment can fall back to its configured default. When no target resolves, Lender does not post at all. Configure the ledger endpoint and its credentials before you expect bookings. Until they resolve, intents wait in the outbox and nothing reaches the ledger. [Lender in the platform](/en/products/lender/lender-in-the-platform) covers the full path, including what the accounting profile contributes to it. ## What the service exposes *** | Surface | Purpose | | - | - | | `/api/v1/...` | The whole product API, described by one OpenAPI document. | | `/health`, `/readyz`, `/version` | Liveness, dependency readiness, and build metadata. All three answer before authentication. | | `/api/v1/streaming/manifest` | The catalog of events this deployment publishes. | | `/api/v1/systemplane/...` | Runtime configuration administration. | Read the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract shared across Lerian products. ## Stores *** | Dependency | Role | | - | - | | PostgreSQL | Every domain table, and the outbox. One migration set applies. | | Valkey or Redis | Idempotency records, and the jurisdiction cache. | | RedPanda | Lifecycle events. The service does not start without it. | | Midaz | The destination of every posting. | [Configuration and deploy](/en/products/lender/configuration-and-deploy) lists the settings behind each one. ## Time-driven work *** The accrual run also runs as a scheduled job. Each job has its own on or off default and its own schedule, and you can override both. See [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs). ## Next steps *** > **[How origination works](/en/products/lender/how-origination-works)** > > One application from submitted to disbursed, and the transaction that does the work. > **[Core concepts](/en/products/lender/core-concepts)** > > The vocabulary the whole product shares. > **[Lender in the platform](/en/products/lender/lender-in-the-platform)** > > The posting path, the event catalog, and tenant isolation. > **[Configuration and deploy](/en/products/lender/configuration-and-deploy)** > > Dependencies, the container, and the settings that shape a deployment. # Lender console Source: https://docs.lerian.studio/en/products/lender/lender-console The Lender web console: how it signs in, the screens it ships with, and the work it leaves to the API. The **Lender console** is a browser interface over the same [API](/en/products/lender/lender-rest-api) you can drive programmatically. It is a single environment-agnostic bundle: a deployment serves the console its configuration at start-up, so the same image points at any environment. This page documents the screens in the console. ## Signing in *** The console is a standard OpenID Connect client. It runs the authorization-code flow with PKCE straight from the browser to the identity provider you configure. Point it at the same Access Manager that issues the API tokens. The console exchanges the code for tokens, and refreshes them on demand before they expire. Tokens live in memory only. The console never writes them to browser storage. A reload or a new tab therefore starts from the sign-in screen rather than from a session someone else could read off the device. After the exchange the console asks Lender who the caller is, and takes the permissions it renders from that answer rather than from the token. The console does not show an action the identity may not perform, unless the deployment's tokens carry no declared permissions at all. In that case, the console shows every action and lets the server refuse the ones the identity may not take. Signing out ends the local session and, when you configure the provider's end-session URL, signs the user out at the provider too. Without that URL, the provider's own session survives and the next sign-in skips the prompt. > [!WARNING] > > **Authentication ships turned off.** In its default configuration the console establishes no session, shows every screen, and sends no bearer token. A deployment gets this state if nobody configures the identity provider. Turn it on and supply the authorization endpoint, the token endpoint, and the client id before the console reaches anyone. If you turn it on without those three, the console refuses loudly, with a misconfiguration screen rather than partial access. ## What you can do in it *** | Area | Screens | What an operator does there | | - | - | - | | Portfolio | Dashboard | The landing screen. Exposure, delinquency buckets, the trend over recent months, and the delinquent-loan register for a business date, each row opening the loan account behind it. See [Portfolio and delinquency](/en/products/lender/portfolio-and-delinquency). | | Credit | Loan products | List and filter the catalog, walk the wizard that creates a product and its first version, and build a version's accounting profile. See [Define a loan product](/en/products/lender/define-a-loan-product). | | Credit | Loan applications | List and filter applications, create one against a product version after previewing its schedule, and open one to approve, reject, withdraw, or disburse it. See [Originate a loan](/en/products/lender/originate-a-loan). | | Credit | Credit instrument | Emit the CCB for an application against the loan product's published document template, list the instruments already emitted for it, and download one. | | Credit | Loan accounts | List live accounts and open one for its schedule, transactions, charges, and audit trail, with record-repayment, prepay, and reschedule actions and the Brazilian panel for PDD stage, prepayment quote, and tax. See [Service a loan](/en/products/lender/service-a-loan). | | Accounting | Accrual runs | Queue a run for a business date, and retry the items of one that did not finish. See [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs). | | Accounting | Journal references | Resolve a correlation id or a reference id to the posting it names. | | Operations | Re-assessments | The worklist of contracts whose qualitative problem-asset characterisation has outlived the re-assessment horizon of its product version. | | Operations | Divergence queue | The consignado exception desk: filter by category, effect, status, assignee, and breached deadline, then open one to compare the client's declared fact against Lender's own, assign it, dispose of it, or send it back to the queue. | | Operations | Assignment desk | Assemble a set of receivables for a fund, check its eligibility, approve it, produce the offer file, ingest the remessa the fund returns, record the endorsement term, and work the exceptions the set raises. | | Settings | Jurisdictions | Read the jurisdiction profiles compiled into the service. | The console has no permission model of its own. Every action it offers goes through the same API operation, authorized by the same identity and tenant. ## What the console does not do *** The console has no screen for reconciliation, CNAB export, rescission capture, portability capture, or a consignado contract sheet and list. An account already in a portability-requested state still displays as one. There is no consignado contract screen because a consignado contract is not created in the console. A CLT contract arrives as a fact from the gateway once the client's own systems have registered it with the payroll rail. Private and INSS lending is originated through the ordinary loan application above. See [Consignado privado](/en/products/lender/consignado-privado). ## Next steps *** > **[Lender REST API](/en/products/lender/lender-rest-api)** > > Everything the console does, driven programmatically. # Lender events Source: https://docs.lerian.studio/en/products/lender/lender-events Use Lender's v3 application streams, source-qualified CloudEvents types, outbox delivery, manifest, and runtime consumer contract. Lender publishes business facts and commands as CloudEvents 1.0 over Kafka. Publication is required, tenant-scoped, and backed by a transactional outbox. ## Configure publication *** | Setting | Value | | - | - | | `STREAMING_ENABLED` | `true` | | `STREAMING_BROKERS` | Your Kafka or RedPanda brokers. | | `STREAMING_CLOUDEVENTS_SOURCE` | `lender`. Startup rejects any other source. | Lender does not start when publication is disabled, when no broker is set, or when the source is invalid. ## Wire contract *** Lender uses the v3 application-stream contract. A topic identifies the producing application and message class, not one event. | Wire element | Value | | - | - | | Fact topic | `lerian.streaming.lender` | | Command topic | `lerian.streaming.lender.commands` | | `ce-source` | `lender` | | `ce-type` | `studio.lerian.lender..` | | Dispatch key | `.` from `ce-resourcetype` and `ce-eventtype` | | Partition key | Tenant identifier | For example, a disbursement fact travels on `lerian.streaming.lender` with `ce-type: studio.lerian.lender.loan_application.disbursed`. `ce-schemaversion` carries the payload version. Schema v2 does not add a topic suffix under the v3 contract. Route on the CloudEvents headers, not on a per-event topic name. ## Published contract *** The manifest declares 39 definitions: 37 facts and two commands. Feature gates control individual integration paths, so consume only the contracts exposed by the deployed manifest. The two catalog commands are: * `consignado.exclusao.requested`: requests a consignado exclusion from the payroll rail. * `consignado.redirecionamento.requested`: requests a consignado redirection from the payroll rail. See [Lender event reference](/en/reference/events/lender) for every catalog definition and its source-qualified `ce-type`. ## Events Lender consumes *** Lender wires 15 gateway-facing fact keys from `lerian.streaming.consignado-gw`. The keys include contract registration, disbursement confirmation, averbação rejection, employment status, redirection outcomes, reconciliation, exclusion outcomes, correction availability, portability outcomes, and refinance outcomes. The corresponding consumers are configuration-gated. Portability and refinance each use one opt-in flag for their pair of outcomes. The gateway has emission paths for the first 13 keys. The portability pair reaches Lender only when its outcome consumer is enabled and the deployed gateway manifest declares the event. The manifest does not declare the two refinance outcomes, so those two listeners receive no live traffic from the current gateway runtime. A Lender handler consumes `match_run.completed` from `lerian.streaming.matcher`. For the default handoff, Matcher must use `STREAMING_CLOUDEVENTS_SOURCE=matcher`. Lender routes `ce-source: matcher`, `ce-type: studio.lerian.matcher.match_run.completed`, `ce-resourcetype: match_run`, and `ce-eventtype: completed`. If Matcher uses another source, inspect its streaming manifest and align both sides before enabling the runtimes. See [Consignado privado](/en/products/lender/consignado-privado) for the payroll journey. ## Durability and delivery *** Lender writes each outbound business event to the PostgreSQL outbox in the same database transaction as the state change. A relay publishes it after commit. Broker delivery is at least once: deduplicate on `(ce-source, ce-id)` and make handlers idempotent. The relay retries each row up to `OUTBOX_MAX_DISPATCH_ATTEMPTS` times. The `OUTBOX_RETRY_WINDOW_SEC` value sets the minimum wait between attempts for a row. It is a per-attempt delay, not a total retry window. The defaults are `10` attempts and `300` seconds. ## Payload conventions *** Payloads are JSON facts, not diffs. Money and rate values use decimal strings rather than JSON numbers. Timestamps use RFC 3339 UTC. Read `ce-schemaversion` before decoding a versioned payload. ## Check the deployed contract *** `GET /api/v1/streaming/manifest` returns the deployed catalog, application topics, schema versions, and delivery policies. It requires a bearer token with `streaming_manifest` `read`. Read the manifest when your consumer starts and reject an unexpected source, event key, or schema version before processing traffic. ## Next steps *** > **[Lender event reference](/en/reference/events/lender)** > > Inspect emitted, catalog-only, and consumed contracts. > **[Consignado privado](/en/products/lender/consignado-privado)** > > Follow the payroll command and fact flow. # Lender in the platform Source: https://docs.lerian.studio/en/products/lender/lender-in-the-platform See how Lender posts to the Midaz ledger, emits lifecycle events on the event backbone, scopes each event and posting to its tenant, and reports telemetry data. Lender is a Lerian primitive that runs on the platform. Four platform seams matter when you operate it: the ledger posting path, the event stream, tenant isolation, and observability. ## The Midaz posting path *** Lender is the source of truth for the credit journey. [Midaz](/en/products/midaz/about-midaz) is the source of truth for balances. Each money event, such as a disbursement, a repayment, or an interest accrual, becomes a balanced double-entry (Every financial movement is recorded as at least two operations: a debit from one account and a credit to another, so the system always balances.) transaction bound for the ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.). The path preserves that posting intent and delivers it idempotently. The ledger reaches consistency once the outbox delivery succeeds and the dispatcher posts, which may lag or stay pending while routing is unavailable. **1. A domain event produces a posting intent** Lender persists a durable **posting intent** on a money event. The intent lands in the same database transaction that changes domain state. Lender writes the intent to a transactional **outbox**, so it survives a crash between "state changed" and "ledger booked." **2. The relay posts a balanced transaction to Midaz** With the Midaz ledger relay configured, an outbox dispatcher posts the balanced double-entry transaction through the official SDK. Otherwise, the durable intent remains in the outbox. The legs come from the product's **accounting profile**. **3. Idempotency is durable** Each posting carries a deterministic idempotency key computed by Lender. A retried post collapses to the same Midaz transaction. Midaz's own idempotency window is a backstop, not the primary guard. **4. Routing fails closed** The transaction books into the ledger organization and ledger the accounting profile resolves. If routing cannot resolve a non-empty target, the post **fails closed**: Lender refuses to book rather than write into an empty or wrong target. With the ledger relay configured, Lender reaches Midaz through [Access Manager](/en/platform/access-manager). It authenticates with machine-to-machine (M2M) credentials and a short-lived JWT, per tenant. ## Lifecycle events *** Lender's event catalog contains 39 definitions: 37 facts and two commands. Every event Lender emits is outbox-backed and tenant-scoped. Runtime emission also depends on the applicable feature gates. Check the deployed manifest and the [Lender event reference](/en/reference/events/lender) before depending on a definition as a live integration. Lender uses the v3 application-stream contract: | Wire element | Value | | - | - | | Fact topic | `lerian.streaming.lender` | | Command topic | `lerian.streaming.lender.commands` | | `ce-source` header | `lender` | | `ce-type` header | `studio.lerian.lender..` | | Partition key | the tenant | All facts share one application topic, and all commands share the command topic. Route by `ce-resourcetype` and `ce-eventtype`. Schema version travels in `ce-schemaversion` and does not change the topic. The events ride the same outbox as the ledger relay, so they share one durability guarantee. The local state change and the event commit together in a single database transaction. The posting intent joins them when the change produces one. The outbox dispatcher applies the ledger posting afterward, when it relays the intent to Midaz. Lender's state and the Midaz booking therefore reach consistency later, not in the same transaction. > [!NOTE] > > Lender wires 15 payroll-gateway keys from `lerian.streaming.consignado-gw`. The corresponding consumers are configuration-gated. Its Matcher listener consumes `match_run.completed` from `lerian.streaming.matcher`. The contract matches only when Matcher uses `STREAMING_CLOUDEVENTS_SOURCE=matcher` and sends `ce-source: matcher`, `ce-type: studio.lerian.matcher.match_run.completed`, `ce-resourcetype: match_run`, and `ce-eventtype: completed`. See [Lender events](/en/products/lender/lender-events) and [Consignado privado](/en/products/lender/consignado-privado). ## Tenant isolation *** Like every Lerian product, Lender is built for full tenant isolation. * **Tenant-scoped** events and ledger postings: every emitted event and every Midaz transaction carries the tenant it belongs to. ## Observability *** Lender emits structured logs, OpenTelemetry traces, and metrics on the same observability stack as the rest of the platform, including database, assertion, and panic-recovery metrics. See [Observability](/en/platform/observability). ## Next steps *** > **[Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs)** > > Go deeper on posting rules, accrual runs, and journal references. > **[Configuration and deploy](/en/products/lender/configuration-and-deploy)** > > See the dependencies and configuration the posting path and event stream require. # Prerequisites Source: https://docs.lerian.studio/en/products/lender/lender-prerequisites What must be true before your first Lender call: the services it depends on, the migrations, the configuration that origination requires, and the token that carries the officer. Lender needs a small set of things in place before its first API call. This page lists them in the order you set them up. When every item here is true, follow the [Quick start](/en/products/lender/lender-quick-start) to originate a loan. ## Services Lender depends on *** | Service | Version | Why Lender needs it | | - | - | - | | **PostgreSQL** | 17 | The primary data store. Every product, application, schedule, and posting intent lives here. | | **Valkey** (Redis-compatible) | 8 | Idempotency records, cache, and rate limiting. | | **Identity provider** | — | Issues the bearer tokens Lender authorizes each route against. | | **Midaz** | — | The ledger the disbursement posting reaches. Optional for your first loan. | | **RedPanda** | — | The event backbone for the event catalog. Required. | PostgreSQL, Valkey, and RedPanda are required: Lender does not start without the event backbone. An identity provider is required when route authorization is enabled, which is the default. Production requires it. A first loan needs no Midaz. Read [Ledger postings](#ledger-postings) below for what waits. ## Run Lender locally *** Lender is a single Go service. A local run needs the Go toolchain at 1.26 or later, Docker with Compose, and Make. **1. Create the environment file** `make set-env` copies `config/.env.example` to `config/.env`. Edit that file for every setting on this page. **2. Start the dependencies** `make up` starts the Compose dependencies and the service on port `8080`. **3. Apply the migrations** `make migrate-up` applies the migrations. See the next section. **4. Run with live reload** `make dev` runs the service with Air. ## Apply the migrations *** Lender does not migrate the database when it starts. You apply migrations out of band, from one set in the `migrations` directory. The container image carries a `/migrate` binary that applies it. The set also seeds the **jurisdiction registry**: two active jurisdictions, `BR` for Brazil and `XX`, the generic base. No API creates a jurisdiction. The deployed binary carries each profile's behavior, and the table records which codes that binary knows. Read [Jurisdictions](/en/products/lender/jurisdictions) for what a profile decides. ## Required configuration *** The PostgreSQL outbox is mandatory and is initialized automatically. Configure authentication when your deployment enables route authorization. ### An authenticated subject: `PLUGIN_AUTH_ENABLED=true`, the default Lender records who acted on every loan application. It reads the assigned officer from the bearer token's subject, not from the request body. Every loan-application call therefore needs an authenticated identity. `PLUGIN_AUTH_ENABLED` defaults to `true`. Set `PLUGIN_AUTH_HOST` to the address of your identity provider. Then send a bearer token on every call. The token needs a non-empty subject claim, and the same subject must create, approve, and disburse the application. Lender defines its roles and permissions in a seed file. Load that seed into your identity provider. The officer's role needs these permissions: | Resource | Actions | | - | - | | `loan_product` | `write` | | `accounting` | `write` | | `loan_applications` | `preview:schedule`, `create`, `approve`, `disburse` | | `loan_accounts` | `read` | ### A durable posting sink: always present A disbursed loan records its posting intent durably. Lender initializes its transactional PostgreSQL outbox on every deployment, in the same database transaction that moves the application to `disbursed`. `OUTBOX_ENABLED` does not exist. The dispatcher relays each intent afterwards, on the cadence `OUTBOX_DISPATCH_INTERVAL_SEC` sets. Read [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs) for what the ledger receives. ## Ledger postings *** Point Lender at Midaz when you want postings to land in the ledger: | Variable | Purpose | | - | - | | `MIDAZ_LEDGER_BASE_URL` | The ledger address. | | `MIDAZ_OAUTH_TOKEN_URL` | Where Lender gets its ledger credentials. | | `MIDAZ_LEDGER_ORGANIZATION_ID`, `MIDAZ_LEDGER_ID` | The default posting target. | The loan originates with or without these. Until you configure a ledger endpoint, posting intents stay durable in the outbox and the ledger entry waits. Configure the ledger before you expect bookings. ## Tenant defaults *** `DEFAULT_TENANT_ID` identifies the tenant, and it must be a valid UUID. The migrations bind the default tenant, `11111111-1111-1111-1111-111111111111`, to `BR`. If you set another `DEFAULT_TENANT_ID`, add its row to `public.tenant_jurisdictions`, or Lender refuses its requests with 403. ## Event streaming *** Lender does not start without the event backbone. Set `STREAMING_ENABLED=true`, point `STREAMING_BROKERS` at RedPanda, and set `STREAMING_CLOUDEVENTS_SOURCE=lender`. Lender validates these values when it starts. ## Checklist *** * PostgreSQL 17, Valkey 8, and RedPanda reachable. * Migrations applied. * `STREAMING_ENABLED=true` and `STREAMING_BROKERS` set. * When route authorization is enabled, ensure the identity provider is reachable, and use a bearer token with a subject and every permission in the table above. * `DEFAULT_TENANT_ID` a valid UUID. ## Next steps *** > **[Quick start](/en/products/lender/lender-quick-start)** > > Seven calls from an empty database to a disbursed loan. > **[Configuration and deploy](/en/products/lender/configuration-and-deploy)** > > The full container shape, the wider environment surface, and the ledger posting path. # Quick start Source: https://docs.lerian.studio/en/products/lender/lender-quick-start Seven calls from an empty database to a disbursed loan: a product, a version, an accounting profile, an activation, an application, an approval, and a disbursement. This page takes you from an empty database to one disbursed loan in seven calls. Every call sends `application/json`. **Send every money and rate field as a decimal string**, never as a JSON number. Complete the [Prerequisites](/en/products/lender/lender-prerequisites) first. Send the same bearer token on all seven calls. Lender takes the officer from the token subject, and only that officer can approve and disburse the application. > [!NOTE] > > This walkthrough books a Brazilian loan with tax regime `none`, which computes no IOF and keeps the amounts in step 7 simple. Read the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack) for a product that withholds IOF. A payroll-deducted contract belongs to the consignado privado bounded context. Read [Consignado privado](/en/products/lender/consignado-privado) for its vocabulary and its lifecycle topics. ## The seven calls *** ``` 1. POST /api/v1/loan-products → productId 2. POST /api/v1/loan-products/{productId}/versions → versionId 3. POST /api/v1/loan-products/{productId}/accounting-profiles 4. POST /api/v1/loan-products/{productId}/activate → active 5. POST /api/v1/loan-applications → pending_approval 6. POST /api/v1/loan-applications/{id}/approve → approved 7. POST /api/v1/loan-applications/{id}/disburse → disbursed ``` ## 1. Create the product *** `POST /api/v1/loan-products` ```json theme={null} { "name": "Personal loan", "loanType": "personal", "jurisdictionCode": "BR" } ``` `loanType` takes `personal`, `commercial`, or `card`. `jurisdictionCode` is `BR`. Any other code returns 422. The response carries the new `id`. The product is in `draft`. ## 2. Create a version *** `POST /api/v1/loan-products/{productId}/versions` ```json theme={null} { "jurisdictionCode": "BR", "changeReason": "initial version", "currency": "BRL", "rateMode": "fixed", "fixedAnnualRateBps": 1800, "rateBand": { "minAnnualRateBps": 1200, "maxAnnualRateBps": 4800 }, "admittedBorrowerKinds": ["pessoaFisica"], "jurisdictionExtensions": { "taxRegime": "none", "normPortfolioClass": "C5" } } ``` The version is the immutable snapshot of terms an application binds to. * **`currency` has no default.** Supply a valid ISO-4217 code in uppercase. The version is the currency source of truth from here to the ledger. * **`rateBand` is required.** It is the price band the version authorizes, in annual basis points. An application whose rate falls outside it is refused. * **`admittedBorrowerKinds` names who the version lends to.** An empty or omitted list admits no borrower. * **`normPortfolioClass` is the carteira.** A Brazilian version without one cannot be activated in step 4. * **A fixed version carries no floating linkage.** With `rateMode: fixed`, omit `floatingRateTableId`, `floatingSpreadBps`, and `requiresFloatingRate`. Lender rejects a fixed version that carries any of them. * **`jurisdictionCode` is required**, and it must match the product's. A version cannot move a product to another jurisdiction. The response carries the `versionId`. ## 3. Bind an accounting profile *** `POST /api/v1/loan-products/{productId}/accounting-profiles` The profile maps each accounting event onto general-ledger accounts. Activation refuses a product without one, so bind it now. ```json theme={null} { "loanProductVersionId": "", "accountingMode": "accrual", "postingRules": [ { "eventType": "disbursement", "legs": [ { "account": "1100.10.001", "role": "principal", "side": "debit" }, { "account": "1000.10.001", "role": "cash", "side": "credit" } ] }, { "eventType": "repayment", "legs": [ { "account": "1000.10.001", "role": "cash", "side": "debit" }, { "account": "1100.10.001", "role": "principal", "side": "credit" } ] }, { "eventType": "prepayment", "legs": [ { "account": "1000.10.001", "role": "cash", "side": "debit" }, { "account": "4200.10.001", "component": "charge_rebate", "side": "debit", "optional": true }, { "account": "4300.10.001", "component": "iof_refund", "side": "debit", "optional": true }, { "account": "1100.10.001", "role": "principal", "side": "credit" }, { "account": "2400.10.001", "component": "iof_due", "side": "credit", "optional": true } ] }, { "eventType": "accrual", "legs": [ { "account": "1200.10.001", "role": "interest", "side": "debit" }, { "account": "4100.10.001", "role": "interest", "side": "credit" } ] }, { "eventType": "collection_unapplied", "legs": [ { "account": "1000.10.001", "role": "cash", "side": "debit" }, { "account": "2100.10.001", "role": "unapplied_cash", "side": "credit" } ] }, { "eventType": "collection_reapply", "legs": [ { "account": "2100.10.001", "role": "unapplied_cash", "side": "debit" }, { "account": "1100.10.001", "role": "principal", "side": "credit" } ] }, { "eventType": "collection_refund", "legs": [ { "account": "2100.10.001", "role": "unapplied_cash", "side": "debit" }, { "account": "1000.10.001", "role": "cash", "side": "credit" } ] } ] } ``` Send one rule per accounting event. Seven events need a rule: `disbursement`, `repayment`, `prepayment`, `accrual`, `collection_unapplied`, `collection_reapply`, and `collection_refund`. The other event types are optional. Fewer than seven rules is rejected before the handler runs. A product under Brazilian credit-loss rules also needs `provision_constitution` and `provision_reversal`, or its measured provision does not post to the ledger. Activation does not check them. Each leg declares exactly one of `role` or `component`, and no account appears on both sides of the same rule. Four events also carry a fixed leg shape: | Event | Legs | | - | - | | `accrual` | Exactly one debit and one credit. | | `prepayment` | Exactly five, in this order: `cash` debit, optional `charge_rebate` debit, optional `iof_refund` debit, `principal` credit, optional `iof_due` credit. | | `collection_unapplied` | Exactly two: `cash` debit, `unapplied_cash` credit. | | `collection_refund` | Exactly two: `unapplied_cash` debit, `cash` credit. | `collection_reapply` takes `unapplied_cash` as its only debit. Each credit it carries must also appear as a credit on `repayment` or `prepayment`, with the same account and the same role. `disbursement` and `repayment` each need at least one debit and one credit. Keep the `disbursement` rule at two legs, as above. Lender fills the `cash` leg with the net amount and every other structural leg with the gross amount. A `component` leg takes a computed withholding, and tax regime `none` computes none, so the two-leg rule is the one that balances in this walkthrough. Read [Define a loan product](/en/products/lender/define-a-loan-product) for the wider product surface. ## 4. Activate the product *** `POST /api/v1/loan-products/{productId}/activate` ```json theme={null} { "versionId": "" } ``` Activation pins the version that new applications originate on. It refuses a product that has no accounting profile, and a Brazilian version that has no carteira. An application on a product that is not active is refused. ## 5. Create the application *** `POST /api/v1/loan-applications` Send the header `X-Idempotency`. Lender requires it on this call and on steps 6 and 7. A retry with the same value replays the first response and writes nothing new. ```json theme={null} { "loanProductVersionId": "", "borrowerId": "3f2b8c1e-7a4d-4e9b-9c2a-5d6e7f8a9b0c", "borrowerKind": "pessoaFisica", "requestedPrincipalAmount": "50000.00", "requestedInterestRate": "0.01500000", "requestedInstallments": 24, "expectedDisbursementDate": "2026-08-01T12:00:00Z" } ``` * `borrowerId` is a UUID. `borrowerKind` takes `pessoaFisica` or `pessoaJuridica`, and the version must admit it. * `requestedInterestRate` is the **monthly** rate as a decimal string at scale 8. It must be greater than `0` and no greater than `1`. Lender builds the schedule from this rate and compares it with the version's rate band in annual basis points: `"0.01500000"` a month is `1800` bps a year, inside the band from step 2. * `requestedInstallments` is between 1 and 600. * `previewScheduleSnapshotId` is optional. To cite the quote you showed the borrower, send the `scheduleOfferSnapshotId` that `POST /api/v1/loan-applications/preview-schedule` returned. * There is no `assignedOfficerId` field. Lender sets it from the token subject. The application comes back in `pending_approval`, carrying the jurisdiction code and profile version Lender resolved from the product version. ## 6. Approve *** `POST /api/v1/loan-applications/{id}/approve` ```json theme={null} { "approvedAmount": "48000.00", "decisionAt": "", "note": "within policy" } ``` `approvedAmount` becomes the ceiling on everything you disburse. The application moves to `approved` and carries the decision record. ## 7. Disburse *** `POST /api/v1/loan-applications/{id}/disburse` ```json theme={null} { "loanAccountId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "grossRequestedAmount": "48000.00", "netDeliveredAmount": "48000.00", "disbursedAt": "", "jurisdictionExtensions": { "disbursedAt": "", "loanProductVersionId": "", "requestId": "" } } ``` > [!WARNING] > > **With tax regime `none`, `netDeliveredAmount` must equal `grossRequestedAmount`.** Lender accepts a disbursement only when net equals gross minus withholdings minus the retained origination fee. This regime computes no withholdings, and this walkthrough retains no fee. `loanAccountId` is a UUID **you** supply. Lender does not mint it. It identifies the loan account this contract services under, and it is immutable across later tranches of the same application. `jurisdictionExtensions` carries what the Brazilian pipeline reads: the disbursement instant, the version from step 2, and this call's `X-Idempotency` value as `requestId`. The disbursement fails when one is absent. Lender also checks that: * `netDeliveredAmount` does not exceed `grossRequestedAmount`. * `grossRequestedAmount` equals `approvedAmount`, because this is the application's only disbursement. A release in tranches declares `trancheNumber`, and its running total does not exceed `approvedAmount`. * `decisionAt` and `disbursedAt` are not before the application's creation, and `disbursedAt` is not before the approval decision. * The jurisdiction and profile version still match the pair Lender resolved at step 5. ## Confirm the loan exists *** The disburse response carries the application in `disbursed` with its disbursement event. Then read the loan account: | Call | What it returns | | - | - | | `GET /api/v1/loan-accounts/{loanAccountId}` | The active loan account: status, principal balance, open balance, and disbursement date. | | `GET /api/v1/loan-accounts/{loanAccountId}/schedule` | The origination schedule, one entry per installment, all unpaid. | The response also carries `profileVersion`, the jurisdiction profile version, not a product version. It carries no currency. The loan uses the `currency` you set on the loan product version in step 2. If you configured the Midaz ledger relay, the disbursement posting reaches the ledger when the outbox dispatcher relays the intent. That happens shortly after the call, not inside it. Otherwise the intent remains in the outbox. ## Next steps *** > **[Service a loan](/en/products/lender/service-a-loan)** > > Record repayments, prepay, reschedule, and correct a live loan account. > **[How accrual works](/en/products/lender/how-accrual-works)** > > Interest recognition runs on its own schedule. Start an accrual run, or enable the heartbeat. # Lender REST API Source: https://docs.lerian.studio/en/products/lender/lender-rest-api Orient yourself in the Lender API: the /api/v1 base path, bearer authentication, resource-and-action authorization, decimal-string money, scoped idempotency, pagination, the problem+json error shape, and the operations grouped by job. Lender serves one HTTP API. Every operation sits under the base path `/api/v1`, and nothing versions in the host. A product list is `GET /api/v1/loan-products`. This page covers what the operations share, then groups them by the job they do. Each operation has its own page under the **Lender** anchor in the [API Reference](/en/reference/introduction), with full request and response shapes. > [!NOTE] > > 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 *** Authentication is deployment-configured. `PLUGIN_AUTH_ENABLED` defaults to `true`. When enabled, protected routes require a JWT bearer token: ```http theme={null} Authorization: Bearer ``` On Lerian Cloud, Lender resolves the tenant from the `tenantId` claim of the validated identity. Two reads are public and take no token: [list jurisdictions](/en/reference/products/lender/list-jurisdictions) and [get a jurisdiction](/en/reference/products/lender/get-jurisdiction). The registry is deployment metadata, so a client can read it before it has an identity. The probes stay outside authentication so an orchestrator reaches them without a token: `/health`, `/readyz`, and `/version`. ### Authorization *** Lender authorizes each request against the `lender` application, a resource, and an action. The resource follows the surface, and the actions are granular rather than one write: | Resource | Actions | | - | - | | `loan_product` | `read`, `write` | | `loan_applications` | `read`, `preview:schedule`, `create`, `approve`, `reject`, `withdraw`, `disburse`, `capitalization-consent:ingest`, `preview:tax` | | `loan_accounts` | `read`, `write`, `audit:read`, `charge:apply`, `repayment:preview`, `repayment:record`, `repayment:reverse`, `returned_payment:collect`, `prepayment:record`, `reschedule`, `cet:read`, `pdd:transition` | | `accounting` | `read`, `write`, `retry`, `supersede` | | `streaming_manifest` | `read` | Grant the officer role only the actions its job needs. Lender defines its roles and permissions in a seed file you load into your identity provider. [Prerequisites](/en/products/lender/lender-prerequisites) shows the minimum set for origination. ### Tenant and officer identity *** **The tenant is never a header, a query parameter, or a body field.** A self-hosted Lender uses `DEFAULT_TENANT_ID`. On Lerian Cloud, Lender resolves the tenant from the validated identity. The assigned officer comes from the token subject in the same way. No loan-application body carries an officer field, and no client-supplied value overrides the subject. ## Requests and responses *** Every operation that carries a body sends and returns `application/json`. Send money amounts and decimal rates such as `requestedInterestRate` as decimal strings: `"50000.00"`, `"0.01500000"`. Product-version and floating-rate values `fixedAnnualRateBps`, `floatingSpreadBps`, and `annualRateBps` are integer basis points. Timestamps are RFC 3339 in UTC. ## Idempotency *** Lender holds 29 operations to an at-most-once contract, in two classes. The class decides which header you send and where the guarantee lives. | Class | Header | Where the guarantee lives | Operations | | - | - | - | - | | Enforced at the edge | `X-Idempotency`, required by the schema | A middleware in front of the handler, backed by the idempotency store | 26 writes: the five loan-application writes (create, approve, reject, withdraw, disburse), prepayment and its Brazil alias, the Brazil prepayment quote, the Brazil problem-asset assessment, reschedule, product charges, accrual runs and their retry, the chart-supersession sweep, assignment-set facts, assignment funding payoff, consignado collection settlement, and the nine consignado operator actions | | Committed with the money | `X-Request-ID`, with `X-Idempotency` as the fallback | A marker written in the same database transaction as the money | 3 writes: record a repayment, reverse a transaction, collect a returned-payment receivable | Send your own key. On the 26 edge-enforced operations: * A retry of a **completed** call replays the first response and stamps `X-Idempotency-Replayed: true` on it. Lender records nothing a second time. * A retry while the first call is still **in flight** answers `409`. Wait and read the resource. Do not send the command again. * A request carrying **no key** answers `400` with the code `IDEMPOTENCY_KEY_REQUIRED`. The key is a required header on the operation's schema, so the refusal happens before any handler logic runs. * A key sent under a spelling Lender does not read (`Idempotency-Key` or `X-Idempotency-Key`) answers `400` with the code `IDEMPOTENCY_KEY_HEADER_UNKNOWN`, rather than executing the command unprotected. Sending one of those alongside the contract header is fine. The refusal fires only when `X-Idempotency` is absent. * Lender scopes the key to your tenant, and the key expires after the window `IDEMPOTENCY_RETRY_WINDOW_SEC` sets, which defaults to 86400 seconds (24 hours). **The contract is fail-closed, in both directions.** A deployment with no reachable idempotency store does not start. If the store becomes unreachable while it runs, a guarded operation answers `503` with the code `IDEMPOTENCY_UNAVAILABLE` rather than executing unprotected. Every refusal carries a machine-readable code, and a client branches on it to choose between resending and reconciling. The `IDEMPOTENCY_OUTCOME_UNKNOWN` and `IDEMPOTENCY_OUTCOME_UNKNOWN_UNFENCED` codes say the request reached the handler and the outcome cannot be established. Read the resource before deciding. The refusal codes form a published contract. Renaming a code breaks that contract. The three transactional operations work differently. All read `X-Request-ID` first and fall back to `X-Idempotency` when it is absent. Send one of the two: a call that carries neither answers `422`. Lender stores the request id in the database together with the facts of the call. A retry that carries the same id and the same facts replays the first response on the normal response path, with no replay header. The same request id with **different** facts answers `409` rather than replaying, so one id can never record two different amounts. That record does not expire. The facts Lender compares differ by operation: | Operation | Facts compared against the stored request id | | - | - | | Record a repayment | The loan account, the amount, and the effective date (`transactionDate` when `effectiveDate` is absent). | | Reverse a transaction | The transaction being reversed, the loan account, the reversal effective date, the reason, the profile version, and the jurisdiction code. The amount comes from the original transaction, so it is not compared. | | Collect a returned-payment receivable | The receivable and the collection itself. Redelivering the same collection under the same request identity answers `200` with the receivable that already stands; a second, different collection against a collected receivable answers `409`. | ## Pagination *** Paging is per operation, not global. Read the reference page for the operation you call, and send only the parameters it declares. | Read | Parameters | | - | - | | [List loan products](/en/reference/products/lender/list-loan-products) and [list Brazilian loan products](/en/reference/products/lender/list-loan-products-br) | `limit` (default 25, ceiling 100) and `offset` (default 0, ceiling 10000) | | [Audit events](/en/reference/products/lender/list-loan-account-audit-events-huma) | `limit` alone (default 50, ceiling 100) | Lender rejects a value outside the range rather than clamping it. Every other read declares its own parameters, so narrow it with the identifiers and filters on its reference page. ## Errors *** Huma and global-handler errors answer `application/problem+json` and follow [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457). Authorization and idempotency middleware can use their own response formats. | Field | What it carries | | - | - | | `status` | The HTTP status code. | | `title` | The status name. | | `detail` | What went wrong on this occurrence. | | `errors` | Optional schema-validation details. Each entry carries a `location`, a `message`, and the `value` Lender received. | Branch on status and content type. For a `422`, use `errors` when present. Handler or domain validation can return only top-level `detail`. A server-side failure answers with a generic detail, so a raw cause never reaches a client. ## The operations by job *** ### Catalogue a product Eight operations own the catalog. [Create a product](/en/reference/products/lender/create-loan-product) and [append a version](/en/reference/products/lender/create-loan-product-version) build the terms an application binds to. The version is immutable. [Bind an accounting profile](/en/reference/products/lender/create-loan-product-accounting-profile) maps each accounting event onto general-ledger accounts, and activation requires it. [Apply a charge](/en/reference/products/lender/create-loan-product-charges) and [read floating rates](/en/reference/products/lender/list-loan-product-floating-rates) complete the surface, alongside [list](/en/reference/products/lender/list-loan-products), [get](/en/reference/products/lender/get-loan-product), and [activate](/en/reference/products/lender/activate-loan-product). Read [Define a loan product](/en/products/lender/define-a-loan-product). ### Originate Six operations carry an application from submitted to disbursed: [create](/en/reference/products/lender/create-loan-application), then one of [approve](/en/reference/products/lender/approve-loan-application), [reject](/en/reference/products/lender/reject-loan-application), or [withdraw](/en/reference/products/lender/withdraw-loan-application), then [disburse](/en/reference/products/lender/disburse-loan-application). [Preview a schedule](/en/reference/products/lender/preview-loan-schedule) computes installments for a quote and can record it as an offer. [List](/en/reference/products/lender/list-loan-applications) and [get](/en/reference/products/lender/get-loan-application) read applications. Read [How origination works](/en/products/lender/how-origination-works) for the state machine and [Quick start](/en/products/lender/lender-quick-start) for the seven calls end to end. ### Service a live loan Five reads describe the account: [the account](/en/reference/products/lender/get-active-loan-account), [its schedule](/en/reference/products/lender/get-active-loan-schedule), [its transactions](/en/reference/products/lender/list-active-loan-transactions), [its charges](/en/reference/products/lender/list-active-loan-charges), and [its audit history](/en/reference/products/lender/list-loan-account-audit-events-huma). Five writes move money or the schedule: [preview a repayment](/en/reference/products/lender/preview-repayment) before you [record it](/en/reference/products/lender/record-repayment), [prepay](/en/reference/products/lender/prepay-loan-account), [reschedule](/en/reference/products/lender/reschedule-loan-account), and [reverse a transaction](/en/reference/products/lender/reverse-loan-account-transaction). Nothing rewrites history. A reversal posts a new transaction that compensates the original. Read [Service a loan](/en/products/lender/service-a-loan). ### Account and book [Start an accrual run](/en/reference/products/lender/create-accrual-run) recognizes interest for a competence period. [List journal references](/en/reference/products/lender/get-journal-reference) by correlation id and [read one](/en/reference/products/lender/get-journal-reference-by-id) to find the accounting record a run wrote. Read [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs). ### Discover jurisdictions The two public reads report which jurisdiction codes this deployment carries and what each profile decides. Read [Jurisdictions](/en/products/lender/jurisdictions). ### Brazil The Brazil pack adds regulated reads and writes under `/api/v1/br`. They are [CET disclosure](/en/reference/products/lender/get-loan-account-cet-disclosure), [the credit-operation descriptor](/en/reference/products/lender/get-loan-account-credit-operation-descriptor), [PDD stage](/en/reference/products/lender/get-loan-account-pdd-stage) and [its transitions](/en/reference/products/lender/apply-loan-account-pdd-stage-transition), [a prepayment quote](/en/reference/products/lender/create-prepayment-quote) with [its payoff statement](/en/reference/products/lender/get-payoff-statement), [tax preview](/en/reference/products/lender/preview-tax), and [capitalization consent](/en/reference/products/lender/ingest-capitalization-clause-consent). The pack also carries its own product paths, which behave like the generic ones under Brazilian rules. Read [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). The payroll-deducted journey is an event conversation with the payroll rail, not a set of REST calls. Read [Consignado privado](/en/products/lender/consignado-privado). ### More surfaces Each of these families has its own operations in the reference. * **Assign receivables to a fund.** `/api/v1/assignment/...` registers funds, assembles and scores sets of receivables against a fund's eligibility rule, and approves or discards them. It also produces the offer file and the endorsement term, and ingests the administrator's return file. It records externally-observed facts, registers a receivable's credit instrument at the registradora, and works the exception queue. * **Collect a returned payment.** `/api/v1/returned-payment-receivables` lists and reads the separate borrower debts a reversed final payment opens when the contract it closed can no longer take the money back. It also records a collection against one. * **Promote a product version.** `POST /api/v1/loan-products/{id}/promote` repoints a live product at another of its versions. As a result, new applications originate on it, while contracts already written keep the version they were born on. The operation's Brazil alias is `/api/v1/br/loan-products/{id}/promote`. * **Read and revise product versions.** `/api/v1/loan-product-versions` lists and reads the version snapshots, and declares a charge model revision on one. Under the Brazil pack, it also declares a version's norm classification. * **Template the documents.** `/api/v1/loan-products/{productId}/document-templates` and the matching surface under an assignment fund draft, read, and publish the template versions Lender renders a generated document from. * **Table the floating rates.** `/api/v1/rate-tables` creates a floating-rate table and appends the periods Lender reads a floating product's rate from. * **Issue a credit instrument.** `/api/v1/loan-applications/{applicationId}/credit-instruments` issues the instrument that evidences an application, lists the instruments issued, and downloads one. * **Follow an accrual run.** `/api/v1/accrual-runs` lists runs, reads one and its per-item decisions, and re-posts the recognitions the ledger has not acknowledged through `POST /api/v1/accrual-runs/{id}/retry`. The `/api/v1/accounting-profiles` surface lists the configured profiles. The supersession pair supersedes a product's chart of accounts and settles the change onto every live contract's balances. * **Watch the portfolio.** `/api/v1/dashboard/portfolio` and the two delinquency reads answer from the daily portfolio snapshot. The daily PDD promotion sweep builds that snapshot for its business date before it scans, and it ships enabled, so a default deployment has one. The separate `core-daily-portfolio-snapshot` job drives the same producer on its own schedule for deployments that want it built independently of the sweep. * **Ladder the provision stages.** `/api/v1/br/pdd-stage-ladders` publishes a PDD stage ladder version and reads the one in force. * **Work the extraordinary collection.** `/api/v1/br/consignado/collection-instructions` and the settlement pair list the open instructions, and ingest the collection agent's Arquivo de Baixa Extraordinária. They also list the lines it refused. The operator surface under `/api/v1/internal/br/consignado/...` appoints the agent's channel, works quarantined lines and inbox items, and disposes over-deduction refunds owed to workers. * **Inspect the identity and the stream.** `GET /api/v1/me` reports the session the token resolves to, and `GET /api/v1/internal/streaming/discards` lists the inbound records quarantined by the consumers. ## Next steps *** > **[Quick start](/en/products/lender/lender-quick-start)** > > Seven calls from an empty database to a disbursed loan. > **[Events](/en/products/lender/lender-events)** > > Subscribe to the credit journey instead of polling. > **[API Reference](/en/reference/introduction)** > > Every operation, with full request and response shapes. > **[Prerequisites](/en/products/lender/lender-prerequisites)** > > The services, migrations, and configuration a first call needs. # Lender SDK and embedding Source: https://docs.lerian.studio/en/products/lender/lender-sdk-and-embedding Integrate Lender from your own software: the REST surface that carries every operation, what your client must handle for money, errors, retries and paging, and the pattern for running Lender behind a product your users already use. **Lender's REST API is the whole client surface.** Products, applications, loan accounts, accrual runs, and the Brazilian regulatory records are all HTTP calls under `/api/v1`. Nothing exists only inside a client library. Call Lender with the HTTP client your stack already has. There is no Lender client library to install, so the API is the contract you write against. This page covers what your client must handle and what to keep on your own side. It then covers how to place Lender behind a product your users already talk to. Read [Lender REST API](/en/products/lender/lender-rest-api) for the operations themselves. > [!NOTE] > > 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 client or SDK generation. ## What your client must handle *** Four rules cover most of the code you write against Lender. ### Money travels as a decimal string Money amounts are JSON decimal strings, normally with two decimal places. Decimal rates such as `requestedInterestRate` are strings with eight decimal places. The `fixedAnnualRateBps`, `floatingSpreadBps`, and `annualRateBps` fields are integer basis points: ```json theme={null} { "requestedPrincipalAmount": "50000.00", "requestedInterestRate": "0.01500000" } ``` Send the string, and parse the string with your language's decimal type. A binary float loses cents, and a JSON number invites one. Timestamps are RFC 3339 in UTC. ### Errors answer problem+json Most operation and fallback errors answer `application/problem+json`. Do not assume that for 401 and 403 responses. Branch on `status` and content type. A schema-validation `422` can include an `errors` array with field details, while handler or domain validation can return only top-level `detail`. Treat `detail` as text for a person, not as a key your code matches on. A server-side failure answers with a generic detail on purpose, so no internal cause reaches a client. Log the status and your own correlation identifier, and let Lender's traces carry the rest. ### Retry your writes with your own key The money and lifecycle writes take an idempotency key that you generate. They include the five loan-application writes, product charges, prepayment, reschedule, repayment, and reversal. Derive the key from your own request identifier, so a retry replays the first answer instead of writing a second time. The operations that require `X-Idempotency` replay a completed call with `X-Idempotency-Replayed: true` on the response. They answer `409` while the first call is still in flight. Repayment, reversal, and returned-payment collection key on `X-Request-ID` instead, and fall back to `X-Idempotency` when it is absent. A retry with the same facts replays. The same id with different facts answers `409`. So branch on the response body, never on the presence of the replay header. The middleware fails **closed** during an idempotency-store outage: it refuses with `503` rather than letting an unprotected write through. Treat that refusal as "not executed, try again", and reconcile before reaching for a new key. [Lender REST API](/en/products/lender/lender-rest-api#idempotency) lists which header each of those operations takes, and how long a key lives. ### Page with filters, not with deep offsets Paging is per operation. The product list reads take `limit` and `offset`. The audit history takes `limit` alone. A value outside the declared range answers `422` rather than a quietly reduced page. Read the reference page for the operation you call, and narrow the query instead of walking a long offset. ## Keep the identifiers your writes return *** The origination operations are commands: create, approve, reject, withdraw, and disburse. Each one answers with the full application body. Its `id` identifies the loan application. After disbursement, `disbursementEvent.loanAccountId` identifies the loan account. Store both on your side as you go. Your own record then links your borrower to the loan account. Every servicing read starts from an identifier you already hold. The schedule, the transactions, the charges, and the audit history all key on the loan account. ## Embedding Lender behind your own product *** Lender 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 Lender server to server. Four rules keep that boundary clean. **Never hand a Lender token to a browser or a mobile app**. Your service authenticates your user and decides whether that user may act. It then calls Lender with a token of its own. **Mint the token for the person who acts**. Lender derives the HTTP actor from the token subject, never from a request field. The assigned officer approves and rejects. Either the borrower or assigned officer may withdraw. The pinned jurisdiction's actor policy governs approval and disbursement, so do not assume one human subject performs every transition. **Hold one credential per tenant**. A self-hosted Lender uses `DEFAULT_TENANT_ID`. On Lerian Cloud, the tenant comes from the validated identity, never from a header, a query parameter, or a body field, so your service holds one credential for every tenant it serves. **Learn about state changes from events**. Lender publishes business events through its outbox, and it does not start without the event backbone. Read [Lender events](/en/products/lender/lender-events). > [!NOTE] > > A disbursement records its posting intent in the same database transaction that moves the application to `disbursed`. Lender relays it to Midaz only when the ledger relay is configured. Otherwise, the intent remains in the outbox. Do not treat a `200` on disburse as proof that the ledger already carries the entry. Read [Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs). ## Next steps *** > **[Lender REST API](/en/products/lender/lender-rest-api)** > > The base path, authentication, idempotency, and the operations by job. > **[Lender events](/en/products/lender/lender-events)** > > The wire contract and the events you can subscribe to. > **[Quick start](/en/products/lender/lender-quick-start)** > > Seven calls from an empty database to a disbursed loan. > **[Prerequisites](/en/products/lender/lender-prerequisites)** > > The services, migrations, and configuration a first call needs. # Originate a loan Source: https://docs.lerian.studio/en/products/lender/originate-a-loan Walk a loan application through its lifecycle (preview the schedule, submit, decide, and disburse) and see the loan account it produces. Origination is the journey from "a borrower wants credit" to "a loan account exists and the disbursement is recorded". Lender models it as an explicit application lifecycle. The lifecycle keeps a record of every decision, and it makes every disbursement traceable. ## The application lifecycle *** An application moves through named states. Each transition is a distinct operation, and each keeps a decision or disbursement record: ``` submit ─────► pending_approval ── approve ──► approved ── disburse ──► loan account │ │ │ reject withdraw withdraw ``` ## Steps *** **1. Preview the schedule (optional)** `POST /api/v1/loan-applications/preview-schedule` computes the amortisation schedule for prospective terms **without originating anything**. When it records the quote as an offer, it returns the offer's `scheduleOfferSnapshotId`, which the application can cite. Use it to show installments and disclosures before anyone commits. **2. Submit the application** `POST /api/v1/loan-applications` creates the application against the product's current version. It declares the borrower kind, and the requested rate must fall inside the version's rate band. **3. Decide** Resolve the application with exactly one of: * `POST /api/v1/loan-applications/{id}/approve`: accept it, with a decision record. * `POST /api/v1/loan-applications/{id}/reject`: decline it, with a decision record. * `POST /api/v1/loan-applications/{id}/withdraw`: retract it while pending approval or, after approval, before disbursement. **4. Disburse** `POST /api/v1/loan-applications/{id}/disburse` records the funds delivered. It takes the `X-Idempotency` key every write on this page takes. A retry under the same key therefore replays the first response and records no second disbursement. One database transaction commits the disbursement event, the origination schedule, and a balanced posting intent. When the call returns, the loan is an active **loan account** you service from here on. The ledger booking is not in that transaction. With the ledger relay configured, an outbox dispatcher relays the intent to Midaz shortly after the response, through the product's accounting profile. Otherwise, the intent remains in the outbox. See [the posting path](/en/products/lender/lender-in-the-platform). ## Every write takes an idempotency key *** Creating an application and moving it (approve, reject, withdraw, disburse) all require the `X-Idempotency` header. Lender reads the key from that spelling and no other. It refuses `Idempotency-Key` and `X-Idempotency-Key` by name, so a client is never silently unprotected because it guessed the header. Sending one of those alongside the contract header is fine. The refusal fires only when `X-Idempotency` is absent. | What you send | What you get | | - | - | | No key | `400`, and nothing runs. | | The same key, the same body | The first response, replayed. Nothing runs a second time. | | The same key, a different body | `422`. The key is already spent on a different instruction. | The guard fails **closed**. When the idempotency store cannot answer, Lender refuses the request with `503` instead of waving it through unprotected. Creation costs a key deliberately: without one, a client that times out on create and calls again books a second application for the same borrower. That is the cheapest duplicate in the service to produce and the most expensive to unpick. It is expensive because both copies look legitimate and only a person can say which one is real. > [!NOTE] > > On the four lifecycle writes, you can reconcile a request that ends unresolved and resubmit it under a **new** key. Read the application by id and ask whether it already holds the state you wanted. Disbursement is the exception, where a new key is not a safe recovery at all. A genuine second release declares its tranche instead. ## The product has to be active *** Origination reads the loan product's activation status and refuses a product that is not active, with `LENDER-0225` and a `422`. The refusal sits on the three steps that decide new credit: creating the application, approving it, and releasing the money over the API. A product switched off after an application was filed therefore stops that application before the money leaves. Creating an application must also name the product's **current** version, or Lender refuses it with `LENDER-0226` and a `422`. That check runs at creation and nowhere else, because an application keeps the version it was born on for life. Promoting a new version therefore never kills the applications standing against the old one, and each of them still books on the version it named. See [Define a loan product](/en/products/lender/define-a-loan-product). Two paths sit outside this gate on purpose: * The **consignado** rail, which records a contract the client's own systems already registered with the payroll rail. Refusing a fact that already happened would leave the book denying a contract the borrower holds. * A **substantial renegotiation**, whose replacement contract inherits the product version of the contract it replaces. The replacement contract inherits that version whether or not that product is still active and whether or not that version is still current. Renegotiating services credit already written. Only a new application is new credit. Lender gates nothing after origination. Reject, withdraw, servicing, and accounting keep working against a product retired years later. ## What you get *** Disbursement produces a **loan account**, the servicing view of the live contract, carrying its schedule, transactions, charges, and audit history. Continue in [Service a loan](/en/products/lender/service-a-loan). ## What happens downstream *** The lifecycle emits `loan_application.submitted`, `loan_application.approved`, `loan_application.rejected`, `loan_application.withdrawn`, and `loan_application.disbursed`, each at schema version `2.0.0`. Disbursement records a durable posting intent and relays it to the ledger only when you configure the ledger relay. Read [the posting path](/en/products/lender/lender-in-the-platform). > [!NOTE] > > Brazilian origination adds regulated steps (CET disclosure and capitalization consent) covered in the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). The payroll-deducted **consignado** flow forms its own Brazilian bounded context. See [Consignado privado](/en/products/lender/consignado-privado). ## Next steps *** > **[Service a loan](/en/products/lender/service-a-loan)** > > Record repayments, prepay, reschedule, and correct an active loan account. # Portfolio and delinquency Source: https://docs.lerian.studio/en/products/lender/portfolio-and-delinquency Read the active loan book in aggregate: the portfolio dashboard, the delinquent-loan register, and the nightly snapshot both of them read. Servicing tracks each loan account individually. The portfolio surface reads the book in aggregate. With it, you can see the shape of the portfolio (A grouping of accounts belonging to the same account holder, business unit, or purpose. It ties together checking, savings, and investment accounts for a unified view.) and which loans fall behind, without querying loan by loan. Both reads answer from a **snapshot** of one business day, never from a live scan of the book. That makes them cheap enough to put on a dashboard. It is also why they answer for a date rather than for "now". ## Portfolio dashboard *** `GET /api/v1/dashboard/portfolio` returns the aggregate picture of the active book for a business date. The picture holds outstanding, current and delinquent exposure, the count of active and delinquent loans, and disbursement and repayment volume. It also holds the portfolio-at-risk ratios at 30, 60 and 90 days. Alongside them, it returns the loan count and exposure per delinquency bucket, and a month-by-month trend leading up to the date. It also returns the drill-down rows for the bucket you asked about. Narrow the read with `productIds` and `productVersionIds`. Omit `asOf` and Lender uses the current business day in Brazil. ## Delinquent loans *** `GET /api/v1/dashboard/delinquent-loans` returns the register behind the numbers: the loan accounts sitting in one delinquency bucket, page by page, so you can prioritise collections and provisioning. Ask for a bucket with `bucketCode` (`current`, `1_30`, `31_60`, `61_90`, `91_plus`, or the cumulative `par30`, `par60` and `par90`), and narrow to one loan officer's book with `assignedOfficerId`. Brazil has an alias, `GET /api/v1/br/dashboard/delinquent-loans`, that adds a filter on the regulatory *carteira* C1 to C5 of Res. BCB 352/2023 to every filter above: the business date, the products, the product versions, the bucket, the loan officer and the paging all still apply. Lender derives the carteira from the product, so one that no product declares returns an empty page. ## Where the numbers come from *** A nightly snapshot job, off by default, measures every live loan account against the business day and writes the snapshot these two reads answer from. The **PDD promotion sweep** described in the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack) depends on the same snapshot. It builds the snapshot for its own business day when nothing else did. A deployment that leaves the snapshot job off and keeps the sweep on therefore still has a measured book. > [!WARNING] > > Lender refuses a business date nobody measured with `503`, and never answers it with a page of zeros. An unmeasured book and a book with nothing overdue produce the same numbers. Answering the first as if it were the second would report a healthy portfolio that was never looked at. Treat the refusal as "retry", not as "the book is empty". > > The retry only starts answering once a producer has measured that date. The producer is the [nightly PDD promotion sweep](/en/products/lender/brazil-regulatory-pack#the-nightly-promotion-sweep), which ships on, or the dedicated snapshot job, which ships off. With both of them off, no amount of retrying helps. Each answer carries its own provenance. It names which snapshot it came from, the business date that snapshot measured, and when it was generated. It also says whether the freshness policy considers it stale, and why. ## Audit is per-account *** Portfolio views are aggregate reads. The authoritative, immutable record of what happened to any single loan is its audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) (`GET /api/v1/loan-accounts/{id}/audit-events`), covered in [Service a loan](/en/products/lender/service-a-loan). Every repayment, reversal, and replay lands there. ## Next steps *** > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > PDD staging, the nightly promotion sweep, and the provisioning the stages drive. # Service a loan Source: https://docs.lerian.studio/en/products/lender/service-a-loan Operate an active loan account: read its schedule and transactions, record and preview repayments, prepay, reschedule, and correct with reversals. After disbursement, a loan becomes a **loan account**, the servicing view of the live contract. Servicing covers the whole life of the account. Money comes in, the schedule changes, and you correct mistakes without a rewrite of history. ## Read the account *** | Operation | Purpose | | - | - | | `GET /api/v1/loan-accounts/{id}` | Read the active loan account. | | `GET /api/v1/loan-accounts/{id}/schedule` | Read the current installment schedule. | | `GET /api/v1/loan-accounts/{id}/transactions` | List transactions posted against the account. | | `GET /api/v1/loan-accounts/{id}/charges` | List active charges on the account. | | `GET /api/v1/loan-accounts/{id}/audit-events` | Read the immutable audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) of lifecycle events. | ## Record money *** **1. Preview the allocation** `POST /api/v1/loan-accounts/{id}/preview-repayment` shows how Lender would allocate a repayment across outstanding installments **without recording it**. Lender pays the oldest due installment first. Inside each installment it pays penalties, then fees, then interest, then principal. **2. Record the repayment** `POST /api/v1/loan-accounts/{id}/repayments` records money received and allocates it across the schedule. Send a request id in `X-Request-ID`, or in `X-Idempotency` as the fallback. A call with neither answers `422`. The same id with the same loan account, amount, and effective date returns the repayment already recorded. The same id with different facts answers `409`. See [Idempotency](/en/products/lender/lender-rest-api#idempotency). **3. Prepay** `POST /api/v1/loan-accounts/{id}/prepayments` settles the loan ahead of schedule, in whole or in part. A Brazilian loan account needs a [prepayment quote](/en/products/lender/brazil-regulatory-pack) first. ## Change the schedule *** `POST /api/v1/loan-accounts/{id}/reschedules` rewrites the remaining schedule, for a renegotiation, for example. The change is a new schedule state, not an edit of the old one. ## Correct without destroying *** Corrections preserve a consistent, auditable timeline. To undo a recorded transaction, reverse it: `POST /api/v1/loan-accounts/{id}/transactions/{transactionId}/reverse` A reversal appends a compensating transaction rather than deleting anything, so the account keeps a complete audit trail. A reversal takes a request id on the same headers as a repayment. The same id replays the reversal already recorded only when every reversal fact matches too. The reversal facts are the transaction you reverse, the loan account, the reversal effective date, the reason, the profile version, and the jurisdiction code. Any difference in those facts answers `409`. > [!WARNING] > > Servicing never edits the past. Repayments, reschedules, and reversals all append new facts. Lender re-derives the current state from that history, so the account stays explainable from its own record. ## A returned final payment becomes a receivable *** Reversing the payment that settled a **consignado** loan does not always give the debt back to the contract. The exclusion order tells the payer entity to stop deducting from payroll. Once the order has gone to the rail and the rail did not reject it, the payroll mandate is gone. Nothing in Lender registers it again. A contract revived there would be debt with no instrument behind it. It would be chased for arrears, provisioned, and offered back to the official as something whose deduction could be cancelled a second time. Lender still accepts the reversal, because the bank returned the money and the ledger has to agree with the bank. The destination changes. Lender opens a **returned-payment receivable** against the borrower, outside the credit operation, with no payroll mandate and collected by other means. The reversal answers with which of the two happened, and with the receivable's id when one opened. The contract keeps its settled state. Nothing reopens. Lender replays no schedule and undoes no allocation. > [!NOTE] > > An exclusion still in flight counts as issued. A reversal cannot wait for the rail to answer. If a revival happens before the rail answers, nothing undoes it when the answer comes back confirmed. Where the rail **rejected** the exclusion, the mandate never died, and the reversal revives the contract exactly as it always did. ### Working the receivable | Operation | Purpose | | - | - | | `GET /api/v1/returned-payment-receivables` | List receivables, narrowed by loan account, borrower, or status. | | `GET /api/v1/returned-payment-receivables/{id}` | Read one receivable. | | `POST /api/v1/returned-payment-receivables/{id}/collections` | Record that the borrower paid it. | A collection settles the whole debt, so it carries no amount: Lender reads the figure from the receivable. It takes a request id on the same headers a repayment takes. Resending the same collection under that same request id answers `200` with the collection already standing. A second, different collection against a debt already collected answers `409` with `LENDER-0325`. Lender refuses a collection with `422` and `LENDER-0326` when its collection instant is not RFC 3339, or when it has no request id. The same refusal covers a collector name longer than the field holds, or a request id already spent collecting a different debt. The receivable names the contract it came from, so you find a contract's receivables by narrowing the list to that loan account. It is not part of the contract's own servicing view, because it is no longer the contract's debt. > [!WARNING] > > Lender refuses a returned payment that carried cash the institution held unapplied, with `422` and `LENDER-0323`. That refusal is terminal by design. Lender does not record the reversal, and it leaves the contract exactly as it was. Lender carries the receivable at **face value**. The daily PDD sweep ages it along the ladder of the product it came from, and provisions the expected loss for its age. At the final stage, the sweep writes it off. A product whose chart can divert a returned payment must carry the three rules for that allowance and charge-off. Otherwise, Lender refuses the chart with `422` and `LENDER-0406`. ## What happens downstream *** Servicing emits `repayment.recorded`, `repayment_reversal.recorded`, `loan_schedule.prepayment_applied`, `loan_schedule.rescheduled`, `returned_payment_receivable.opened`, `returned_payment_receivable.collected`, and `returned_payment_receivable.written_off`. A repayment, a reversal, and a prepayment record durable posting intents. Lender relays them to the ledger only when you configure the ledger relay. Read [the posting path](/en/products/lender/lender-in-the-platform). ## Next steps *** > **[Accounting and accrual runs](/en/products/lender/accounting-and-accrual-runs)** > > Recognize interest over time and read the journal reference each run records. > **[Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack)** > > Read the PDD stage of an overdue loan and apply a stage transition. # What is Lender? Source: https://docs.lerian.studio/en/products/lender/what-is-lender Meet Lender, Lerian's credit journey engine for consumer credit (CDC), from product definition through origination, servicing, and audit. **Lender** is Lerian's credit journey engine. It gives financial institutions one product to run the full lifecycle of a credit operation. The lifecycle runs from the loan product definition, through origination and disbursement, to servicing, repayment, and audit-ready history. The journey it runs is **consumer credit (CDC)**. Lerian offers Lender to licensed institutions and maintains its repository internally. ## What a credit journey engine is *** A credit operation is a journey that spans months or years and touches many teams. A credit journey engine manages that journey end to end: * **Product definition**: Loan products with versioned terms, charges, and rate configurations. Every contract traces back to the exact product version it used at origination. * **Origination**: Application intake, approval or rejection decisions, disbursement, schedule preview, and the regulatory disclosures the jurisdiction requires. * **Servicing**: Active contract schedules, repayment recording, prepayment, rescheduling, and corrections, with reversals and replays that keep history consistent. * **Accounting**: Posting rules that map financial events onto general-ledger accounts, durable posting intents, and accrual runs that recognize interest month by month. * **Audit**: An immutable audit trail (A chronological, immutable record of every action and transaction in the system, essential for regulatory compliance and dispute resolution.) of every lifecycle event, preserved for compliance. Most institutions assemble this from separate systems: a product catalog here, an origination tool there, servicing in the core, spreadsheets in between. Lender's premise is that the journey belongs in one engine, with one consistent history. ## The domains *** Lender has eight bounded domains. Every credit journey, whatever the product, flows through the first five: | Domain | What it owns | | - | - | | **Products** | Loan products, immutable product versions, activation, charge templates, floating-rate tables, and the accounting profile that says how the product books to the ledger. | | **Origination** | The loan application lifecycle (submit, approve, reject, withdraw, disburse), plus schedule preview and decision records. | | **Servicing** | The active loan account: schedules, repayments, prepayments, reschedules, and corrections applied through reversals and replays. | | **Accounting** | Accounting profiles, posting rules, the posting intents that carry an event to the ledger, accrual runs, and the journal references that identify each run to your own accounting. | | **Audit** | The immutable audit trail of every lifecycle event on a loan account. | | **Assignment** | Assigning receivables to a fund: funds, sets, their documents, and the cession facts. | | **Issuance** | The credit instruments that evidence an application. | | **Portfolio** | The portfolio dashboard and the delinquency reads. | Start with [Core concepts](/en/products/lender/core-concepts) for the vocabulary, then follow the guides for each domain. ## Jurisdiction-aware, Brazil first *** Credit rules differ by jurisdiction, so Lender is jurisdiction-aware from the start. It has a generic base, which books no credit, and a Brazilian profile that books credit. The CDC journey maps onto that model. CDC covers fixed-schedule consumer credit, with the disclosures consumer regulation requires. The deepest journey is **consignado privado**, payroll-deducted lending in Brazil, with its own collateral model and averbação lifecycle tracking. See [Consignado privado](/en/products/lender/consignado-privado). For everything the Brazilian profile adds (CET disclosure, IOF preview, capitalization consent, PDD staging, and more), see the [Brazil regulatory pack](/en/products/lender/brazil-regulatory-pack). ## How Lender composes with the platform *** Lender is a Lerian primitive: it runs on the platform rather than beside it. * **Ledger**: Money events, such as a disbursement, a repayment, or an interest accrual, record durable posting intents. Those intents follow the accounting profiles and posting rules you configure. Lender relays them to the double-entry (Every financial movement is recorded as at least two operations: a debit from one account and a credit to another, so the system always balances.) ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.) only when you configure the ledger relay. The ledger stays the source of truth for balances. Lender stays the source of truth for the credit journey. * **Multi-tenancy**: Like every Lerian product, Lender is built for full tenant isolation from the ground up. * **Events**: Lifecycle events publish on the platform's event backbone. Downstream products and your own services can react to the credit journey as it unfolds. * **Observability**: Structured logs, traces, and metrics on the same observability stack as the rest of the platform. See [Lender in the platform](/en/products/lender/lender-in-the-platform) for the posting path, the event catalog, and tenant isolation. ## Next steps *** > **[Core concepts](/en/products/lender/core-concepts)** > > Learn the vocabulary: loan products, accounting profiles, applications, loan accounts, accrual runs, and the Brazil terms. > **[Lender in the platform](/en/products/lender/lender-in-the-platform)** > > See how Lender posts to the ledger (The core financial book that records all transactions, balances, and operations for an organization. It is the single source of truth for a business unit's finances.), emits events, and isolates tenants. > **[Originate a loan](/en/products/lender/originate-a-loan)** > > Walk an application from intake through preview, approval, and disbursement. > **[Consignado privado](/en/products/lender/consignado-privado)** > > Explore the Brazilian journey: the vocabulary, the collateral model, and averbação. If you want to talk about credit journeys on the Lerian platform, [contact us](https://lerian.studio/contact). # Matcher MCP server Source: https://docs.lerian.studio/en/products/matcher/mcp/matcher-mcp-overview Connect AI assistants to Matcher through the Model Context Protocol, a secure, token-relayed tool surface over the reconciliation engine. The **Matcher MCP server** exposes Matcher's reconciliation surface as [Model Context Protocol](https://modelcontextprotocol.io) tools. An AI assistant, or any MCP client, can then operate Matcher on your behalf. It can inspect contexts, start reconciliation runs, work exceptions, and pull reports. It has the same permissions you already have. > [!WARNING] > > Matcher is only available if you've acquired the Matcher product. To get access, [get in touch with our team](https://lerian.studio/contact). ## How it connects *** The server speaks **Streamable HTTP** and exposes one MCP endpoint (`POST /mcp`). There is no stdio transport: every client connects to it as a *remote* server over the network. Point any Streamable-HTTP MCP client at the endpoint URL Lerian provides and send your Matcher bearer token on the connection. For example, with Claude Code: ```bash theme={null} claude mcp add --transport http matcher https://your-matcher-mcp.example.com/mcp \ --header "Authorization: Bearer " ``` Any MCP client that supports Streamable HTTP works the same way: give it the URL and the `Authorization: Bearer ` header. ## Auth posture *** For Matcher API calls, the server is a **credential relay** that adds no identity of its own: * **Bearer token for API calls.** Tools that dispatch Matcher API requests fail closed without a bearer token. They forward the client-supplied credential without logging, storing, or echoing it. * **Local utilities.** `mcp_ping`, `matcher_list_operations`, and `matcher_describe_operation` run locally and need no token. `mcp_whoami` does not call Matcher. It still needs a bearer credential, because it reports that a credential arrived. Without a credential, it returns a tool error. * **Tenant follows the token for API calls.** No API-dispatching tool accepts a tenant parameter. Matcher resolves the tenant from the relayed JWT. * **No session state.** Each request builds a fresh in-memory server. To check your client configuration, call `mcp_whoami` with a bearer credential after you connect. It reports only that the credential arrived, never its value. ## What you can do with it *** The server exposes curated tool families for configuration, reconciliation runs, exceptions and disputes, ingestion, and reporting. It also exposes a generic bridge for operations with JSON request bodies. See [Matcher MCP tools](/en/products/matcher/mcp/matcher-mcp-tools) for the catalog. # Matcher MCP tools Source: https://docs.lerian.studio/en/products/matcher/mcp/matcher-mcp-tools The tool families the Matcher MCP server exposes: curated tools by category, plus generic discovery and JSON-relay tools for Matcher operations. The Matcher MCP server exposes a **curated** tool surface: ergonomic, validated tools for common operations. It also exposes a **generic discovery pair** and a **JSON relay** for indexed operations. Curated tools follow a `family_action` naming convention (for example, `context_list` or `match_run_start`), so related tools share a prefix. This page lists the families with representative examples. It is not an exhaustive catalog. Connect a client and list the available tools to see the full surface for your version. ## Curated families *** | Category | Families | What they cover | Representative tools | | - | - | - | - | | **Configuration** | `context_*`, `source_*`, `field_map_*` | Reconciliation contexts, their data sources, and the field maps that normalize incoming records. | `context_create`, `context_setup_progress`, `source_list`, `field_map_update` | | **Matching rules** | `match_rule_*` | The match rules a context applies, including their evaluation order. | `match_rule_create`, `match_rule_reorder` | | **Fees** | `fee_schedule_*`, `fee_rule_*` | Expected-fee schedules and the rules attached to them, including simulation before rollout. | `fee_schedule_simulate`, `fee_rule_create` | | **Reconciliation runs** | `match_run_*` | Driving reconciliation: start a run, follow its progress, inspect the resulting match groups. | `match_run_start`, `match_run_groups` | | **Exceptions** | `exception_*` | Working unmatched transactions: listing, history and comments, per-exception actions, and bulk operations. | `exception_list`, `exception_force_match`, `exception_bulk_resolve` | | **Disputes** | `dispute_*` | The dispute lifecycle for contested exceptions. | `dispute_submit_evidence`, `dispute_close` | | **Ingestion** | `ingestion_*` | The import lifecycle: upload data, inspect jobs and per-row errors, search and ignore transactions. | `ingestion_upload`, `ingestion_job_errors_list` | | **Reporting** | `dashboard_*`, `report_*` | Dashboard aggregates and focused slices, plus report reads, counts, and exports. | `dashboard_match_rate`, `report_summary`, `report_export_unmatched` | ## Generic trio *** Sometimes a curated tool does not cover the operation you need. Then use the generic tools to inspect the API contract that the server embeds at startup. When the operation accepts a JSON body, or needs no body, you can invoke it: | Tool | Purpose | | - | - | | `matcher_list_operations` | List the operations in the embedded OpenAPI index built when the MCP server starts; optionally filter by tag. It does not call Matcher or require a credential. | | `matcher_describe_operation` | Describe one indexed operation: its method, templated path, path and query parameters, summary, and, when present, a dereferenced JSON request-body schema. It does not include response schemas. | | `matcher_invoke` | Assemble, validate, and dispatch an indexed operation with path/query parameters and, where applicable, a JSON request body, using your relayed credentials. | `matcher_invoke` does not support multipart or raw-binary bodies. Use an applicable curated tool or call the Matcher HTTP API directly for those operations. Curated tools and `matcher_invoke` share the same HTTP-client contract, fail-closed token relay, and [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) error mapping. For Matcher API failures, both return structured tool errors that preserve the problem's status, title, detail, and code. `exception_dispatch`, `exception_bulk_dispatch`, and `matcher_invoke` accept an optional `idempotencyKey`. If you omit it on a dispatch, the server sends an `X-Idempotency` key that it derives from the request. A retry of the same dispatch carries the same key. ## Utilities *** | Tool | Purpose | | - | - | | `mcp_ping` | Confirm the server is reachable. | | `mcp_whoami` | Report whether your client's bearer credential arrived (presence only, never the value). It returns a tool error when none is present. | # Governance Source: https://docs.lerian.studio/en/products/matcher/reference/matcher-governance Manage actor PII mappings, list audit-log archives and download archive objects, and re-verify Matcher's audit hash chain. Matcher's governance surface groups three capabilities under `/v1/governance`: * **Actor mappings**: link opaque actor IDs to PII, with pseudonymize and delete operations. * **Archives**: list completed audit-log archives and download archive objects from object storage. * **Audit logs**: append-only, hash-chained history with a read-only integrity check. > [!NOTE] > > Every governance route stays inside the caller's tenant. Actor-mapping reads have two authorization tiers: the list, which omits `displayName` and `email`, and the single-record de-anonymization read. This separation keeps identity resolution apart from browse access. ## Actor mappings *** An actor mapping links an opaque `actorId` (for example `user:550e8400-e29b-41d4-a716-446655440000`) to human-readable PII (`displayName`, `email`). Matcher encrypts `displayName` and `email` before it stores them and never stores them in plaintext. List rows omit the mapping PII fields (`displayName`, `email`) by design, but they do return the `actorId` itself. On upsert, Matcher trims leading and trailing whitespace and rejects empty or whitespace-only IDs and IDs longer than 255 characters. Matcher does not impose an opaque-ID format and does not redact the value. An `actorId` that itself contains PII, such as an email address, appears in list rows as its stored value. Pseudonymization keeps that value. Use opaque identifiers if list access must stay PII-free. The single-record `GET` returns cleartext identity and requires the `deanonymize` permission. Audit logs can retain the raw `actorId`, which can be an email address. ### List actor mappings Cursor-paginated rows that omit `displayName` and `email`. Filter by an actor-ID prefix. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/actor-mappings?actorId=user:&limit=25" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "items": [ { "actorId": "user:550e8400-e29b-41d4-a716-446655440000", "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" } ], "limit": 25 } ``` Query parameters: `actorId` (prefix filter), `limit` (default 25, capped at 100), and `cursor`. ### Upsert an actor mapping Creates or updates the PII for an actor ID. `PUT` is idempotent. The same call creates the record on first use and updates it thereafter. Supply at least one of `displayName` or `email`. ```bash theme={null} curl -X PUT "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "displayName": "John Doe", "email": "john.doe@example.com" }' ``` ```json theme={null} { "actorId": "user:550e8400-e29b-41d4-a716-446655440000", "displayName": "John Doe", "email": "john.doe@example.com", "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" } ``` ### Get one actor mapping (de-anonymize) Returns the cleartext PII for a single actor ID. This is the de-anonymization primitive, so the narrower `deanonymize` permission gates it rather than plain read. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \ -H "Authorization: Bearer $TOKEN" ``` ### Pseudonymize Replaces the mapping's `displayName` and `email` with `[REDACTED]` while preserving the record and its `actorId` link. This scrubs PII from the mapping only. Immutable audit records keep the raw `actorId` from the original write, and that value can itself be an email address. Historical audit logs and archived files keep their original values. Responds `204 No Content`. ```bash theme={null} curl -X POST "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}/pseudonymize" \ -H "Authorization: Bearer $TOKEN" ``` ### Delete a mapping Permanently removes the mapping. Responds `204 No Content`. ```bash theme={null} curl -X DELETE "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \ -H "Authorization: Bearer $TOKEN" ``` > [!NOTE] > > Pseudonymize keeps the record and scrubs its PII. Delete removes the record entirely. Choose pseudonymize when you must retain the audit linkage, delete when the record itself must not persist. ## Archives *** When Lerian enables audit archival for your environment, Matcher exports each monthly audit-log partition older than the retention period that Lerian sets to object storage. It verifies the export and then drops the partition from the database. ### List archives Offset-paginated. Filter by date range. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/archives?from=2024-01-01&to=2024-03-31&limit=20&offset=0" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "partitionName": "audit_logs_2024_01", "dateRangeStart": "2024-01-01T00:00:00Z", "dateRangeEnd": "2024-02-01T00:00:00Z", "rowCount": 150000, "compressedSizeBytes": 10485760, "storageClass": "GLACIER", "checksum": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b", "status": "COMPLETE", "archivedAt": "2024-04-01T02:30:00Z" } ], "limit": 20, "hasMore": true } ``` Query parameters: `from`, `to` (`YYYY-MM-DD` or RFC 3339), `limit` (1–200, default 20), and `offset`. The endpoint lists only `COMPLETE` archives and never surfaces in-progress or failed archives. `checksum` is the hex-encoded SHA-256 of the decompressed JSONL content, so decompress a downloaded archive before you hash it. ### Download an archive Returns a presigned URL plus the checksum for integrity verification. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/archives/{id}/download" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "downloadUrl": "https://s3.amazonaws.com/bucket/archive.gz?X-Amz-Signature=...", "expiresAt": "2026-02-05T13:00:00Z", "checksum": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b" } ``` > [!NOTE] > > To download only completed archives, take archive IDs from the list endpoint, which returns `COMPLETE` archives only. ## Audit logs *** Matcher records configuration changes and exception resolutions as append-only, per-tenant audit records. It links each record into a SHA-256 hash chain (`recordHash` = `SHA-256(prevHash || canonical content)`). Use the verify endpoint below for the integrity verdict. ### List audit logs Cursor-paginated, with rich filters. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs?actor=user@example.com&action=create&entity_type=context&date_from=2025-01-01&date_to=2025-01-31&limit=20" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "tenantId": "550e8400-e29b-41d4-a716-446655440001", "entityType": "context", "entityId": "550e8400-e29b-41d4-a716-446655440002", "action": "create", "actorId": "user@example.com", "changes": { }, "truncated": false, "originalSize": 0, "createdAt": "2025-01-15T10:30:00Z", "tenantSeq": 1, "recordHash": "dd3f8a09dda3a8fdcd1e5c54ef76a9168bbabbfd92ad1dd736400d03a3f8a585", "prevHash": "0000000000000000000000000000000000000000000000000000000000000000", "hashVersion": 1 } ], "limit": 20, "hasMore": false } ``` Query parameters: `actor`, `action`, `entity_type`, `date_from`, `date_to` (`YYYY-MM-DD` or RFC 3339), `limit` (1–200, default 20), and `cursor`. `actor` filters on the record's `actorId` value. That value is the raw actor identifier from the original write, an email address in this example. Audit records store the identifier as-is. It does not have to match an actor-mapping `actorId`. When a diff is too large to keep in full, `changes` carries a truncation-marker envelope instead of the full diff, and `truncated` becomes `true` with `originalSize` reporting the pre-truncation byte size. ### Verify the audit chain Re-verifies that every inspected record matches its stored hash. Verification walks a contiguous span with a `maxRecords` bound, so `intact` speaks only for that inspected span. At the HTTP endpoint, a supplied `maxRecords` must be in the 1–10,000 range, and a value outside it returns `422`. When you omit it, Matcher uses the 10,000-record default. By default, verification starts at the lowest stored sequence. To start at a later record, send the optional `fromSeq`: the first `tenantSeq` to inspect, an integer of 1 or higher. When the first record the check reads is not sequence 1, the check trusts that record's stored link. It does not verify the records before it. The check stays strictly read-only. ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/verify?maxRecords=10000" \ -H "Authorization: Bearer $TOKEN" ``` ```json theme={null} { "intact": true, "fromSeq": 1, "verifiedCount": 1024, "truncated": false } ``` The `intact` field is `true` when the whole span stays unbroken. When the check finds a break, `firstBrokenSeq` reports the `tenantSeq` of the first failing record, and `verifiedCount` reports how many records held before it. The `truncated` field is `true` when the chain holds more records than the `maxRecords` inspection bound allowed. The response always carries `fromSeq`. ### Get one audit log ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/{id}" \ -H "Authorization: Bearer $TOKEN" ``` > [!NOTE] > > You can also list an entity's history directly with `GET /v1/governance/entities/{entityType}/{entityId}/audit-logs` (cursor-paginated), which is convenient when you already know the entity to audit. ## Response codes *** | Status | Meaning | | - | - | | `200` | Mapping, archive, or audit data returned | | `204` | Actor mapping pseudonymized or deleted | | `400` | Application-level invalid input (missing displayName/email or an invalid date) | | `403` | Missing the required permission tier (e.g. `deanonymize` for single-record PII) | | `404` | Actor mapping, archive, or audit log not found | | `422` | Request/schema validation failed (e.g. invalid email format or a constrained query value) | | `503` | Archive storage is unavailable | # Security Source: https://docs.lerian.studio/en/products/matcher/reference/matcher-security The security guarantees Matcher gives your data: audit records, encrypted actor-mapping names and emails, and verified integrations. This page lists the security guarantees Matcher gives your data. It is not a compliance certification. ## Identity and permissions *** ```bash theme={null} curl -X GET "https://api.matcher.example.com/v1/contexts" \ -H "Authorization: Bearer $TOKEN" ``` For the permission that each operation needs, see the [Matcher API reference](/en/reference/products/matcher/matcher-developer-quick-start). ## Tenant isolation *** With authentication enabled, Matcher reads `tenant_id` or `tenantId` from the token. If tenant claims are optional and absent, it uses the configured default tenant. With `AUTH_ENABLED=false`, it uses that default tenant without reading a token. ## Audit records *** Matcher records configuration changes and exception resolutions as audit records for your tenant. The records are append-only and linked in a SHA-256 hash chain. You can re-check the chain. A check inspects up to 10,000 records, by default from the lowest stored sequence, and never changes a record. To start the check at a later sequence, use the `fromSeq` parameter. When the first record the check reads is not sequence 1, the check trusts that record's stored link. It does not verify the records before it. For the audit and archive endpoints, see [Governance](/en/products/matcher/reference/matcher-governance). ## Personal data *** Matcher encrypts the display name and email of an actor mapping before it stores them. Audit records keep the raw actor identifier from the original request, which can be an email address. For details, see [Actor mappings](/en/products/matcher/reference/matcher-governance#actor-mappings). ## Credentials and inbound calls *** * Matcher stores only the SHA-256 hash of an exception-callback token or an aggregator webhook token. It shows the raw token once, when it creates it. * An exception callback must carry a valid callback token for your tenant. * Matcher accepts an aggregator webhook only on a URL that carries a valid token for your tenant, and only after it verifies the source by HMAC signature or by IP address. A webhook that fails the source check answers `401`. ## Next steps *** > **[Governance](/en/products/matcher/reference/matcher-governance)** > > Manage actor mappings, audit logs, and archives. # Aggregator connections Source: https://docs.lerian.studio/en/products/matcher/ui/aggregator-connections Connect Matcher to the Pluggy or Belvo open finance aggregators, link the account holder's account through the vendor's consent window, and set up the vendor webhook. The **Aggregators** screen in the Matcher module of Lerian Console manages your open finance aggregator connections. An aggregator connection links Matcher to Pluggy or Belvo, so that Matcher can pull account data. Data starts to arrive after the account holder completes consent in the vendor's own window. For the API workflow, see [Aggregator connections](/en/products/matcher/integrations/matcher-aggregator-connections). ## Accessing the Aggregators page *** Navigate to **Settings → Aggregators**. ## Connections table *** | Column | Description | | - | - | | **Vendor** | **Pluggy** or **Belvo** | | **Name** | The connection name | | **Base URL** | The API address of the vendor | | **Account ref** | The vendor reference of the linked account. It is empty until you link an account | | **Consent** | **Awaiting consent** or **Linked**. This column also shows the result of a connectivity check | | **Actions** | The menu of actions for the connection | The table shows 20 connections on each page. Use **Previous** and **Next** to move between pages. ## Creating a connection *** 1. Click **New connection**. The **New aggregator connection** sheet opens. 2. Fill in the fields: | Field | Description | | - | - | | **Vendor** | **Pluggy** or **Belvo** | | **Connection name** | A name for the connection. Each connection needs a different name | | **Base URL** | The API address of the vendor, for example `https://api.pluggy.ai` | | **Account reference** | The vendor reference of the account (optional). Leave it blank to link the account later with **Connect** | | **Client ID** | The client ID that the vendor gave you | | **Secret** | The secret that the vendor gave you | 3. Click **Create connection**. After the save, the **Webhook token created** dialog opens. For the next step, see [Setting up the vendor webhook](#setting-up-the-vendor-webhook). ## Linking an account *** A new connection without an account reference shows **Awaiting consent**. To link the account: 1. Open the **Actions** menu of the row and click **Connect**. The consent window of the vendor opens. 2. The account holder completes consent in the vendor window. 3. Matcher saves the account reference from the vendor. The row then shows **Linked**. If the account holder closes the window before the end, nothing changes. For a linked connection, the menu shows **Reconnect**. It runs the consent again for the same account. If Matcher cannot save the account reference, the **Finish linking this account?** dialog opens. Click **Save reference** to try again. ## Setting up the vendor webhook *** The vendor sends notifications to Matcher through a webhook. Open the **Actions** menu of a row and click **Mint webhook token**. The **Webhook token created** dialog opens. 1. Copy the **Webhook URL** from the dialog. 2. Paste the **Webhook URL** into the dashboard of the vendor. 3. Click **Done**. ## Testing a connection *** Open the **Actions** menu of a row and click **Test credentials**. Matcher runs a connectivity check, and the **Consent** column shows the result. This action shows only when Matcher can test the connection. It requires a **Pluggy** connection that shows **Linked**. ## Editing a connection *** Open the **Actions** menu of a row and click **Edit**. The **Edit aggregator connection** sheet opens. You cannot change the **Vendor**. To use another vendor, delete the connection and create a new one. Leave **Account reference** blank to keep the stored reference. Click **Save changes** to apply the change. ## Deleting a connection *** Open the **Actions** menu of a row and click **Delete**. In the **Delete connection?** dialog, click **Delete**. Matcher removes the connection, and you can use its name for a new connection. You cannot undo this action. # Context fee rules Source: https://docs.lerian.studio/en/products/matcher/ui/context-fee-rules Bind fee schedules to the transactions of a context with side and field predicates, from the Fee Rules tab in the Matcher module of Lerian Console. Use the **Fee Rules** tab of a context to decide which fee schedule applies to which transactions. A fee rule binds one fee schedule to the context. Its side and its predicates select the transactions that the schedule applies to. Matcher uses that schedule to compute the expected fee of each selected transaction. You define the fee schedules themselves in **Settings → Fee Schedules**. See [Managing fee schedules](/en/products/matcher/ui/managing-fee-schedules). ## Accessing the Fee Rules tab *** **Step 1** In the Matcher module, select **Configure → Contexts**. **Step 2** Click a context to open its detail page. **Step 3** Select the **Fee Rules** tab. The **Create rules** step of the [Context setup wizard](/en/products/matcher/ui/context-setup-wizard) also shows this tab when Matcher asks for a fee rule. ## Fee rule list *** The table lists the fee rules of the context in priority order: | Column | Description | | - | - | | **Name** | The fee rule name | | **Side** | **Left side**, **Right side**, or **Either side** | | **Priority** | The evaluation order. Matcher checks lower numbers first | | **Fee schedule** | The fee schedule that the rule applies | | **Predicates** | The number of predicates in the rule | | **Actions** | Pencil button to edit the rule and trash button to delete it | ## How Matcher picks a fee schedule *** For each transaction, Matcher checks the fee rules of the transaction's side in priority order. A rule with **Either side** applies to both sides. The first rule whose predicates all match decides the fee schedule. A rule with no predicates matches every transaction on its side. ## Creating a fee rule *** **Step 1** Click **New fee rule**. The **New fee rule** side sheet opens. **Step 2** Fill in the rule: | Field | Description | | - | - | | **Name** (required) | A name of up to 100 characters. Each fee rule of a context needs a different name | | **Side** | **Left side**, **Right side**, or **Either side** (default) | | **Priority** | A number of 0 or more. The form proposes the number after the highest priority in use. Each fee rule of a context needs a different priority | | **Fee schedule** (required) | The fee schedule to apply. The list shows each schedule with its currency | **Step 3** Add the predicates. Click **Add predicate** to add one. Click **Remove predicate** on a predicate to remove it. See [Predicates](#predicates). **Step 4** Click **Save**. ## Predicates *** A predicate tests one metadata field of the transaction. Transaction metadata holds the source columns that the field map does not map. See [Context field maps](/en/products/matcher/ui/context-field-maps). Each predicate has these fields: * **Field**: the metadata field to test. Type the column name as it appears in the source data. * **Operator**: the comparison to make. * **Value**, **Values**, or **Lower bound** and **Upper bound**: what to compare with. The form shows the fields that the operator needs. | Operator | Matches when | | - | - | | `EQUALS` | The field equals the **Value** | | `NEQ` | The field does not equal the **Value** | | `IN` | The field equals one of the **Values**. Click **Add value** to add a value | | `EXISTS` | The field is present, whatever its value | | `GT` | The field is greater than the **Value** | | `GTE` | The field is greater than or equal to the **Value** | | `LT` | The field is less than the **Value** | | `LTE` | The field is less than or equal to the **Value** | | `BETWEEN` | The field is between the **Lower bound** and the **Upper bound**, both included | `EQUALS` and `IN` compare text without regard to case. `GT`, `GTE`, `LT`, `LTE`, and `BETWEEN` need numbers. A field that is not a number never matches them. A predicate on a field that the transaction does not have never matches. ## Editing a fee rule *** Click the pencil button on a rule row. The **Edit fee rule** side sheet opens with the current values. Change the fields and click **Save**. ## Deleting a fee rule *** Click the trash button on a rule row. A **Remove the fee rule** dialog opens with the rule name. Click **Confirm** to delete the rule. # Context rules Source: https://docs.lerian.studio/en/products/matcher/ui/context-rules Create, reorder, and delete the Exact, Tolerance, Date lag, and Fuzzy match rules of a context from its Rules tab in the Matcher module. Use the **Rules** tab of a context in the Matcher module of Lerian Console to manage its match rules. The rules tell the match engine how to pair transactions across the two sides of the context. ## Accessing the Rules tab *** **Step 1** In the Matcher sidebar, go to **Configure → Contexts**. **Step 2** Click the name of a context to open its page. **Step 3** Select the **Rules** tab. ## Rules list *** The tab lists the match rules in priority order. Rules evaluate from top to bottom, and the first match wins. Each row shows the position number, the strategy badge, and a short summary of the configuration. A row can also show up and down arrows, an edit button, and a delete button. The module sets the priority of a new rule for you. The form has no priority field. To change the order, use the up and down arrows. ## Creating a rule *** **Step 1** On the **Rules** tab, click **New rule**. The **New rule** sheet opens. **Step 2** Select a **Strategy**: | Strategy | Description | | - | - | | **Exact** | Field-for-field equality | | **Tolerance** | Amount and date within a band | | **Date lag** | Settlement delay window | | **Fuzzy** | Approximate reference similarity | The fields of the strategy appear below it. See [Rule types](#rule-types). **Step 3** Optionally, click **Advanced settings** to show more fields. **Step 4** Click **Save**. > [!WARNING] > > You cannot change the strategy of a rule after you create it. To change it, delete the rule and create it again. Matcher never confirms a match from a **Fuzzy** or a **Date lag** rule automatically. These rules propose matches for review. ## Rule types *** ### Exact Matches transactions when the selected fields are equal. Top-level switches: | Field | Description | | - | - | | **Match amount** | Compare the amounts | | **Match currency** | Require the same currency | | **Match date** | Compare the dates | | **Match reference** | Compare the references | **Advanced settings** adds: | Field | Description | | - | - | | **Date precision** | `DAY` or `TIMESTAMP` | | **Compare references ignoring case** | Ignore letter case when you compare references | | **Reference must be present** | When **Match reference** is on, refuse a pair if either reference is blank | | **Sign mode** | `same` matches like signs. `opposite` matches a refund against its charge. `ignore` compares magnitudes only | | **Match base amount** | Also match on the base amount, which is the amount converted to the base currency | | **Match base currency** | Also match on the base currency | | **Allocation (1:N and N:1)** | Partial allocation settings. See [Allocation settings](#allocation-settings) | | **Composite key fields** | Extra named fields to match on, together with the switches above. Click **Add key field** to add one | ### Tolerance Matches transactions when the amounts and dates are within a band. Top-level fields: | Field | Description | | - | - | | **Absolute tolerance** | The largest amount difference that still matches. The amount band uses the larger of this value and the percent tolerance | | **Percent tolerance** | A fraction: `0.005` is 0.5%. The amount band uses the larger of this value and the absolute tolerance | | **Date window (days)** | How many days apart the two sides can be dated, from 0 to 3650 | | **Match currency** | Require the same currency | > [!NOTE] > > Both tolerances start at `0`. A value of `0` is valid. **Advanced settings** adds: | Field | Description | | - | - | | **Rounding mode** | How amounts round before Matcher compares them: `HALF_UP`, `BANKERS`, `FLOOR`, `CEIL`, or `TRUNCATE` | | **Rounding scale** | Decimal places for rounding, from 0 to 10 | | **Percentage base** | The amount that the percent tolerance applies to: `MAX`, `MIN`, `AVERAGE`, `LEFT`, or `RIGHT` | | **Match reference** | Compare the references | | **Compare references ignoring case** | Ignore letter case when you compare references | | **Reference must be present** | When **Match reference** is on, refuse a pair if either reference is blank | | **Sign mode** | `same`, `opposite`, or `ignore`, as for Exact | | **Match base amount** | Apply the amount band to the base amounts, which are the amounts converted to the base currency | | **Match base currency** | Also require the same base currency | | **FX variance band** | Extra cross-currency tolerance as a fraction: `0.0025` is 25 basis points. A difference inside it still matches and opens an FX rate variance exception | | **Lockbox deduction band** | How much a payment set can short-pay an invoice set in N:M matching, as a fraction of the invoice face: `0.05` is 5%. The shortfall stays open | | **Business days and timezone** | The **Holiday calendar** and the **Timezone** for date comparison. The timezone starts at UTC | | **Allocation (1:N and N:1)** | Partial allocation settings. See [Allocation settings](#allocation-settings) | | **Composite key fields** | Extra named fields to match on, as for Exact | ### Date lag Matches transactions that occur within a set number of days of each other. Top-level fields: | Field | Description | | - | - | | **Min days** | The smallest day lag that matches, from 0 to 3650. It starts at `0` | | **Max days** | The largest day lag that matches, from 0 to 3650. It starts at `0` | | **Expected order** | Which side you expect to have the earlier date: `ABS` (either side), `LEFT_BEFORE_RIGHT`, or `RIGHT_BEFORE_LEFT` | | **Include the min and max days** | Include the boundary days in the lag window | > [!NOTE] > > **Max days** must be equal to or greater than **Min days**. If you turn off **Include the min and max days**, **Min days** must be more than `0`. **Advanced settings** adds: | Field | Description | | - | - | | **Fee tolerance** | The largest amount difference that still matches. It starts at `0` | | **Match currency** | Require the same currency | | **Match reference** | Off by default. Turn it on to require equal references | | **Compare references ignoring case** | Ignore letter case when you compare references | | **Reference must be present** | When **Match reference** is on, refuse a pair if either reference is blank | | **Business days and timezone** | The **Holiday calendar**, the **Timezone**, and **Count business days only**. **Count business days only** measures the lag in business days and needs a calendar | | **Allocation (1:N and N:1)** | Partial allocation settings. See [Allocation settings](#allocation-settings) | ### Fuzzy Matches transactions by approximate reference similarity, with optional checks on amount, currency, and date. Top-level fields: | Field | Description | | - | - | | **Minimum similarity** | A ratio from 0 to 1: `0.80` is 80% similar. References below it never match. At or above it, Matcher grades the pair into a confidence score. See [Confidence scoring](/en/products/matcher/reference/matcher-confidence-scoring). It starts at `0.80` | | **Match amount** | Compare the amounts | | **Match currency** | Require the same currency | | **Match date** | Compare the dates | **Advanced settings** adds: | Field | Description | | - | - | | **Date precision** | `DAY` or `TIMESTAMP` | | **Reference must be present** | On by default. Refuse a pair if either reference is blank | | **Sign mode** | `same`, `opposite`, or `ignore` | | **Allocation (1:N and N:1)** | Partial allocation settings. See [Allocation settings](#allocation-settings) | ## Allocation settings *** Each strategy has an **Allocation (1:N and N:1)** block in **Advanced settings**: | Field | Description | | - | - | | **Allow partial allocation** | Split one amount across several counterparts. A remainder stays open as an open item | | **Fee-aware split** | Allocate the gross share of each leg (net plus fee) instead of its net amount | | **Allocation direction** | `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT` | | **Allocation tolerance mode** | `ABS` or `PERCENT` | | **Allocation tolerance** | The remainder that a split can leave: an amount in `ABS` mode, or a fraction in `PERCENT` mode (`0.01` is 1%) | | **Allocate on the base amount** | Turn it on to allocate on the base amount, which is the amount converted to the base currency. On Exact and Tolerance rules, Matcher then also matches on the base amount and the base currency | ## Previewing a rule *** On an **Active** context, the rule sheet also shows a **Preview matches** panel. Use it to test the rule before you save it. **Step 1** Fill in the rule fields. **Step 2** In **Preview matches**, click **Run preview**. The panel shows how many pairs **Would match**, the **Unmatched left** and **Unmatched right** counts, and up to 25 would-match pairs. The preview forms 1:1 pairs only. It does not apply fee normalization, FX variance bands, or allocation, so a live run can match more. The preview saves nothing. When you change a field, run the preview again. ## Editing a rule *** Click the edit button on a rule. The **Edit rule** sheet opens. The **Strategy** field stays locked. Change the other fields and click **Save**. ## Reordering rules *** Use the up and down arrows on a row to move the rule. Rules evaluate from top to bottom, and the first match wins. ## Deleting a rule *** Click the delete button on a rule. A dialog names the rule type and its priority and asks you to confirm. Click **Confirm**. The other rules keep their order. # Context schedules Source: https://docs.lerian.studio/en/products/matcher/ui/context-schedules Run matching automatically by adding cron-based matching schedules to a context from its Matching schedules tab in the Matcher module. Use the **Matching schedules** tab of a context in the Matcher module of Lerian Console to run matching automatically. Each matching schedule sets when Matcher runs matching for the context. > [!NOTE] > > Matching schedules set when automated matching runs. They do not pull data. To schedule when a source pulls data, use the [ingestion schedules](/en/products/matcher/ui/context-sources#managing-ingestion-schedules) of the source. ## Accessing schedules *** **Step 1** In the Matcher sidebar, go to **Configure → Contexts**. **Step 2** Click the name of a context to open its page. **Step 3** Select the **Matching schedules** tab. > [!NOTE] > > Scheduled matching runs only while the context is **Active**. ## Schedule list *** The tab lists the matching schedules of the context. Each row shows: * The cron expression * An **Enabled** or **Disabled** badge * The **Last run** and **Next run** times in UTC. An empty time shows `—` * An edit (pencil) button and a delete (trash) button ## Creating a schedule *** **Step 1** On the **Matching schedules** tab, click **Add matching schedule**. The **Add matching schedule** sheet opens. **Step 2** Type the schedule in **Cron expression (UTC)**, up to 100 characters. The placeholder is `0 2 * * *`. As you type, a preview appears below the field. A common expression shows a phrase such as "Runs every day at 02:00 (UTC)". An uncommon expression shows a generated phrase or "Custom schedule". An expression with the wrong shape shows "Matching schedules take a 5-field cron expression, such as 0 2 \* \* \*." The preview does not block **Save**. Matcher checks the expression when you save. **Step 3** Leave the **Enabled** switch on to create an active schedule. Turn it off to create a disabled schedule. A disabled schedule has no next run time, so it does not run. **Step 4** Click **Save**. > **How to write a schedule expression** > > Matcher reads a **cron expression** in UTC. It has five fields, separated by spaces: `minute hour day-of-month month day-of-week`. > > | Field | Range | > | - | - | > | Minute | `0–59` | > | Hour | `0–23` | > | Day of month | `1–31` | > | Month | `1–12` | > | Day of week | `0–7` (0 and 7 are Sunday) | > > Special characters: > > * `*`: any value > * `,`: a list of values, such as `1,15,30` > * `-`: a range, such as `9-17` > * `/`: a step, such as `*/15` for every 15 units > > Examples: > > * `0 6 * * *`: every day at 06:00 UTC > * `*/15 * * * *`: every 15 minutes > * `0 9 * * 1-5`: at 09:00 UTC on weekdays > > Matcher does not accept `@every` or other named macros. When you restrict both day-of-month and day-of-week, a match in either field starts the schedule. Use an interval of five minutes or more. Matcher refuses `* * * * *` and `*/2 * * * *`. ## Editing a schedule *** Click the pencil button on a schedule row. The **Edit matching schedule** sheet opens with the current **Cron expression (UTC)** and **Enabled** values. Change the fields and click **Save**. For an enabled schedule, Matcher calculates **Next run** again from the current UTC time. When you disable a schedule, it loses its next run time. ## Deleting a schedule *** Click the trash button on a schedule row. A dialog names the schedule and asks you to confirm. Click **Confirm**. > [!WARNING] > > You cannot undo a delete. The schedule does not run again. # Discovery connections Source: https://docs.lerian.studio/en/products/matcher/ui/discovery-connections Connect Matcher to external databases and acquirer APIs, test each connection, browse the tables it exposes, and extract records into Matcher. The **Discovery** screen in the Matcher module of Lerian Console manages your discovery connections. A discovery connection links Matcher to an external database or to an acquirer REST API. Matcher reads the tables and columns of the connection. An extraction then pulls records from the tables that you choose, and Matcher ingests them. For the API workflow, see [Discovery](/en/products/matcher/integrations/matcher-discovery). ## Accessing the Discovery page *** Navigate to **Settings → Discovery**. The page header has two buttons: * **New connection** opens the sheet to create a connection. * **Refresh discovery** syncs all connections again. A message shows the number of synced connections. Chips above the table show the number of connections and the health of the extraction engine. ## Connections table *** | Column | Description | | - | - | | **Name** | The connection name | | **Type** | The connector type, such as `POSTGRESQL` or `STRIPE` | | **Status** | **Available**, **Unreachable**, or **Unknown**. The result of a test that you run on this page also shows here | | **Schema** | **Discovered** when Matcher has read the tables of the connection, or **Pending** | | **Actions** | The menu of actions for the connection | ## Creating a connection *** 1. Click **New connection**. The **New connection** sheet opens. 2. Enter the **Connection name**. 3. Select the **Connector type**. The fields below the select change with the type. 4. Fill in the fields of the type. 5. Click **Save**. ### Database connector types The database types are **PostgreSQL**, **MySQL**, **Oracle**, **SQL Server**, and **MongoDB**. | Field | Description | | - | - | | **Host** | The host name or address of the database server | | **Port** | The port of the database server | | **Database name** | The name of the database | | **User name** | The database user | | **Password** | The password of the database user (optional) | | **Schema** | The schema in the database (optional) | ### REST connector types Each REST type shows its own fields. All REST types also show an optional **Base URL** field, which sets the API address for this connection. | Connector type | Fields | | - | - | | `PAGBANK` | **Establishment ID** and **EDI token** | | `STRIPE` | **API key** | | `PIX_BCB` | **Client ID**, **Client secret**, **Client certificate (PEM)**, and **Client private key (PEM)** | ## Editing a connection *** Open the **Actions** menu of a row and click **Edit**. The **Edit connection** sheet opens. * For a database type, you can change the name and the database fields. * For a REST type, you can change only the **Connection name**. To change other values, delete the connection and create it again. Click **Save** to apply the change. ## Testing a connection *** Open the **Actions** menu of a row and click **Test connection**. The **Status** column shows the result: * **Connection healthy** with the response time in milliseconds. * **Connection failed**, with the reason when Matcher returns one. ## Viewing the schema *** Open the **Actions** menu of a row and click **View schema**. The **Connection schema** sheet lists each table of the connection. For each table, it shows the **Column**, **Type**, and **Nullable** values. If the sheet shows **No schema discovered yet**, click **Refresh discovery** when the connection is reachable. The sheet can also show these markers: | Marker | Meaning | | - | - | | **Last known map** | The connection is unreachable, or the tables are an archived version. The sheet shows the tables that Matcher read last, with the read date | | **Not extractable yet** | Matcher has no confirmed columns for the tables. You cannot extract from them yet. The alert describes the possible causes | | **Columns may be stale** | The columns of the table can be out of date | | **Fewer columns than last read** | The last read returned fewer columns for the table than the read before it | The **Earlier versions** section lists the archived versions of the schema. Each version shows the cause, the archive date, and the number of tables. Click a version to see its tables. ## Starting an extraction *** 1. Open the **Actions** menu of a row and click **Start extraction**. The **Start extraction** sheet opens. 2. In **Tables JSON**, enter a JSON object. Each key is a table name, exactly as the schema lists it. Use `{}` as the value to extract all columns. To extract only some columns, use `{"columns": ["id", "amount"]}`. 3. Click **Start extraction**. The sheet then shows the extraction status, such as **Extracting** or **Complete**. When Matcher sends the records to ingestion, the sheet also shows the **Ingestion job**. If the extraction fails, the sheet shows the error. Click **Run in background** to close the sheet while the extraction runs. A source can also pull data on a schedule from a discovery connection. See [Context sources](/en/products/matcher/ui/context-sources). ## Deleting a connection *** Open the **Actions** menu of a row and click **Delete**. In the **Delete discovery connection?** dialog, click **Delete connection**. Schema reads and extractions that depend on the connection can stop. # Exception details Source: https://docs.lerian.studio/en/products/matcher/ui/exception-details Open one reconciliation exception in the Matcher module of Lerian Console: review its transaction, pair a suggested counterpart, resolve or escalate it, and follow its comments and history. The **Exception** page in the Matcher module of Lerian Console shows one exception. Use it to review the transaction and pair it with a counterpart. You can also resolve or escalate the exception and follow its history. ## Opening an exception *** On the [Exceptions](/en/products/matcher/ui/managing-exceptions) queue, click the **Reason** link of an exception row. On a dispute page, the **Exception** link also opens the exception. ## Page layout *** The page title is **Exception**. A breadcrumb above the title links back to the **Exceptions** queue. Below the title, you see the exception ID, its severity and status badges, and the time of its last update. The header can show these controls: * **View audit trail** opens the **Audit trail** panel. It lists the governance audit events of the exception, newest first. * **Resolve** opens the **Resolve exception** dialog. * **More actions** opens a menu that can hold **Assign**, **Open dispute**, **Dispatch**, and **Adjust entry**. The **Force match** item comes last, under a separator. **Assign** is in the menu only while the exception is **Open**. An exception in **Resolved** or **Pending Resolution** status has no **Resolve** button and no **More actions** menu. The **Pending Resolution** status means that a force match or an adjustment is in progress. The page shows the **Transaction**, **Information**, **Comments**, and **History** panels. Other panels depend on the status of the exception. ## Panels *** ### Transaction The **Transaction** panel shows the transaction ID, **Amount**, **Transaction date**, and **Source**. If the transaction or its source no longer exists, **Amount**, **Transaction date**, and **Source** show a dash. For a **Fee Variance** exception, the panel can also show three settlement rows: | Row | Description | | - | - | | **Expected** | The amount that the transaction should credit under the expected fee. | | **Credited** | The amount that the transaction credited. | | **Difference** | **Credited** minus **Expected**. A negative value means the credit fell short. | ### Information | Field | Description | | - | - | | **Reason** | Why Matcher raised the exception. | | **Originating run** | The latest reconciliation run that surfaced the exception, as a link. | | **Assigned to** | The assignee, when one is set. | | **Due date** | The resolution deadline, when one is set. An overdue date on an **Open** or **Assigned** exception shows in red. | | **External system** | The external system that reported on the exception, when one did. | | **External issue** | The issue ID in that external system. | | **Created** | When Matcher created the exception. | ### Suggested counterparts For an **Open** or **Assigned** exception, the **Suggested counterparts** panel can list possible matches from the opposite side. The panel uses the context of the exception, not the **Active context**. Each candidate shows a score with a **Strong**, **Plausible**, or **Weak** band. It also shows the amount and date differences and why it scored that way. The ranking uses raw amounts. Fee normalization and FX bands do not apply here. To pair a candidate with the exception transaction: **1. Pick the candidate** Click **Match to this** on the candidate. The **Pair these transactions?** dialog opens with the two transactions side by side. **2. Add a note (optional)** Type a reason in **Note (optional)**. **3. Confirm** Click **Pair selected**. Matcher links the two transactions into a confirmed match group. > [!NOTE] > > Pairing a counterpart does not resolve the exception. Resolve the exception afterwards to close it. ### Resolution A **Resolved** or **Pending Resolution** exception shows the **Resolution** panel instead of **Suggested counterparts**. It shows **Resolution type**, **Reason**, **Resolution notes**, **Resolved by**, and **Resolved at**. ### Comments The **Comments** panel lists comments oldest first, with the author and a relative time. To add a comment, type it and click **Send**, or press Ctrl+Enter or Cmd+Enter. A comment can have up to 5,000 characters. A comment can show a delete icon. Click it and confirm in **Delete this comment?** to remove the comment permanently. A **Resolved** or **Pending Resolution** exception takes no new comments. ### History The **History** panel lists what happened to the exception, newest first. Each entry shows the action, the actor or `system`, and a relative time. A new entry can take a few seconds to appear. ## Actions *** ### Resolve **Resolve** opens the **Resolve exception** dialog. It closes the exception with a resolution text. Matcher marks the exception **Resolved** and does not change the transaction. | Field | Description | | - | - | | **Resolution** | Required. How you resolved the exception, up to 255 characters. | | **Reason** | Optional. More context for the audit trail, up to 1,000 characters. | Click **Resolve** to submit. ### Assign The **Assign exception** dialog has one field, **Assignee**. Type who works the exception, up to 255 characters, and click **Assign**. The status changes to **Assigned**. ### Open dispute The **Open dispute** dialog starts a formal dispute about the exception. | Field | Description | | - | - | | **Category** | Required. Bank fee error, Unrecognized charge, Duplicate transaction, Amount mismatch, or Other. | | **Description** | Required. The basis for the dispute, up to 5,000 characters. | Click **Open dispute**. The new dispute starts in the **Open** state. You add evidence and record the outcome on the dispute page. See [Managing disputes](/en/products/matcher/ui/managing-disputes). ### Dispatch The **Dispatch to an external system** dialog records a hand-off of the exception. | Field | Description | | - | - | | **Target system** | Required. Select **Manual**. | | **Queue or project** | Optional. A label for the team that takes the exception, up to 255 characters. | Click **Dispatch**. Matcher adds the hand-off to the exception history. The exception status does not change. ### Adjust entry **Adjust entry** records a correction for the exception transaction and resolves the exception. | Field | Description | | - | - | | **Reason code** | Required. Amount correction, Currency correction, Date correction, or Other. | | **Amount** | Required. A positive decimal amount, for example `150.50`. | | **Currency** | Required. A three-letter currency code, for example `BRL`. | | **Effective date** | Required. The dialog fills in today's date from your computer's calendar. | | **Notes** | Required. An explanation of the correction, up to 1,000 characters. | Click **Adjust entry**. Matcher records the adjustment for the transaction and marks the exception **Resolved**. ### Force match **Force match** overrides the matching engine. Matcher marks the exception transaction as matched and resolves the exception. The transaction does not join a match group. | Field | Description | | - | - | | **Override reason** | Required. Policy exception, Operations approval, Customer dispute, or Data correction. | | **Justification** | Required. Why you force the match, up to 1,000 characters. | Click **Force match** to submit. # Running reconciliations Source: https://docs.lerian.studio/en/products/matcher/ui/executing-reconciliation Start match runs for the active Matcher context from the Reconciliations screen: preview with a dry run or persist with a commit run, then read the results and the run history. The **Reconciliations** screen of the Matcher module in Lerian Console starts match runs for the active context. It also lists the runs of that context. Use a dry run to test your rules and read the statistics without writing results. Use a commit run to persist matches and exceptions. ## Accessing the Reconciliations screen *** Click **Operate → Reconciliations** in the Matcher sidebar. The screen works on the **Active context** that you select in the Matcher sidebar. Badges under the header identify the context. The screen has two parts: the **Run match** panel and the **Run history** table. If no context exists, the screen asks you to create one and shows a **Browse contexts** button. ## Importing data first *** A run matches only the transactions that you imported into the context. To upload a file, use **Operate → New Reconciliation**. See [Uploading reconciliation data](/en/products/matcher/ui/uploading-reconciliation-data). > [!NOTE] > > When the panel finds no imported transactions in the context, it shows the warning **This context has no transactions to match**. The warning has an **Import data** button, and you cannot click **Run match**. ## Run modes *** Choose a mode in the **Mode** list before you start: | Mode | Description | | - | - | | **Commit: persist matches and exceptions** | Writes match groups and exceptions to the context. This is the default mode. | | **Dry run: preview, write nothing** | Evaluates your rules and shows the statistics. It writes no match groups and no exceptions. Use it to test rules before a commit run. | ## Starting a run *** Click **Run match**. The engine runs the match while the request is open, so the button shows **Starting…** until the run ends. You cannot change the mode during a run. Below the controls, a progress block shows the run ID, its status, its mode, and the **Started** and **Finished** times. If Matcher refuses to start the run, the message **The run could not start** shows the reason from Matcher. A run that fails shows its failure reason below the status. ### Following a run in progress While a run is in progress, the panel refreshes every two seconds. It shows a **Stop watching** button. The button stops the refresh in your browser. The run continues in Matcher. After three minutes, the panel stops the refresh and says that the run takes longer than expected. Click **Recheck status** to refresh again. The run detail page refreshes in the same way. ## Reviewing results *** When a run completes with transactions to evaluate, a bar shows the matched share of the run as a percentage. Below the bar, the **Matches** figure gives the number of match groups. A list of statistics per side follows: | Statistic | Description | | - | - | | **Candidates (left)** / **Candidates (right)** | Transactions the run evaluated on each side. | | **Auto-matched (left)** / **Auto-matched (right)** | Transactions that the run matched automatically. | | **Pending review (left)** / **Pending review (right)** | Transactions in matches that wait for manual review. | | **Proposed (left)** / **Proposed (right)** | Transactions in proposed matches that nobody confirmed yet. | | **Unmatched (left)** / **Unmatched (right)** / **Unmatched (external)** | Transactions that the run could not match. | | **Exceptions raised** / **Exceptions updated** | New exceptions from the run, and existing exceptions that the run changed. Both are zero for a dry run. | Below the statistics, a line confirms the mode. For a commit run, it says that Matcher persisted the matches and exceptions to the context. If the run completes with no transactions on either side, the panel shows no statistics. It shows a warning that no transactions matched, with an **Import data** button. ## Viewing match groups *** After a run completes, click **View match groups** to open the run detail page. The page shows the run summary and the match groups of the run. A dry run writes no match groups, so its detail page lists no groups. See [Managing matches](/en/products/matcher/ui/managing-matches). ## Run history *** Below the panel, the **Run history** table lists the runs of the context, newest first. Click a run ID to open its detail page. Click **View exceptions** on a row to open the exceptions queue for that run. See [Managing exceptions](/en/products/matcher/ui/managing-exceptions). While a run on the page is in progress, the table refreshes and shows a **Live** badge. For the columns and the run statuses, see [Operational history](/en/products/matcher/ui/operational-history). If the context has no runs, the table shows a **Run reconciliation** button. The button takes you to the **Run match** panel. # Extraction review Source: https://docs.lerian.studio/en/products/matcher/ui/extraction-review Approve or reject the transactions that Matcher extracts from a PDF document before they enter a source, in the Extraction Review queue of the Matcher module. Matcher can read a PDF document, such as a bank statement, and propose the transactions it finds. These proposals are candidates. A candidate does not enter reconciliation until a person approves it. The **Extraction Review** screen of the Matcher module in Lerian Console is the queue where you approve or reject them. > [!NOTE] > > Document extraction is not available in every environment. When it is off, the screen shows a message instead of the queue. ## How a review starts *** **1. Extract from a document** In the **Sources** tab of a context, use the **Extract from a document** action of a source. Upload one PDF document. See [Context sources](/en/products/matcher/ui/context-sources). **2. Matcher reads the document** Matcher uses AI to read the document and propose candidate transactions. Matcher sends a capped slice of the document's text layer to the language-model provider Lerian configures for your environment. When the PDF has no text layer, Matcher sends the PDF itself. Matcher queues the candidates as one review with the **Pending review** status. **3. Open the review** The dialog shows the number of candidates and an **Open review** link. You can also open the review later from the queue. ## Accessing the Extraction Review screen *** Click **Resolve → Extraction Review** in the Matcher sidebar. The screen shows the **Active context** that you select in the Matcher sidebar. To see another context, change the **Active context**. ## The review queue *** The queue shows the reviews of the context, 25 per page. Use **Previous** and **Next** to move between pages. The **Status** filter shows **Pending review** reviews by default. You can also show **Approved** or **Rejected** reviews. | Column | Description | | - | - | | **Document / source** | The short ID of the source that the document belongs to. Click it to open the review. | | **Status** | The review status. | | **Candidates** | The number of candidate transactions in the review. | | **Created At** | When Matcher created the review. | ## Reading a review *** The review page shows each candidate as a card, **Candidate 1**, **Candidate 2**, and so on. A badge on the card shows how Matcher read it: * **Text layer**: the candidates come from the text layer of the PDF. * **Vision · lower trust**: an AI model produced the values from the document itself. Check these candidates with more care. Each field of a candidate shows the value that Matcher read and an **Extraction confidence** meter. When known, it also shows the page and region of the value. You cannot edit a candidate. You approve or reject the review as a whole. To go back to the queue, click **Queue**. ## Approving a review *** 1. On a **Pending review** review, click **Approve extraction**. 2. In the **Approve extracted transactions?** dialog, click **Approve and commit**. Matcher sends all the candidates of the review to its source as one import, the same way it imports a file. After a successful approval, the review status is **Approved** and the review shows the ID of the import job. The import job also shows in the **Import jobs** table of **Operate → Operational History**. See [Operational history](/en/products/matcher/ui/operational-history). If the context has **Auto-match on upload** turned on, the import starts a match run. You cannot undo an approval. ## Rejecting a review *** 1. On a **Pending review** review, click **Reject extraction**. 2. You can enter a **Rejection reason (optional)**, up to 1,000 characters. 3. Click **Reject extraction**. Matcher discards the candidates. Nothing enters the source. The review status changes to **Rejected**. If you entered a reason, the review page shows it. > [!TIP] > > If another person decides on the review first, the screen reloads the review and shows its new status. For the API of this workflow, see [Extraction reviews](/en/products/matcher/imports/matcher-extraction-reviews). # Layout templates Source: https://docs.lerian.studio/en/products/matcher/ui/layout-templates Define fixed-width file layouts, such as CNAB layouts, as record types and byte ranges, then select them as the file format when you upload data. The **Layout Templates** screen in the Matcher module of Lerian Console holds your fixed-width file layouts. In a fixed-width file, such as a CNAB file, each line is a record with a fixed length. Each field is a byte range in the record. A layout template tells Matcher where each field starts and how long it is. ## Accessing the Layout Templates page *** Navigate to **Settings → Layout Templates**. ## Templates table *** | Column | Description | | - | - | | **Format key** | The key of the template. It combines the region, the family, and the variant. Click it to open the template | | **Variant key** | The variant of the template | | **Record types** | The number of record types in the layout | | **Created At** | The date and time of the template creation | ## Creating a template *** Click **New template**. The **New layout template** page opens. ### Template fields | Field | Description | | - | - | | **Region** | A two-letter region code, such as `BR`, or `XX` for no region | | **Family** | The format family, such as `cnab240` or `cnab400` | | **Variant** | A lowercase name for the bank or brand layout. Use letters, digits, `-`, and `_` | | **Discriminator start byte** | The zero-based position of the bytes that identify the record type | | **Discriminator length** | The number of bytes that identify the record type | | **Required field names (optional)** | Field names that the layout must declare. Separate the names with commas | ### Record types Each record type is a card. Click **Add record type** to add a card. Click **Remove** to delete a card. | Field | Description | | - | - | | **Record-type value** | The value in the discriminator bytes that identifies this record type | | **Record width (bytes)** | The fixed length of a record of this type | Each record type has one or more fields. Click **Add a field** to add a field row: | Field | Description | | - | - | | **Name** | The field name | | **Start byte** | The zero-based position where the field starts | | **Length** | The number of bytes in the field | | **Kind** | `string`, `decimal (money)`, or `date`. A field named `amount` must use `decimal (money)` | Matcher maps fields named `external_id`, `amount`, `currency`, `date`, and `description` to the transaction fields with the same names. It keeps all other fields as transaction metadata. If a layout has no `currency` field, Matcher uses `BRL`. The form shows **Byte-layout problems** when two fields in a record type overlap. It also shows them when a field ends after the record width. Matcher does not save a layout with these problems. Click **Submit template** to save. The template page opens. ## Using a template *** To use a template, upload a file on **Operate → New Reconciliation**. In the **Format** select, choose the format key of the template. The select lists your templates in the **Layout Templates** group. For the upload steps, see [Uploading reconciliation data](/en/products/matcher/ui/uploading-reconciliation-data). ## Template details *** Click a format key in the table to open the template. The page shows two cards: * **Template identity**: the **Format key**, **Region**, **Family**, and **Variant**. * **Registration**: the **Variant key**, the number of **Record types**, the **Discriminator** byte range, and the creation date. The **Byte layout** section has one card for each record type. Each card shows the record width and a table of fields with **Name**, **Byte range**, **Length**, and **Kind**. ## Editing a template *** Click **Edit** on the template page. The **Edit layout template** page opens with the current values. Click **Save changes** to replace the stored layout. This change does not create a copy. Matcher validates the full layout again before it saves the change. ## Duplicating a template *** Click **Duplicate as new** on the template page. The **New layout template** page opens with the values of the template. Change the values, then click **Submit template** to save a separate template. ## Deleting a template *** Click **Delete** on the template page. In the **Delete layout template?** dialog, click **Delete**. Matcher removes the template and frees its format key for a new template. You cannot undo this action. # Managing actor mappings Source: https://docs.lerian.studio/en/products/matcher/ui/managing-actor-mappings Link opaque actor IDs to a display name and an email in the Matcher module, then pseudonymize or delete the mappings. The **Actor Mappings** page of the Matcher module in Lerian Console links an opaque actor ID to a display name, an email, or both. The [audit logs](/en/products/matcher/ui/managing-audit-logs) show actor IDs, not names. You can also pseudonymize or delete a mapping on this page. ## Opening the Actor Mappings page *** In the Matcher module, go to **Settings → Actor Mappings**. ## Browsing actor mappings *** The table shows up to 20 mappings per page. It has these columns: | Column | Description | | - | - | | **Actor ID** | The opaque identifier that Matcher records | | **Created At** | The date and time when the mapping was created | | **Updated At** | The date and time of the last change | | **Actions** | The row actions menu | The table does not show display names or emails. Type in the **Actor ID prefix** field to filter the table. The table updates as you type. It shows the mappings whose actor ID starts with your text. Use **Previous** and **Next** below the table to move between pages. ## Creating an actor mapping *** 1. Click **New mapping**. The **New actor mapping** sheet opens. 2. Fill in the fields. 3. Click **Save**. | Field | Description | | - | - | | **Actor ID** | The opaque identifier. You cannot change it after you save the mapping | | **Display name** | The name to show for this actor (for example, "John Doe") | | **E-mail** | The email address of this actor. It must be a valid email address | You must fill in **Display name**, **E-mail**, or both. ## Editing an actor mapping *** 1. Open the row actions menu and click **Edit**. The **Edit actor mapping** sheet opens with the current values. 2. Change **Display name**, **E-mail**, or both. The **Actor ID** field is locked. 3. Click **Save**. If you clear a field, Matcher keeps its stored value. To erase the display name and email, pseudonymize the mapping. ## Pseudonymizing an actor mapping *** 1. Open the row actions menu and click **Pseudonymize**. The **Pseudonymize actor mapping?** dialog opens. 2. Click **Pseudonymize**. Matcher replaces the display name and the email with `[REDACTED]`. The mapping and its actor ID stay. You cannot restore the original values. ## Deleting an actor mapping *** 1. Open the row actions menu and click **Delete**. The **Delete actor mapping?** dialog opens. 2. Click **Delete**. > [!NOTE] > > Deletion permanently removes the mapping between the actor ID and its display name and email. # Archives Source: https://docs.lerian.studio/en/products/matcher/ui/managing-archives Browse the completed audit-log archives in the Matcher module, filter them by the dates they cover, and download them. The **Archives** page of the Matcher module in Lerian Console lists the completed audit-log archives. Matcher writes an archive when it moves old audit-log records out of the live database. Use this page to find and download those archives. ## Opening the Archives page *** In the Matcher module, go to **Reporting → Archives**. ## Filters *** Open the filter panel from the toolbar above the table. Use the two dates to find the archives whose entire covered period is inside a window. An archive whose covered period is partly outside the window does not appear in the list. | Filter | Description | | - | - | | **Covered from** | The covered period of the archive starts on or after this date | | **Covered to** | The covered period of the archive ends on or before this date. The window includes this whole day | The list updates when you change a date. You do not need to click a button to apply the filter. * If you leave a date empty, that side of the window has no limit. * If **Covered from** is after **Covered to**, the Console swaps the two dates. * A new window returns the list to the first page. * Click **Clear filters** in the toolbar to go back to the default window. > [!NOTE] > > When the page opens, the window ends on the current UTC date and starts 90 days earlier. ## Archive table *** The table lists only **Complete** archives. It has these columns: | Column | Description | | - | - | | **Partition** | The name of the archived database partition | | **Date range** | The start and end dates of the covered period | | **Rows** | The number of audit-log rows in the archive | | **Size** | The compressed size of the archive | | **Storage** | The object-storage class of the archive | | **Status** | **Complete** for every archive in the list | | **Archived** | When the archival finished | | **Actions** | The **Download** button | The page shows 10 archives at a time by default. Use the **Items per page** selector in the toolbar to change this number. Use **Previous** and **Next** below the table to move between pages. ## Downloading an archive *** Click **Download** on the row of the archive. The Console prepares the file and starts the download. Then the checksum appears under the button, as **Checksum:** followed by the value. The checksum is the hex-encoded SHA-256 hash of the decompressed content. Decompress the downloaded archive before you calculate its hash. If the Console cannot prepare the file, the row shows **Could not prepare archive download**. # Audit logs Source: https://docs.lerian.studio/en/products/matcher/ui/managing-audit-logs Search the recorded configuration and operational changes in the Matcher module, open one entry, and check the audit hash chain. The **Audit Logs** page of the Matcher module in Lerian Console lists the recorded changes to reconciliation configuration and operations. Use it to find the actor, the action, and the time of each change. The page is not limited to one context. It does not use the **Active context** selector. ## Opening the Audit Logs page *** In the Matcher module, go to **Reporting → Audit Logs**. ## Verifying chain integrity *** The **Tamper-evidence** panel is above the filters. Click **Verify integrity** to check the audit hash chain. The check reads the records and does not change them. The panel then shows one of these verdicts: | Verdict | What the panel shows | | - | - | | **Chain intact** | The number of records that the check verified | | **Chain TAMPERED: integrity broken** | The chain position of the break, and the number of records verified before it | One run inspects a limited number of records. If the chain is longer, the verdict tells you that the check skipped the most recent entries. Click **Continue from #N** to check the next records. **N** is the next chain position. If the request fails, the panel shows an error and a **Retry** button. A failed request is not a verdict. ## Filters *** Open the filter panel from the toolbar above the table. The list updates as you type. You do not need to click a button to apply a filter. Each change returns the list to the first page. | Filter | Description | | - | - | | **Actor** | The actor ID recorded with the change (for example, `user-123`) | | **Action** | The operation recorded with the change | | **Entity type** | The type of resource that changed (for example, `context`) | | **Date from** | The first day of the time range | | **Date to** | The last day of the time range. The range includes this whole day | The **Actor** and **Entity type** filters match the full value only. The toolbar also has an **Items per page** selector. Click **Clear filters** in the toolbar to remove all filters. ## Audit log table *** The table shows the newest events first. Each row has these columns: | Column | Description | | - | - | | **Entry** | The audit record ID. It links to the detail page of that entry | | **Recorded** | When Matcher recorded the change | | **Action** | The operation performed | | **Entity type** | The type of resource that changed | | **Entity ID** | The identifier of the changed resource | | **Actor** | The actor ID recorded with the change (for example, `user-123`). It is not a person's name | | **Chain** | The chain position (`#`) and the record hash. A record without a hash shows **No chain proof** | | **Changes** | A **View** button that opens the recorded change set. A row without recorded changes shows `-` | Use **Previous** and **Next** below the table to move between pages. The table footer shows the number of events on the current page. ## Viewing change details *** Click **View** in the **Changes** column. The **Audit log changes** panel opens. It names the action, the entity type, and the entity ID. Then it shows the recorded change set as JSON. The fields of the change set depend on the operation. > [!NOTE] > > If a change set was larger than the audit payload limit, the row shows **Diff truncated**. Matcher stored a marker instead of the full change set. You cannot get the full change set back from that record. ## Viewing an entry *** Click the ID in the **Entry** column. The **Audit entry** page opens with two panels: | Panel | Fields | | - | - | | **Details** | **ID**, **Action**, **Entity type**, **Entity ID**, **Actor**, and **Recorded** | | **Tamper-evidence** | **Chain position**, **Record hash**, **Previous hash**, and **Hash version** | For a record without a hash, the **Tamper-evidence** panel shows a **Chain** note instead of these fields. Click **View** next to **Changes** to open the recorded change set. ## Audit trail of one entity *** The **General Information** tab of a context and the detail page of an exception have a **View audit trail** button. Click it to open the **Audit trail** panel. The panel lists the events of that one entity, newest first. For more information, see [Managing contexts](/en/products/matcher/ui/managing-contexts) and [Exception details](/en/products/matcher/ui/exception-details). # Managing disputes Source: https://docs.lerian.studio/en/products/matcher/ui/managing-disputes Track disputes raised against reconciliation exceptions in the Matcher module of Lerian Console: filter the list, submit evidence, and record the outcome. Use the **Disputes** page in the Matcher module of Lerian Console to track disputes. A dispute is a formal challenge against an exception. You open a dispute from the exception page. Then you submit evidence and record whether the dispute was won or lost. ## Accessing the Disputes page *** Click **Exception Handling → Disputes** in the Matcher sidebar. The list shows disputes from all contexts, not only the **Active context**. Above the filters, badges count the disputes on the current page: **On page**, **Open**, **Pending evidence**, **Won**, and **Lost**. ## Filters *** The list updates when you change a filter. | Filter | Options | | - | - | | **State** | Draft, Open, Pending evidence, Won, or Lost. | | **Category** | Bank fee error, Unrecognized charge, Duplicate transaction, or Other. | | **Date from** / **Date to** | The range of creation dates. | | **Sort by** / **Order** | Created, Updated, State, or Category, in Descending or Ascending order. **Order** appears after you select a field. | | **Items per page** | The number of rows on each page. | If no dispute matches the filters, the empty list shows a **Clear filters** button. ## Disputes table *** | Column | Description | | - | - | | **Dispute** | The first eight characters of the dispute ID, as a link to the dispute page. | | **Exception** | The ID of the related exception, with a copy control. | | **Category** | The dispute category. | | **State** | The current state. | | **Opened by** | Who opened the dispute. | | **Created** | When someone opened the dispute, as a relative time. | ## Dispute lifecycle *** A dispute that you open from an exception starts in the **Open** state. When you close it, you record the outcome: **Won** or **Lost**. A **Won** or **Lost** dispute takes no more evidence, and you cannot close it again. Closing a dispute does not change the exception or its transaction. To close the exception, resolve it on the exception page. See [Exception details](/en/products/matcher/ui/exception-details). ## Dispute categories *** | Category | Description | | - | - | | **Bank fee error** | A discrepancy that an incorrect bank fee causes. | | **Unrecognized charge** | A charge that you cannot identify. | | **Duplicate transaction** | A transaction that appears more than once. | | **Amount mismatch** | The amounts on the two sides do not agree. | | **Other** | Any other reason. | ## Dispute page *** Click a dispute ID in the list to open the **Dispute** page. A breadcrumb above the title links back to **Disputes**. Below the title, you see the dispute ID and its state. While the dispute is **Open**, the header can show **Submit evidence** and **Close dispute**. The page has two panels: **Information** and **Evidence**. ### Information | Field | Description | | - | - | | **Exception** | The ID of the related exception, as a link to the exception page. | | **Category** | The dispute category. | | **Opened by** | Who opened the dispute. | | **Description** | The full description of the dispute. | | **Created** | When someone opened the dispute. | | **Updated** | When the dispute last changed. | | **Resolution** | The explanation of the outcome. It appears after you close the dispute. | ### Evidence The **Evidence** panel lists the evidence items. Each item shows its comment, who submitted it, and when. An item with a link shows **Open attachment**. If the dispute has no evidence, the panel shows "No evidence submitted yet." ## Submitting evidence *** **1. Open the dialog** Click **Submit evidence** in the page header. **2. Describe the evidence** Type a **Comment** of up to 1,000 characters. You must fill in this field. **3. Attach the evidence** Use one of the two tabs: * **Link**: Type an `https` address in **File URL (optional)**, up to 2,048 characters. * **Upload file**: Choose a PDF, PNG, JPEG, or CSV file of up to 10 MB in **File**. **4. Submit** Click **Submit evidence**. ## Closing a dispute *** Click **Close dispute** in the page header and fill in the dialog: | Field | Description | | - | - | | **Outcome** | Required. Select **Won** or **Lost**. | | **Resolution** | Required. The explanation of the outcome, up to 5,000 characters. | Click **Close dispute** to record the outcome. # Managing exceptions Source: https://docs.lerian.studio/en/products/matcher/ui/managing-exceptions Review reconciliation exceptions in the Matcher module of Lerian Console: read the queue tallies, filter the queue, run bulk actions, and open an exception to resolve it. Use the **Exceptions** queue in the Matcher module of Lerian Console to review reconciliation exceptions. An exception is a break that needs human review, such as a transaction without a match. ## Accessing the Exceptions queue *** Click **Exception Handling → Exceptions** in the Matcher sidebar. The queue shows the exceptions of the **Active context** that you select in the Matcher sidebar. To see another context, change the **Active context**. ### Queue tallies Above the filters, tallies count the whole filtered queue, not only the current page. | Tally | What it counts | | - | - | | **Queue** | The exceptions that match the filters. | | **Open** | The exceptions that are not resolved yet: **Open**, **Assigned**, and **Pending Resolution**. | | **Overdue** | The unresolved exceptions with a due date in the past. The tally shows in red when it is above zero. | | **Resolved** | The **Resolved** exceptions. | A small bar after the tallies shows the resolved share of the queue. ### Exceptions of one run To see the exceptions of one reconciliation run, click **View exceptions** on that run in [Operational History](/en/products/matcher/ui/operational-history). The **Originating run** link on an exception page opens the same view. To go back to the full queue, click **View all exceptions**. ## Filters *** The list updates when you change a filter. | Filter | Options | | - | - | | **Search...** | Free text. Matcher searches the transaction ID, the reason, and the external issue ID. | | **Status** | Open, Assigned, Pending Resolution, or Resolved. | | **Severity** | Low, Medium, High, or Critical. | | **Reason** | Why Matcher raised the exception, for example Unmatched, Fee Variance, or Aged Break. | | **Assigned to** | Select **Assigned to me** to see the exceptions assigned to your email address. | | **Date from** / **Date to** | The range of creation dates. | | **Minimum age in days** | The minimum age of the exception, from 0 to 3,650 days. | | **Sort by** / **Order** | Severity, Created, Updated, or Status, in Descending or Ascending order. **Order** appears after you select a field. | | **Items per page** | The number of rows on each page. | With **Sort by** set to **Default**, the queue shows unresolved statuses first. Inside each status, the newest exception comes first. If no exception matches the filters, the empty list shows a **Clear filters** button. ## Exception table *** | Column | Description | | - | - | | Checkbox | Selects the row for a bulk action. The header checkbox selects every row on the page. | | **Severity** | A colored dot. Put the pointer on the dot to see the severity name. | | **Transaction** | The transaction ID, with a copy control. | | **Source** | The name of the source of the transaction. | | **Amount** | The amount and currency of the transaction. | | **Reason** | Why Matcher raised the exception, as a link to the exception page. A status badge follows the link when the status is not **Open**. | | **Due date** | The resolution deadline, when one is set. An overdue date on an **Open** or **Assigned** exception shows in red with a warning icon. | | **Assigned to** | The assignee, when one is set. | Click a **Reason** link to open the exception. See [Exception details](/en/products/matcher/ui/exception-details). Matcher classifies the severity of each exception automatically. For the rules, see [Exception routing](/en/products/matcher/configuration/matcher-exception-routing). ## Bulk actions *** **1. Select the exceptions** Select the row checkboxes. A bar above the table shows how many exceptions you selected. **2. Extend the selection (optional)** Click **Select all N matching** to select every exception that matches the filters. If the selection cannot hold all of them, a notice shows how many it holds. **3. Choose the action** Click **Assign**, **Resolve**, or **Dispatch** in the bar. **4. Complete the dialog** Fill in the fields and click the action button. | Action | Fields | | - | - | | **Assign** | **Assignee**: the person who works the exceptions. | | **Resolve** | **Resolution** and an optional **Reason**. All selected exceptions get the same values. | | **Dispatch** | For **Target system**, select **Manual**. **Queue or project** is optional. | The Console sends a selection of more than 100 exceptions in batches of 100. The bar shows the number of batches. When all exceptions succeed, a message shows the count and the dialog closes. When some fail, the dialog shows how many succeeded and how many failed, with a reason for each failure. Exceptions that succeed leave the selection. Failed exceptions stay selected, so you can read each reason and try again. Click **Clear selection** to clear the selection. A change to a filter or to the **Active context** also clears it. ## Resolving exceptions *** Open an exception to resolve it. These actions on the exception page resolve the exception: * **Resolve**: Close the exception with a resolution text. * **Force match**: Mark the transaction as matched and close the exception. * **Adjust entry**: Record an adjustment for the transaction and close the exception. **Match to this** pairs the transaction with a suggested counterpart. After a pairing, you still resolve the exception. For each action, see [Exception details](/en/products/matcher/ui/exception-details). # Managing fee schedules Source: https://docs.lerian.studio/en/products/matcher/ui/managing-fee-schedules Define flat, percentage, tiered, or formula fees in the Matcher module so that Matcher can check fees on matched transactions and flag differences. The **Fee Schedules** page of the Matcher module in Lerian Console lists your fee schedules. A fee schedule defines expected fees: flat, percentage, tiered, or formula. Matcher uses them to check the fees between matched transactions and to flag differences. All contexts share the same fee schedules. To apply a schedule to a context, add a fee rule on the **Fee Rules** tab of that context, as [Context fee rules](/en/products/matcher/ui/context-fee-rules) describes. ## Opening the Fee Schedules page *** In the Matcher module, go to **Settings → Fee Schedules**. The page does not use the **Active context** selector. ## Fee schedules table *** The table shows up to 25 schedules per page. It has these columns: | Column | Description | | - | - | | **Name** | The name of the fee schedule | | **Currency** | The currency code (for example, `BRL`) | | **Application order** | **Parallel** or **Cascading** | | **Rounding** | The rounding method and the number of decimal places (for example, "Half Up, 2 decimals") | | **Items** | The number of fee items in the schedule | | **Used by** | The number of contexts whose fee rules use the schedule, or "Not used by any context" | | **Created At** | The date when the schedule was created | | **Actions** | The row actions menu | Use **Previous** and **Next** below the table to move between pages. ### Application order | Order | Description | | - | - | | **Parallel** | Each fee item uses the original gross amount | | **Cascading** | Each fee item uses the net amount that remains after the previous items | Matcher applies the fee items in priority order, lowest number first. ### Rounding methods | Method | Description | | - | - | | **Half Up** | Rounds to the nearest value. A value exactly halfway rounds away from zero | | **Bankers** | Rounds to the nearest value. A value exactly halfway rounds to the nearest even digit | | **Floor** | Rounds toward negative infinity | | **Ceil** | Rounds toward positive infinity | | **Truncate** | Removes the extra decimal places without rounding | ### Structure types | Type | Description | | - | - | | **FLAT: flat amount** | A fixed amount in the schedule currency (for example, `5.00`) | | **PERCENTAGE: percentage of base** | A rate on the base amount | | **TIERED: marginal tranches** | Tiers by base amount. Each tier charges its rate on the part of the amount inside that tier | | **EXPRESSION: formula** | A formula over the gross amount and transaction metadata keys | ## Creating a fee schedule *** 1. Click **New fee schedule**. The **New fee schedule** sheet opens. 2. Fill in the schedule fields. 3. Fill in the first fee item. Click **Add fee item** to add more items. 4. Click **Save**. ### Schedule fields | Field | Description | | - | - | | **Name** | A name for the schedule (for example, "Bank Fees BRL") | | **Currency** | The currency code (for example, `BRL`). You cannot change it later | | **Application order** | **Parallel** or **Cascading**. The default is **Parallel** | | **Rounding method** | **Half Up**, **Bankers**, **Floor**, **Ceil**, or **Truncate**. The default is **Half Up** | | **Decimal places** | The number of decimal places after rounding, from 0 to 10. The default is 2 | ### Fee item fields | Field | Description | | - | - | | **Name** | A name for the fee item (for example, "Wire Fee") | | **Priority** | A whole number, 0 or more. Lower numbers apply first. Each item in a schedule needs a different priority | | **Structure** | The structure type of the fee item | The structure type adds more fields: | Structure | Fields | Description | | - | - | - | | **FLAT: flat amount** | **Amount** | The fixed amount in the schedule currency | | **PERCENTAGE: percentage of base** | **Rate** | A fraction of the base amount, from 0 to 1. For example, `0.029` is 2.9% | | **TIERED: marginal tranches** | **Tier 1 rate**, **Tier 1 up to**, and one pair for each other tier | Each tier has a rate from 0 to 1 and an upper limit. Click **Add tier** to add a tier. Each limit must be higher than the limit before it. Leave the last limit empty to make that tier open-ended | | **EXPRESSION: formula** | **Expression** | A formula over `gross` and transaction metadata keys. You can use `+ - * /`, parentheses, and the functions `days_late(a, b)`, `days_between(a, b)`, `max(…)`, `min(…)`, `abs(x)`, and `clamp(x, lo, hi)`. If the formula only multiplies or divides `gross` by numbers, its rate must be from 0 to 1. Matcher checks the formula when you save | > [!NOTE] > > You cannot change the fee items after you save the schedule. To change them, create a new schedule. ## Viewing a fee schedule *** Open the row actions menu and click **View**. A dialog shows the currency, the application order, and the rounding of the schedule. It lists the fee items in priority order. ## Editing a fee schedule *** 1. Open the row actions menu and click **Edit**. A sheet opens with the current values. 2. Change **Name**, **Application order**, **Rounding method**, or **Decimal places**. 3. Click **Save**. The **Currency** field is locked. The sheet shows the fee items, but you cannot edit them. ## Simulating fees *** Use a simulation to estimate the fees of a schedule for an amount. A simulation records nothing. 1. Open the row actions menu and click **Simulate**. The **Simulate fees** dialog opens. 2. Enter the amount in **Gross amount**. The label shows the currency of the schedule, for example **Gross amount (BRL)**. 3. Click **Run simulation**. The result shows the **Gross** amount, the **Total fee**, and the **Net** amount. A table lists each fee item by **Name**, with the **Base used** and the **Fee**. ## Deleting a fee schedule *** 1. Open the row actions menu and click **Delete**. A confirmation dialog opens. 2. Click **Confirm**. > [!NOTE] > > Matcher does not delete a schedule that a fee rule still uses. The **Used by** column shows how many contexts use the schedule. # Managing matches Source: https://docs.lerian.studio/en/products/matcher/ui/managing-matches Review the match groups of a reconciliation run in the Matcher module, unmatch a wrong group, and record an adjustment against a group. Match groups are the pairings that a reconciliation run produced. In the Matcher module of Lerian Console, they live inside the detail page of each run. To pair transactions by hand, use the **Unmatched** workbench. See [Manual matching](/en/products/matcher/ui/manual-matching). ## Viewing the match groups of a run *** **Step 1** Click **Operate → Reconciliations** in the Matcher sidebar. **Step 2** In **Run history**, click the run ID in the **Run** column. The **Match groups** page of the run opens. The page shows a summary of the run, a **Rule breakdown** with the number of groups for each rule, and the match groups. * **Match rate**: the share of the run's transactions that matched, by count. * **Breakdown**: **Matches**, **Proposed**, **Pending review**, and the unmatched counts for the left side, the right side, and external transactions. * **Rule breakdown**: after a completed run, a warning names each rule that matched no groups. While the run is in progress, the page refreshes as the run advances. Click **Stop watching** to stop the refresh. If you stop watching, or the refresh times out, click **Recheck status** to read the run status again. ### Filtering by status Use the status filter at the top of the page to show **All** groups or only the **Proposed**, **Confirmed**, **Rejected**, or **Revoked** groups. The filter applies to all groups of the run, not only to the loaded page. ### The match group card Each card shows: * The group status and its short ID. * The **Rule** that produced the group. * The rejection or revocation reason, when the group has one. * Badges with the exception reasons on the group's transactions, when there are any. * A confidence meter with the label **High confidence**, **Medium confidence**, or **Low confidence**. * The items of the group, by side. Each item shows the **Allocated** and **Expected** amounts. A difference between them shows as a warning on the item. ## Unmatching a group *** Unmatch a group when the match is wrong. Only **Proposed** and **Confirmed** groups offer **Unmatch**. 1. Click **Unmatch** on the group card. 2. In the **Unmatch this group?** dialog, enter a **Reason**. The reason is required. 3. Click **Unmatch**. Matcher moves the group's transactions back to the unmatched backlog. A **Proposed** group becomes **Rejected**. A **Confirmed** group becomes **Revoked**. Unmatching a **Confirmed** group also reverses what its confirmation recorded: * Matcher takes back the group's entries on open items. An open item that only this group supports becomes **Withdrawn**. See [Open items](/en/products/matcher/ui/open-items). * Matcher voids the fee variances that the confirmation recorded. * Matcher closes the fee and FX exceptions that the confirmation raised, when they are still **Open** or **Assigned** and have no active dispute. Matcher can refuse to unmatch a **Confirmed** group when other activity already depends on its open item. For example, a later run already netted against the same open item. A refused unmatch changes nothing. The error message says what to do next. ## Recording an adjustment *** Record an adjustment to explain a variance in a match group, for example a bank fee or an FX difference. 1. Click **Adjust** on the group card. The **Record adjustment** dialog opens. 2. Fill in the fields. All fields are required. 3. Click **Record adjustment**. | Field | Description | | - | - | | **Adjustment type** | **Bank fee**, **FX difference**, **Rounding**, **Write-off**, or **Miscellaneous**. | | **Direction** | **Debit** or **Credit**. The direction sets the sign of the entry. | | **Amount** | A positive amount. Matcher records it as you enter it. | | **Currency** | A three-letter currency code. The field starts with the currency of the group's first item. | | **Description** | A short label for the entry, up to 500 characters. | | **Reason** | Why the entry closes the gap, up to 2,000 characters. | Matcher records the adjustment against the group. The group and its transactions do not change. # Manual matching Source: https://docs.lerian.studio/en/products/matcher/ui/manual-matching Pair transactions that the engine could not reconcile: build a basket in the Unmatched workbench of the Matcher module and link it into a confirmed match group. Transactions that the matching engine did not reconcile appear in the **Unmatched** workbench of the Matcher module in Lerian Console. Use the workbench to review the backlog, pair counterpart transactions into a match group, or ignore a transaction. ## Accessing the Unmatched workbench *** Click **Resolve → Unmatched** in the Matcher sidebar. The workbench shows the **Active context** that you select in the Matcher sidebar. To work on another context, change the **Active context**. ## The unmatched backlog *** The table lists the transactions of the context that are not matched, 25 per page. Use **Previous** and **Next** to move between pages. | Column | Description | | - | - | | **Transaction** | The short transaction ID. | | **Source** | The source name. When the side of the source is known, a second line shows **Left side** or **Right side**. | | **Amount** | The transaction amount and currency. | | **Status** | The transaction status, for example **Unmatched** or **Ignored**. | | **Exception** | The ID of the exception on the transaction, or a dash. | | **Date** | The transaction date. | ### Date window By default, the table shows the last 90 days in UTC, today included. To change the window: 1. Set **Date from** and **Date to**. 2. Click **Apply filters**. A date window can cover up to 90 days. To see older transactions, click **Show all history**. The table then shows the full backlog. To go back to a date window, click **Use date window**. > [!WARNING] > > **Apply filters**, **Show all history**, and **Use date window** empty the pairing basket. Moving between pages keeps the basket. ## Building a pairing basket *** Select the checkbox of each transaction that you want to pair. The selected transactions go into a pairing basket. The basket bar above the table shows how many transactions you selected. To empty the basket, click **Clear**. Select only transactions with the **Unmatched** status. A manual match must include at least two transactions, with at least one from the left side and one from the right side. Until your selection meets this rule, the basket bar shows a hint and **Pair selected** stays disabled. ### Suggested counterparts When the basket holds exactly one transaction, the **Suggested counterparts** panel opens. It lists opposite-side transactions that the engine ranks as plausible matches. Each suggestion shows: * The amount, the date, and the rule type that ranked it. * A score with the label **Strong**, **Plausible**, or **Weak**. * Badges that show which fields agree, for example **Amount agrees** or **Date differs**. * The amount difference and the number of days between the dates. The ranking uses raw amounts. It does not apply fee normalization or FX tolerance. To add a suggestion to the basket, click **Match to this**. ## Creating the match *** **1. Open the confirmation** Click **Pair selected** in the basket bar. The **Pair these transactions?** dialog opens. **2. Check the selection** The dialog shows the left-side and right-side transactions. It also shows the **Amount difference** between the two sides, over raw amounts. **3. Confirm** Click **Pair selected**. Matcher links the transactions into a confirmed match group, and they leave the backlog. > [!NOTE] > > Matcher gives a manual match a confidence of 100 and confirms it immediately. Each manual match creates its own **Commit** run in the run history of the context. To undo a manual match, open that run and unmatch the group. See [Managing matches](/en/products/matcher/ui/managing-matches). ## Ignoring a transaction *** Ignore a transaction to exclude it from matching, for example a duplicate entry. Only rows with the **Unmatched** status show the **Ignore** action. 1. Click **Ignore** on the row. 2. In the **Ignore this transaction?** dialog, click **Ignore transaction**. Matcher sets the transaction status to **Ignored**. An ignored transaction no longer takes part in matching. You cannot undo this action. # Open items Source: https://docs.lerian.studio/en/products/matcher/ui/open-items Track the residual balances that partial matches carry forward across reconciliation runs in the Matcher module, by status, age, and running balance. An open item is the unpaid remainder of an obligation that Matcher carries from one run to the next. When a confirmed match covers only part of an expected amount, Matcher opens an open item for the rest. Later runs net new transactions against it. Open items differ from unmatched transactions. An open item comes from a match that left a remainder. The **Open Items** screen of the Matcher module in Lerian Console lists them. You cannot change open items on this screen. ## Accessing the Open Items screen *** Click **Resolve → Open Items** in the Matcher sidebar. The screen shows the **Active context** that you select in the Matcher sidebar. To see another context, change the **Active context**. ## Open item statuses *** | Status | Description | | - | - | | **Open** | No later transaction has netted against the item. | | **Partially cleared** | Later transactions netted part of the balance, and a remainder is still open. | | **Cleared** | The running balance reached zero, within the tolerance. | | **Aged** | The item stayed open longer than the aging threshold of the context. Matcher raised an exception for it. | | **Withdrawn** | An operator unmatched the confirmed match behind the item, and no other match still counts toward it. | Only **Open** and **Partially cleared** items take part in later runs. ## The open items table *** The table shows 25 open items per page. Use **Previous** and **Next** to move between pages. | Column | Description | | - | - | | **Identity** | The key that Matcher uses to find the item in later runs. It combines the match key values and the currency. | | **Expected** | The total owed on this identity. | | **Running balance** | The amount still open. It changes as later transactions net against the item. | | **Status** | The open item status. | | **Age** | The number of days the item has been open. The icon changes at 7 days and again at 30 days. | | **Obligation date** | The business date of the original obligation, or a dash. | | **Last activity** | When the balance or status last changed. | | **Investigate** | A link to the related run or to the exceptions queue. | ### Filtering and sorting * **Status**: show **All statuses** or one status. * **Sort by**: keep the **Default order**, or sort by **Age** or **Running balance**. * **Order**: **Descending** or **Ascending**. This filter shows when you sort by **Age** or **Running balance**. ## Investigating an open item *** The **Investigate** column shows one link for each item: * **View source run** opens the detail page of the match run linked to the item. See [Managing matches](/en/products/matcher/ui/managing-matches). * **View aged break** or **View exceptions** opens the exceptions queue. These links show when the item has no linked run. See [Managing exceptions](/en/products/matcher/ui/managing-exceptions). To take back an open item that a wrong match created, unmatch the confirmed match behind it. See [Managing matches](/en/products/matcher/ui/managing-matches). # Operational history Source: https://docs.lerian.studio/en/products/matcher/ui/operational-history Review the reconciliation runs and import jobs of the active Matcher context on one screen, with live-refreshing tables and status badges. The **Operational History** screen of the Matcher module in Lerian Console shows the history of the active context. It has two tables: **Run history** for reconciliation runs and **Import jobs** for data imports. > [!NOTE] > > Export jobs are not on this screen. They are on **Reporting → Exports**. See [Reports and exports](/en/products/matcher/ui/reports-and-exports). ## Accessing the Operational History screen *** Click **Operate → Operational History** in the Matcher sidebar. The screen shows the **Active context** that you select in the Matcher sidebar. To see another context, change the **Active context**. If no context exists, the screen asks you to create one and shows a **Browse contexts** button. ## Run history *** The **Run history** table lists the runs of the context, newest first, 20 per page. Use **Previous** and **Next** to move between pages. While a run on the page is in progress, the table shows a **Live** badge. It refreshes every two seconds, for up to three minutes. | Column | Description | | - | - | | **Run** | The short run ID. Click it to open the run detail page with the match groups of the run. | | **Mode** | **Commit** or **Dry run**. | | **Status** | The run status, as a colored badge. | | **Matches** | The number of match groups that the run found. | | **Unmatched** | The unmatched transactions of both sides, plus the external unmatched count. The figure shows in red when it is above zero. | | **Split** | A small bar with the matched share of the run. | | **Started** | When the run started. | | Last column (no header) | A **View exceptions** link. It opens the exceptions queue for the run. See [Managing exceptions](/en/products/matcher/ui/managing-exceptions). | ### Run statuses | Status | Description | | - | - | | **Queued** | The run waits to start. | | **Processing** | The engine processes the run. | | **Finalizing** | The engine has stored the matches and finishes the run. | | **Completed** | The run ended without an error. | | **Failed** | The run ended with an error. | ### Run modes | Mode | Description | | - | - | | **Commit** | The run persisted its match groups and exceptions. | | **Dry run** | The run evaluated the rules and wrote no match groups or exceptions. Use it to test rules. | For how to start a run, see [Running reconciliations](/en/products/matcher/ui/executing-reconciliation). ## Import jobs *** The **Import jobs** table lists the import jobs of the context, newest first, 15 per page. While a job on the page waits or runs, the table refreshes every two seconds and shows a **Live** badge. | Column | Description | | - | - | | **File** | The name of the imported file. | | **Status** | The job status, as a colored badge. | | **Rows imported** | The rows that Matcher stored, out of the total rows in the file. When the import dropped duplicate rows, a note shows how many. | | **Failed rows** | The rows that failed to import. The figure shows in red when it is above zero. | | **Created At** | When Matcher created the job. | | **Finished at** | When the job finished. | | Last column (no header) | **View errors** for a job that completed with errors. **Details** for other jobs. | Click a row, **Details**, or **View errors** to open the **Import details** panel. ### Import details The **Import details** panel shows the job status and **Rows imported**. Depending on the job, it can also show: * Notes about duplicate rows, with the duplicate reason. * The number of rows imported for review. The link opens **Resolve → Unmatched**. * A summary of the outcome, for example the percentage of rows that failed. * **Import errors**, for a completed job with failed rows. Each row of the list gives the file row number, the field, and the error message. * **Produced transactions**, for a completed job. Each row gives the reference, amount, date, and status of a transaction that the job created. ### Import job statuses | Status | Description | | - | - | | **Queued** | The job waits to start. | | **Processing** | Matcher reads the file. | | **Completed** | The job completed and no row failed. | | **Completed with errors** | The job completed, but at least one row failed. Click **View errors** to see the failed rows. | | **Failed** | The job stopped before it completed. Check the file and its format, then upload it again. | To upload a file, see [Uploading reconciliation data](/en/products/matcher/ui/uploading-reconciliation-data). # Reports and exports Source: https://docs.lerian.studio/en/products/matcher/ui/reports-and-exports Compare reconciliation health across contexts, read the variance, matched, unmatched, and summary reports, and export report files in CSV, JSON, or XML. Three screens in the **Reporting** group of the Matcher module in Lerian Console show reconciliation figures. The **Portfolio** screen compares all your contexts. The **Reports** screen shows the reports for one context. The **Exports** screen writes a report to a file that you can download. ## Portfolio *** Navigate to **Reporting → Portfolio**. The page shows reconciliation health for all contexts in one table, with one row for each context. This page does not use the **Active context**. Use **Last 7 days**, **Last 30 days**, or **Last 90 days** to set the window. The default is **Last 90 days**. | Column | Description | | - | - | | **Context** | The context name. Click it to open the context | | **Match rate** | The percentage of the context's transactions that matched in the window | | **Backlog** | The number of unmatched transactions | | **Cash exposure** | The unmatched amount. If a context has amounts in more than one currency, the cell shows the number of currencies. Hover over the cell to see each amount | | **SLA** | The SLA compliance rate of the context's exceptions | | **Overdue** | The number of open exceptions that are past their SLA | Click a column header to sort the table. The table opens sorted by **Backlog**, with the highest value first. A row of totals above the table shows the number of contexts, **Match rate**, **Backlog**, **Overdue**, and the **Unmatched** amount for each currency. The totals include only the contexts that loaded. If a context does not load, its row shows **Failed** and a **Retry** button. Click **Retry failed** to load all failed contexts again. ## Reports *** Navigate to **Reporting → Reports**. The page shows the reports for the **Active context** that you select in the Matcher sidebar. ### Report window Set **Date from** and **Date to**, then click **Apply filters**. The default window starts 30 days before today (UTC) and ends today. The window can include at most 90 days. The **Window** chip shows the dates that the reports use. ### Report types Select a tab to choose the report: | Tab | Content | | - | - | | **Variance** | Fee totals for each source, fee schedule, and currency. Columns: **Source**, **Fee schedule**, **Expected**, **Actual**, **Net variance**, **Adjusted**, **Outstanding**, and **Variance %** | | **Matched** | The matched transactions. Columns: **Transaction**, **Match group**, **Source**, **Amount**, and **Date** | | **Unmatched** | The unmatched transactions. Columns: **Transaction**, **Source**, **Amount**, and **Date** | | **Summary** | Five totals: **Matched records**, **Unmatched records**, **Total amount**, **Matched amount**, and **Unmatched amount** | The **Variance** report columns have these meanings: * **Expected**: the fee total that the fee schedule calculates. * **Actual**: the fee total that Matcher found. * **Net variance**: **Actual** minus **Expected**. * **Adjusted**: the adjustments recorded against the fee variances of the row. * **Outstanding**: the part of the variance that no adjustment explains. The **Variance**, **Matched**, and **Unmatched** reports show 25 rows on each page. Use **Previous** and **Next** to move between pages. ## Exports *** Navigate to **Reporting → Exports**. The page lists all export jobs. An export job writes a report to a file in the background. Use the **Context** filter to show the jobs of one context. The default is **All contexts**. | Column | Description | | - | - | | **File** | The name of the export file | | **Context** | The context of the job | | **Report type** | **Matched**, **Unmatched**, **Variance**, or **Exceptions** | | **Format** | `CSV`, `JSON`, or `XML` | | **Status** | **Queued**, **Running**, **Succeeded**, **Failed**, **Expired**, or **Canceled**. A failed job also shows the error | | **Records** | The number of records written to the file | | **Created** | The date and time of the job creation | | **Finished at** | When the job ended | | **Actions** | The action for the job status | While a job is **Queued** or **Running**, the list refreshes every two seconds and shows a **Live** badge. After five minutes, the automatic refresh stops. Click **Recheck status** to start it again. The jobs continue to run in the background. ### Creating an export 1. Click **New export**. The **Create export job** sheet opens for the context in the **Context** filter. If the filter shows **All contexts**, the sheet uses the **Active context**. 2. Fill in the fields: | Field | Description | | - | - | | **Report type** | **Matched**, **Unmatched**, **Variance**, or **Exceptions**. The default is **Matched** | | **Format** | `CSV`, `JSON`, or `XML`. The default is `CSV` | | **Date from** and **Date to** | The window of the export. The default window starts 30 days before today. The dates can be at most 365 days apart | | **Source** | **All sources**, or one source of the context | 3. Click **Create export**. The new job appears in the list with the **Queued** status. ### Job actions | Status | Action | | - | - | | **Succeeded** | Click **Download** to open the file in a new tab. The row then shows the SHA-256 digest of the file. You can copy the digest and compare it with the digest of the downloaded file | | **Queued** or **Running** | Click **Cancel** to stop the job | | **Failed** or **Expired** | Click **Retry** to create a new job with the same report type and format. The new job uses the default 30-day window, not the original dates | # Rule suggestions Source: https://docs.lerian.studio/en/products/matcher/ui/rule-suggestions Review the match rules that Matcher proposes for a context, then approve or reject each one from the Rule Suggestions page of the Matcher module. Use the **Rule Suggestions** page in the Matcher module of Lerian Console to review the match rules that Matcher proposes for a context. A suggestion changes nothing on its own. Matcher creates a rule from a suggestion only when you approve it. To learn how Matcher produces suggestions, see [Rule suggestions](/en/products/matcher/matching/matcher-rule-suggestions). ## Accessing the Rule Suggestions page *** **Step 1** In the Matcher sidebar, select a context in **Active context**. **Step 2** Go to **Configure → Rule Suggestions**. The page shows the suggestions of the active context. With no active context, the page asks you to select one and shows a **Browse contexts** button. When rule suggestions are off, the page shows a notice. ## Generating suggestions *** Click **Suggest rules** in the page header. A message shows how many suggestions Matcher added to the list. Matcher proposes **Exact**, **Tolerance**, and **Date Lag** rules only. Matcher uses AI to propose these rules. It sends aggregate statistics of your match history to the language-model provider Lerian configures for your environment. ## Suggestions list *** | Column | Description | | - | - | | **Type** | The strategy of the proposed rule | | **Priority** | The priority that the proposed rule gets | | **Confidence** | How confident Matcher is in the proposal, as a percentage with a bar | | **Status** | **Pending Review**, **Approved**, or **Rejected** | | **Created** | When Matcher created the suggestion | Use the **Status** filter to show the suggestions with one status. Use **Items per page** and the page controls below the table to move through the list. The **Confidence** bar has three bands: high from 90%, medium from 70%, and low below 70%. This value rates the proposal itself. It is not the confidence score of a match. ## Viewing a suggestion *** Click **View** on a row. The suggestion detail opens below the list. Click **View** again to close it. The **Proposed rule** panel shows: | Field | Description | | - | - | | **Type** | The strategy of the proposed rule | | **Priority** | The priority that the proposed rule gets | | **Status** | The status of the suggestion | | **Rationale** | A short reason for the proposal | | **Expected improvement** | A note on the expected effect on automatic matching | | **Created rule** | The ID of the match rule that Matcher created from the suggestion. It shows only after Matcher creates the rule | The **Configuration** panel shows the confidence and a plain-language summary of the rule under **Matches when**. Click **Show raw configuration** to see the rule configuration as JSON. ## Approving a suggestion *** The **Approve** and **Reject** buttons show only on a suggestion with **Pending Review** status. **Step 1** Open the suggestion and click **Approve**. The **Approve suggestion** dialog opens. **Step 2** Click **Approve**. When Matcher creates the rule, a message shows the new rule ID, and the suggestion moves to **Approved**. The new rule is on the [Rules](/en/products/matcher/ui/context-rules) tab of the context, at the proposed priority. ## Rejecting a suggestion *** **Step 1** Open the suggestion and click **Reject**. The **Reject suggestion** dialog opens. **Step 2** Optionally, type a reason in **Reason (optional)**, up to 1000 characters. **Step 3** Click **Reject suggestion**. The suggestion moves to **Rejected**. Matcher creates no rule. # Uploading reconciliation data Source: https://docs.lerian.studio/en/products/matcher/ui/uploading-reconciliation-data Upload a transaction file into one source of the active Matcher context from the New Reconciliation screen, then follow the import job. A reconciliation run can match only the transactions that you imported. The **New Reconciliation** screen of the Matcher module in Lerian Console uploads transaction files. Each file goes into one source of the active context. The screen then follows the import job on the same page. ## Before you upload *** * The screen works on the **Active context** that you select in the Matcher sidebar. With no context, the screen asks you to select one first. * The context must be active. For a context in another status, the screen shows an alert and blocks the upload. Click **Activate this context** to open the context page, then activate the context there. For an archived context, the button is **Restore this context**. See [Managing contexts](/en/products/matcher/ui/managing-contexts). * The context needs a source to receive the file. See [Context sources](/en/products/matcher/ui/context-sources). ## Uploading a file *** **Step 1** Click **Operate → New Reconciliation** in the Matcher sidebar. **Step 2** In **Source leg**, select the source that receives the file. Each item shows the source name, its side, and its type. **Step 3** Drop a transaction file onto the upload area, or click the area to browse. You can upload one file at a time. The page shows the name and size of the file. **Step 4** Check the **Format**. The page fills it from the file name when it can. If it stays empty, select the format. > [!NOTE] > > The **Format** list groups the built-in formats by region. It also lists your layout templates. See [Formats and templates](/en/products/matcher/imports/matcher-import-formats) and [Layout templates](/en/products/matcher/ui/layout-templates). **Step 5** Optionally, click **Preview file**. The **File preview** panel shows the columns that Matcher detects and some sample rows. You can preview a file when its format is delimited (CSV), JSON, XML, or camt.053. **Step 6** Click **Upload**. A progress bar shows while the file uploads. When Matcher accepts the file, the message **Upload accepted** appears. > [!NOTE] > > The page accepts files up to 100 MB. For a larger file, the page shows a message and keeps **Upload** disabled. Split the file into smaller files and upload each one. ## Tracking the import *** After the upload, an import panel shows the file name, the job ID, and the job status. While the job runs, the panel checks its status every 1.5 seconds, up to 80 times. Click **Stop watching** to stop the checks. The import continues in Matcher. If the checks stop before the job ends, click **Recheck status** to check again. When the job ends, the panel shows **Total rows**, **Imported**, and **Failed rows**. Click **Dismiss import** to close the panel. The **Import jobs** table below the panel lists the imports of the context, newest first. It refreshes while a job is in progress. Click a row to open the import details. The same table is on the **Operational History** screen. For its columns and statuses, see [Operational history](/en/products/matcher/ui/operational-history). ## Next step *** To match the imported transactions, start a run on **Operate → Reconciliations**. See [Running reconciliations](/en/products/matcher/ui/executing-reconciliation). # Interfaces Source: https://docs.lerian.studio/en/interfaces/introduction Lerian's integrations to payment products, through a Lerian rail or through a partner institution. An interface is Lerian's integration to a payment product. It gives you the API, the ledger integration, and the operational controls for that product. The national payment system is different from a Lerian rail. SPI is the Pix system and SPB carries TED. A Lerian rail is software for native messaging, and it can connect an institution to one of those systems. This tab documents the interfaces that reach payment products through a configured provider or a Lerian rail. Lerian offers seven interfaces: * [TED Lerian](/en/interfaces/ted-lerian/what-is-ted-lerian) * [TED JD](/en/interfaces/ted-jd/ted-in-lerian) * [Pix Lerian](/en/interfaces/pix-lerian) * [Pix JD](/en/interfaces/pix-jd/direct-pix-via-jd) * [JD Courier](/en/interfaces/jd-courier/what-is-jd-courier) * [Pix BTG](/en/interfaces/pix-btg/indirect-pix-overview) * [Payments BTG](/en/interfaces/payments-btg/boleto-and-bill-payment) ## What runs in production today *** Pix runs in production through two interfaces. The JD interface connects your institution as a direct participant through a certified PSTI. The BTG interface connects you as an indirect participant. TED runs through JD. Pix Lerian is an application plugin, not the native Lerian SPI rail. In v1.0.0, Pix Lerian is available for integration and testing with the mock provider adapter. # What is Pix? Source: https://docs.lerian.studio/en/interfaces/pix/pix-overview What Pix is and how Brazil's instant payment system works: 24/7 settlement, Pix keys, QR Codes, SPI, DICT, and Direct or Indirect participant models. Pix enables instant transfers between individuals, businesses, and financial institutions. Pix operates 24/7, including weekends and holidays. Transactions typically settle in under 10 seconds. Pix is low-cost, accessible, and secure across the financial system. # How Pix works *** Pix supports multiple initiation methods: * **Pix Keys:** CPF/CNPJ, phone number, email address, or EVP (random key) * **QR Codes:** Static (reusable) and dynamic (single-use, metadata-rich) * **Manual entry:** Full banking details (bank, agency, account) After confirmation, BACEN’s infrastructure settles the transaction between the institutions, and the receiving institution credits the recipient instantly. # What Pix offers *** Pix combines speed, accessibility, and regulatory-grade security: * **Instant settlement**: Routed and cleared in seconds * **Always available**: 24/7 operation * **Low cost** or free for individuals * **Secure and auditable**: Every transaction has an E2E ID * **User-friendly**: Human-readable identifiers reduce friction and errors Pix is Brazil’s most widely adopted payment rail. People use it for everyday transfers, merchant payments, and service flows. # Pix modalities *** Pix supports several operational flows defined by BACEN: | Type | Description | | - | - | | **Common Pix** | Standard transfer via key or full account details | | **Pix QR Code** | Static or dynamic EMV-based payment initiation | | **Pix Saque** | Cash withdrawal at authorized merchants | | **Pix Troco** | Payment + cash withdrawal in a single operation | | **Pix Cobrança** | Pix with due dates, penalties, discounts, and reconciliation | | **Pix Agendado** | Scheduled Pix | | **Pix Automático** | Recurring payments | These modalities cover both consumer and business use cases. # Pix infrastructure: SPI and DICT *** Pix relies on two core services operated by BACEN: ## SPI (Instant Payment System) *** The SPI is Pix’s settlement engine. It handles: * Transaction routing between institutions * Real-time clearing * Final, irrevocable settlement Institutions connect to SPI through the **SPI Gateway**, which requires strong cryptography, mTLS, and regulated network connectivity. ## DICT (Directory of Transactional Account Identifiers) *** DICT is the nationwide registry that stores **Pix keys**. It ensures: * Key uniqueness * Lookup of recipient information * Portability between institutions * Ownership validation * Fraud and dispute checks (linked to MED and infractions) Every Pix operation also includes a **unique End-to-End ID (E2E)** for reconciliation and dispute resolution across all participants. # Participants in the Pix ecosystem *** BACEN defines participation models based on how institutions connect to its infrastructure: ### Direct Participants Institutions that connect directly to SPI and DICT. They manage: * Their own Pix messaging and connectivity * Real-time liquidity * Full regulatory, operational, and security compliance This model provides maximum autonomy and control but requires significant infrastructure and governance maturity. ### Indirect Participants Institutions that access Pix through a direct participant (the “sponsor”). The sponsor handles: * Connectivity * Clearing * Settlement * Compliance with BACEN protocols This reduces operational complexity and speeds up time-to-market. ### A practical way to understand it | Scenario | Direct participation | Indirect participation | | - | - | - | | **Transportation** | Owning an airplane and being a licensed pilot | Flying using a commercial airline | | **Food service** | Opening your own restaurant with all legal permits | Operating inside a licensed kitchen | | **Real estate** | Building an entire building | Renting an apartment in a completed building | In all cases, the goal is the same, but the responsibility and complexity shift. End users see no difference between the two models. ### PSTI (Information Technology Service Provider) A PSTI is a BACEN-regulated provider authorized to operate the infrastructure required for financial institutions to connect to the RSFN (National Financial System Network). Using a PSTI allows institutions to: * Connect to BACEN networks without building full infrastructure * Rely on certified, secure, and compliant systems * Maintain control of their ledger and business logic PSTIs ensure regulated communication, data protection, and high availability for Pix participation. # Requirements for participation *** To offer Pix, institutions must follow BACEN’s operational and security standards, which typically include: * mTLS authentication and ICP-Brasil certificates * Regulated connection to the RSFN (directly or via PSTI) * 24/7 availability and redundancy * Real-time monitoring and SLA adherence * Fraud prevention and dispute handling (MED) * Compliance with DICT and SPI message formats * Timely reporting, logging, and auditability * Following nightly limit rules and configurable customer limits > [!TIP] > > **Regulatory reference** > > For Pix rules, deadlines, and official requirements, always refer to the [official documentation](https://www.bcb.gov.br/estabilidadefinanceira/pix-normas) published by the Central Bank of Brazil (BACEN). This page is a practical overview only. > > BACEN’s materials are the authoritative source for Pix regulations. They contain the most complete and current specifications. # Pix in the Lerian platform *** Lerian provides **Pix Lerian**, an application plugin for operating Pix through stable application contracts and Midaz ledger integration. **Lerian SPI** is a separate native Pix rail design, not a connectivity option available through Pix Lerian v1.0.0. This release supports integration and testing with the mock provider adapter. The native SPI messaging connector is still under development and is not available for use as a production rail. Pix Lerian covers keys and claims, transfers and refunds, collections, and BR Codes. The connectivity provider is configured behind the public contract, so the client integration remains stable. Key characteristics: * **One interface, different connectivity providers**: the client application uses the same public contract regardless of the configured provider * **Provider-adapter boundary**: the client application stays independent of the configured adapter contract * **Native ecosystem integration**: works with Midaz as the ledger and Access Manager for authentication and authorization > **[Pix Lerian](/en/interfaces/pix-lerian)** > > Understand the interface, its domains, and the recommended integration journey. > **[Main domains](/en/interfaces/pix/main-domains-overview)** > > Explore the core Pix domains: DICT, Transactions, QR Codes, and MED. > **[Explore the Pix Lerian API](/en/reference/interfaces/pix-lerian/dict/create-entry)** > > Browse the client-facing operations for keys, claims, payments, refunds, and collections. # Best practices Source: https://docs.lerian.studio/en/platform/access-manager/am-best-practices Best practices for operating Access Manager securely: manage credentials, lock down environments, and protect identity and authorization at scale. Access Manager holds the identity, authorization, and credentials for every protected Lerian product. Treat it as critical infrastructure and operate it with the controls below. ## Credentials *** ### Create credentials for calling applications For machine-to-machine access, the calling application uses its `client_id` and `client_secret` to request a token. The receiving service validates that bearer token. Configure the receiving service to enforce the route. It does not need a second M2M client credential for the same call. ### Protect the deployment environment Protect the hosts, deployment secrets, and administrator access that support Access Manager. Limit access to the physical or virtual machines and the secret-management systems that deliver its configuration. ### Limit access to credential-management endpoints Identity management endpoints can expose application data. Restrict access to these endpoints and keep sensitive management operations isolated from general back-office workflows. ## Security recommendations *** ### Use Application-to-Application flows for sensitive endpoints For critical access such as Ledger automation, use dedicated Applications. This gives you clear control over each integration. Delete an application through the documented Identity API when you no longer need it. Handle any existing tokens or credentials through your environment's incident-response procedure. ### Use user-based credentials for manual actions When a person needs access for debugging, operations, or support, issue per-user credentials instead of sharing `client_credentials`. You can then manage access and remove permissions for each person separately. End a session only through the normal logout flow with that session's `id_token_hint`. That flow is not an administrator-targeted "log out another user" control. Reserve `client_credentials` for machine-to-machine applications. Run regular access reviews for applications and permissions. This minimizes exposure and keeps access limited to authorized users. Always enforce the principle of least privilege for both users and applications. Grant only the exact permissions each one needs. ## Operational changes *** How you change access depends on what you change: everyday operations or platform data. ### Use the management surfaces after bootstrap After the environment starts, use Identity APIs or Lerian Console for operational access changes: * create, update, or remove users * assign users to groups * create or delete machine-to-machine applications * configure providers and MFA. Bootstrap seed data is not a day-to-day configuration surface. Editing seed files after deployment does not reliably update an existing environment. ### Ship platform permission changes as controlled updates Built-in resources, actions, roles, groups, and permission sets are platform data. Change them through migrations or an idempotent reconciler so existing environments converge predictably. Avoid one-off database edits and manual permission changes. They create drift between environments and make access reviews harder to trust. > [!TIP] > > See our [Security Recommendations](/en/products/midaz/security-recommendations) for more guidance on securing your infrastructure. # How Access Manager works Source: https://docs.lerian.studio/en/platform/access-manager/am-components See how Access Manager's Auth, Identity, and product-level enforcement fit together to authenticate subjects and enforce permissions across products. Access Manager has two services. Each protected Lerian product enforces access control at the route level through the Auth client. Identity provides the management. Together, they cover the three jobs of access control: managing access data, deciding access at runtime, and enforcing those decisions inside each product. * **Auth** is the runtime decision layer. It issues tokens, verifies MFA, returns user information, and answers permission checks. * **Identity** is the management layer. It stores users, groups, applications, communication providers, application-provider links, and MFA configuration. * **Product-level enforcement** is the runtime integration inside protected products. When a product enables its Auth client and configures an Auth address, it calls Auth before product business logic runs. It is not a third Access Manager service. For example, Identity creates a user and assigns the user to a product group. Auth issues the access token and answers permission checks for that token. A product configured for enforcement applies those decisions on its protected routes before the request reaches the product handler. > **[Auth service](/en/platform/access-manager/auth-plugin)** > > Runtime tokens, MFA verification, user information, permission decisions, and cache. > **[Identity service](/en/platform/access-manager/identity-plugin)** > > Management of users, groups, applications, communication providers, and MFA configuration. > **[Product-level enforcement](/en/platform/access-manager/product-level-enforcement)** > > Route-level checks inside protected Lerian products that call Auth before business logic when their Auth client is enabled and configured. > [!WARNING] > > Auth, Identity, and product-level enforcement do not replace each other. Identity defines access data, Auth makes authentication and authorization decisions, and products with their route-level integration enabled enforce those decisions at runtime. For day-to-day usage, see [Using Access Manager](/en/platform/access-manager/using-access-manager) for the API workflow and [Access Manager via Lerian Console](/en/platform/access-manager/using-access-manager-with-midaz-console) for the visual flow. # Access Manager lifecycle stages Source: https://docs.lerian.studio/en/platform/access-manager/am-lifecycle-stages The stages of an Access Manager deployment, from installing Auth and Identity to daily operation, and what changes hands at each one. An Access Manager deployment moves through four stages: Install, Bootstrap, Enable, and Operate. Each stage has its own owner. > [!NOTE] > > Turn enforcement on in a product only after Access Manager runs and reports healthy. A product that enforces access first rejects every protected request. ## Install *** Deploy the two Access Manager services and connect them to their dependencies. Auth answers access decisions at runtime. Identity holds the users, groups, and applications behind those decisions. Database schema changes run as a separate migration step. The services never change the schema when they start. Confirm that both services report healthy and that your products can reach Auth over the network. The platform operator does this once per environment. ## Bootstrap *** Bootstrap prepares a new environment before any product enforces access. * **Bootstrap** seeds the base organization, roles, groups, applications, and permission sets that the platform relies on. > [!WARNING] > > The identity backend starts with seeded administrator accounts. Change every seeded administrator password before you expose the deployment. Bootstrap applies its seed data only during initial setup. After the environment starts, treat that data as read-only. The platform operator owns this stage, and it runs once per environment. ## Enable *** Enforcement is a per-product switch. You set the Auth variables in the `.env` file of each Lerian product or plugin where you want access enforced. Most products and plugins ship with enforcement off and require you to turn it on for production. Streaming Hub rejects enforcement off in production. Check each Brazil rail's environment-variables page for its default. > [!WARNING] > > With enforcement off and `AUTH_REQUIRED` unset, protected routes admit every request without a token. In the products that read `AUTH_REQUIRED`, set it to `true` to make those routes refuse with `503` when the Auth client is disabled or misconfigured. [Product-level enforcement](/en/platform/access-manager/product-level-enforcement) lists these products. By default a machine-to-machine token acts as the product editor role. Set `AUTH_M2M_INVERSION_ENABLED=true` to use the application's own subject. Variable names differ by product family. For the per-product table, read [Enabling Access Manager](/en/platform/access-manager/enabling-access-manager). The team that deploys each product repeats this stage once per product. ## Operate *** Day-to-day work starts after the environment is up. It covers token requests, user and group management, MFA configuration, and calls to protected APIs. Use the Identity APIs or Lerian Console for these tasks. Access tokens issued to the applications that Identity creates expire after one hour. Applications created outside Identity carry their own lifetime. Administrators, end users, and integrations share this stage. It continues for the life of the deployment. ## Next steps *** * [Installing Access Manager](/en/platform/access-manager/installing-access-manager) * [Enabling Access Manager](/en/platform/access-manager/enabling-access-manager) * [Using Access Manager](/en/platform/access-manager/using-access-manager) # Auth service Source: https://docs.lerian.studio/en/platform/access-manager/auth-plugin The Auth service issues OAuth2/OIDC tokens, validates sessions, verifies permissions, and handles MFA challenges during user sign-in. Auth is the runtime access service for Access Manager. It sits between your protected Lerian products and the configured identity provider. It gives products a single interface for token lifecycle, user information, permission checks, logout, and MFA login verification. Use Auth when you need to: * request access tokens for human users or machine-to-machine applications. * refresh an expired access token. * retrieve OIDC-compatible user information. * validate whether a subject can perform an action on a resource. * retrieve the permissions available to the authenticated user. * end a user session. * initiate and verify MFA challenges during login. Auth delegates identity data to the identity provider and caches token, permission, and MFA-related data with Valkey to reduce repeated calls during normal operation. ## Main flows *** Auth supports the access flows used by Lerian products and integrations. ![Conceptual authentication and authorization flow through the Auth plugin, including token issuance and permission checks](https://mintcdn.com/lerian-49cb71fc/SFzzdxyH5SN7w_fC/images/en/d2/am-auth-flow.svg?fit=max\&auto=format\&n=SFzzdxyH5SN7w_fC\&q=85\&s=dbd54982476146d65ae4e836f578ccfc) Figure 1. Authentication and Authorization Flow ### Authentication flow 1. **Token request** * Human users authenticate with the `password` grant. * Service integrations authenticate with the `client_credentials` grant. * Auth forwards the request to the identity provider and returns the access token, refresh token, and ID token when applicable. 2. **Token refresh** * Clients exchange a refresh token for a new access token. * Auth validates the refresh token with the identity provider before issuing the new token. 3. **Token validation** * Protected products validate bearer tokens before accepting a request. * Auth extracts trusted token claims to identify the subject and tenant context. * Auth can cache validation results to reduce repeated identity-provider calls. ### Organization context On Lerian Cloud, Auth takes the organization from the token. In a BYOC deployment, it uses the configured organization. The product that receives an authorized request owns its own data isolation. ### Browser SSO flow 1. **Start the flow** * The browser starts SSO through Auth with an email address, an eligible provider, and an S256 PKCE `code_challenge`. Auth resolves the tenant server-side from the email address and redirects the browser to the upstream identity provider. 2. **Handle the callback** * The identity provider redirects the browser to `PLUGIN_AUTH_SSO_CALLBACK_URL`. This absolute callback URL must be registered in the identity provider application's redirect-URI allowlist. * The Console submits the provider authorization code, the Auth state, and the matching PKCE `codeVerifier` through Auth's `sso_code` grant. Auth consumes the flow once, relays the code to the identity provider, and returns the token envelope. Before issuing tokens, Auth verifies that the email returned by the identity provider resolves to the tenant that started the flow. Auth rejects the login if the email is absent or resolves to a different tenant. The rejection uses the same generic SSO failure as other refused relays. Configure the tenant's OAuth provider and SSO policy through [Identity](/en/platform/access-manager/identity-plugin). The Auth callback URL must match the application's allowed redirect URI. ### MFA login flow 1. **Challenge required** * When MFA is required, Auth returns an MFA challenge state instead of completing the login immediately. 2. **Start challenge delivery** * For email MFA, the client submits the returned MFA token to request challenge delivery, and Auth sends the challenge code by email. * A TOTP application generates its passcode locally, without a delivery request. 3. **Challenge verification** * Auth verifies the challenge and returns access tokens when the verification succeeds. 4. **Session controls** * MFA challenges expire after the configured TTL. * Failed attempts are limited to protect the account from repeated guessing. ### Authorization flow 1. **Enforce access** * A protected product asks whether the authenticated subject can perform a specific action on a specific resource. * Auth evaluates the request against the configured Access Manager permissions. * Auth can cache successful authorization decisions for performance. 2. **Retrieve permissions** * A client can retrieve the permissions available to the authenticated user. * Auth returns permissions as a map of resources to allowed actions. ### User information flow 1. **Profile request** * The client requests user profile information with a bearer token. * Auth returns OIDC-compatible user information. ### Logout flow 1. **User logout** * The client sends a logout request with the ID token hint. * Auth invalidates the session in the identity provider. ## API overview *** Auth exposes APIs for: * requesting access tokens with `password` or `client_credentials`. * refreshing access tokens. * ending user sessions. * validating user permissions. * retrieving user information. * retrieving user permissions. * starting, completing, and discovering browser SSO flows. * initiating an MFA challenge. * verifying an MFA login challenge. Auth exposes both credential-bearing entry points and protected operations. Follow the security requirements documented for each endpoint. For technical details on endpoints and usage, check the [Auth APIs](/en/reference/platform/access-manager/am-auth-apis) documentation. ## Permission decisions *** When a protected product asks Auth whether a subject can perform an action on a resource, Auth resolves the subject (a human user or a machine-to-machine application), looks up the permissions associated with it, and returns an authorized or denied decision. For human users, Auth evaluates permissions embedded in their identity data, including direct user permissions and applicable roles. For machine-to-machine applications, permissions come from the application's configured permission set. Auth evaluates the data Identity manages and does not invent permissions. For the full subject/resource/action model, examples, and how routes are protected, see [Product-level enforcement](/en/platform/access-manager/product-level-enforcement). To inspect what the authenticated subject can reach, use [Retrieve User Permissions](/en/reference/platform/access-manager/retrieve-user-permissions). ## Data storage and caching *** Auth uses structured policy data and cache entries to reduce repeated work: * **Policy data** stores access-control rules for users, groups, and applications. * **Token cache** stores token validation results. * **Permission cache** stores successful authorization decisions. * **User permissions cache** stores the map of resources and actions available to a user. * **MFA cache** stores temporary challenge state and attempt counters. # Caradhras in Access Manager Source: https://docs.lerian.studio/en/platform/access-manager/caradhras-in-access-manager See what the Auth and Identity services use Caradhras for, where the identity data lives, and which variables point Access Manager at it. Access Manager keeps its identity data in Caradhras. Identity and Auth connect to the same backend, each for a different job. ## What each service uses it for *** * **Identity** stores users, groups, applications, communication providers, application-provider links, and MFA configuration in the backend. * **Auth** sits between your protected Lerian products and Caradhras. It forwards token requests to the backend and validates refresh tokens with it. It also reads the token claims that identify the subject and the tenant context. * **Product-level enforcement** depends on the claims that Caradhras issues. A token is internal when it carries the `isInternal` claim with the value `true`. Caradhras issues that claim only for applications that the platform provisions as internal Lerian services. Caradhras holds the access data. Identity writes it, Auth reads it to decide, and each protected product acts on the decision. For example, Identity creates a user and puts that user in a product group. Auth then issues the access token and answers the permission checks that follow. ## Where the data lives *** Caradhras persists this data in the PostgreSQL database that you configure for Access Manager. PostgreSQL backs Caradhras and does not replace it. Schema changes run as a separate migration step, never when a service starts. Both services reach the backend at the address you configure. The Access Manager Helm chart deploys the backend alongside the two services. Valkey caches token, permission, and MFA-related data, so that Auth repeats fewer calls to the backend during normal operation. ## Tokens and keys *** Access tokens issued to the applications that Identity creates expire after one hour. Applications created outside Identity carry their own lifetime. `AUTHORIZER_JWT_CERTIFICATE` holds a public key. A fresh identity backend mints its own key pair on first start, and this variable tells Access Manager which public key to trust. A configured but unusable value stops the service from starting. ## Configuration *** These variables connect Access Manager to Caradhras. * `AUTHORIZER_ADDRESS` gives the address of the identity backend. Outside `development`, `staging` and `local`, use an https address with a certificate from a public certificate authority. Read [Installing Access Manager](/en/platform/access-manager/installing-access-manager). * `AUTHORIZER_JWT_CERTIFICATE` supplies the public key that validates the tokens the identity backend issues. Read the [Access Manager chart README](https://github.com/LerianStudio/helm/tree/main/charts/plugin-access-manager). * `PLUGIN_AUTH_SSO_STATIC_ORGANIZATION` pins pre-login SSO to one Caradhras organization. Read [Identity service](/en/platform/access-manager/identity-plugin). * `PLATFORM_INTERNAL_CIDRS` declares the networks that platform services call Caradhras from. When a tenant IP allowlist is active, Auth can deny platform requests from networks not listed here. Caradhras reads its own `PLATFORM_INTERNAL_CIDRS`. Set it to the same value. Read [Tenant IP allowlist](/en/platform/access-manager/product-level-enforcement#tenant-ip-allowlist). # Identity service Source: https://docs.lerian.studio/en/platform/access-manager/identity-plugin The Identity service manages users, groups, roles, applications, providers, SSO configuration, and MFA configuration for Access Manager deployments. Identity is the management service for Access Manager. It's where administrators define who can access Lerian products: which groups and roles people belong to, which applications can authenticate with machine-to-machine credentials, and which communication or OAuth providers are available. Identity does not issue access tokens or make runtime authorization decisions. Auth uses the identity data managed here to authenticate subjects and evaluate permissions. Use Identity when you need to: * create, update, list, or delete users. * assign users to product groups or direct permissions. * create, update, list, or delete groups and inspect their permissions. * create, update, list, or delete tenant-scoped custom roles and assign their permissions, users, and groups. * create, list, retrieve, or delete machine-to-machine applications. * create, update, list, retrieve, or delete communication providers. * link providers to applications and select the default provider. * configure the tenant's SSO policy and active OAuth provider. * initiate, verify, enable, disable, review, or change MFA settings for users. * reset or update user passwords. ## Users and groups *** Identity manages human access through users, groups, and direct user permissions. A group represents a permission set for a product or Access Manager area. For example, you can assign a user to a Midaz viewer group to inspect ledger data without changing it. You can also assign that user to a Reporter contributor group to create report templates. A user can also receive a direct permission without belonging to a group for that product. Identity exposes user endpoints to list users, create users, retrieve a user, update user information, manage group assignments and direct permissions, delete users, update passwords, and reset passwords. User and group list endpoints use `page` and `limit` for pagination. On Lerian Cloud, the service returns only the users and groups that belong to your tenant. When you create or update a user, send the group IDs returned by [List Groups](/en/reference/platform/access-manager/list-groups). The API handles the internal organization prefixing. Clients should not build `organization/group` values manually. > [!WARNING] > > Do not send tenant ownership in user payloads. Identity derives the tenant scope from the bearer token, then applies the requested user and group changes inside that tenant. ### Roles Access Manager uses role levels as a common convention across products. The effective actions for each role come from the product or application permission set. Identity also supports custom roles scoped to the tenant. | Role | Typical access | | - | - | | Admin | Full access, including administrative operations. | | Editor | Can read, create, update, and delete resources. | | Contributor | Typically can read, create, and update resources. Delete access depends on the product permission set. | | Viewer | Read-only access. | > [!WARNING] > > Role scope is per product or application. A user can be an Editor in Midaz, a Viewer in Reporter, and have no access to Fees. Access Manager supplies the platform permission catalog. You can create, update, and delete tenant-scoped groups, then use [List Groups](/en/reference/platform/access-manager/list-groups) and [Retrieve Group Details](/en/reference/platform/access-manager/retrieve-group-details) to inspect the groups available in your environment. Identity deletes a group only when it has no members and no subgroups. Otherwise it returns `409` with code `IDE-1058` or `IDE-1059`. Group assignments are one way to grant access. Identity also supports direct user-permission assignment. You can create custom roles and assign permissions, users, and groups to them. Built-in system roles are immutable: Identity rejects attempts to create, update, or delete them. Use custom roles when the standard role levels do not express the access model you need. > [!WARNING] > > A user without a group for a product can still have access through direct user permissions. Check the user's effective permissions before concluding that they cannot access a product. For the resource-action model, the action vocabularies used by each product, and how routes are protected at runtime, see [Product-level enforcement](/en/platform/access-manager/product-level-enforcement). For the API workflow that ties users, groups, and tokens together, see [Using Access Manager](/en/platform/access-manager/using-access-manager). ## Applications *** Applications represent machine-to-machine clients for the `client_credentials` grant. Use them when a service, job, or integration needs to authenticate without a human user. An application stores the `clientId` and `clientSecret` used by Auth during the `client_credentials` flow. Identity returns the `clientSecret` only in the response to the create request. If you lose the secret, delete the application and create a new one. After creating an application, the integration can request an access token from Auth and call protected Lerian APIs according to its configured permissions. Identity supports: * listing applications. * creating applications. * retrieving application details. * deleting applications. For example, a reconciliation job can use a Bank Transfer application to request a token and call only the endpoints needed for its workflow. Identity accepts only application names from its catalog when it creates or deletes applications. Application names are product identifiers, such as `midaz` or `reporter`, not UI display labels. [List Available Applications](/en/reference/platform/access-manager/list-available-applications) returns the catalog. > [!NOTE] > > Identity filters internal applications from the public application list. On Lerian Cloud, it also returns only applications of your tenant. ### Tenant scoping On Lerian Cloud, Identity uses the authenticated context as the tenant boundary for management operations: * user operations apply to the caller's tenant organization. * group lists include only permission groups available in that tenant. * application lists include only machine-to-machine applications bound to that tenant. * application credentials created for an integration belong to the tenant that created them. This keeps operational access local to the tenant. An administrator token from one tenant cannot list or mutate another tenant's users, groups, or applications through the public Identity APIs. ## Communication providers *** Communication providers define delivery services available to applications. Email providers can deliver MFA challenges. Identity manages providers separately from applications so you can reuse and control the same provider consistently. Identity supports: * listing providers. * creating providers. * retrieving provider details. * updating providers. * deleting providers. Identity also supports application-provider links: * list providers linked to an application. * link a provider to an application. * update a provider link. * unlink a provider from an application. * set the default provider for an application. Use a default provider when an application has more than one linked provider and needs a preferred one. For SSO, Identity manages one active OAuth provider per tenant. Supported provider types are Google, `AzureAD` (Microsoft Entra ID), Okta, and Custom. The Auth SSO callback URL must be an absolute URL and appear in the application's redirect-URI allowlist, or the authorization-code relay will fail. Before saving a candidate SSO provider, run its preflight validation. It checks the configuration, OIDC endpoint discovery, client credentials, and the callback redirect URI without creating or changing a provider, application-provider link, or application. The validation result reports a failed check. A malformed or unauthorized request returns an HTTP error. For a BYOC deployment with one fixed Caradhras organization, set `PLUGIN_AUTH_SSO_STATIC_ORGANIZATION` in Auth. Pre-login SSO then resolves every request to that organization instead of looking up an organization from the user's email. ## MFA management *** Identity manages MFA configuration for users. Auth uses that configuration during login when MFA is required. Identity supports: * initiating MFA setup. * verifying an MFA passcode during setup. * enabling MFA after verification. * disabling MFA. * retrieving the current MFA status. * setting the preferred MFA method. MFA supports an authenticator app or email. Email MFA requires an email provider and a saved email address for the user. The standard MFA setup and management operations are self-service: the caller's token subject must match the target user. Administrative MFA operations are separate and apply only to the methods supported by those operations. ## Architecture and identity flow *** ![Identity architecture showing how the identity plugin manages user profiles and MFA methods across the authentication flow](https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/identity-flow.svg?fit=max\&auto=format\&n=SEOef3JqTInYAAau\&q=85\&s=ba2b5eb1102760d9938fa4babab656da) Figure 1. Identity Flow 1. **Management request** * An administrator or authorized client calls an Identity API. * When Identity's Auth client is enabled and configured, the request is authenticated and checked against Access Manager permissions. Set `AUTH_REQUIRED=true` when the deployment must refuse management requests if that client is disabled or misconfigured. 2. **Request processing** * Identity validates the payload and applies the requested operation. * The service updates users, groups, roles, applications, providers, provider links, SSO configuration, or MFA configuration in the configured identity system. 3. **Runtime use** * Auth reads the resulting identity data during token, permission, and MFA flows. * Protected Lerian products rely on Auth decisions before processing product operations. ## API overview *** Identity exposes APIs for: * users. * groups. * custom roles and their assignments. * applications. * providers. * application-provider links. * SSO policy and OAuth-provider configuration. * MFA setup and management. * tenant IP allowlist configuration. * self-service profile and phone management. * password reset and password update flows. When its Auth client is enabled and configured, Identity protects management access through Access Manager permissions. For technical details, check the [Identity APIs](/en/reference/platform/access-manager/am-identity-apis) documentation. # Product-level enforcement Source: https://docs.lerian.studio/en/platform/access-manager/product-level-enforcement Understand how protected Lerian products enforce Access Manager decisions at the route level by calling Auth before business logic runs. Product-level enforcement is the runtime integration inside each protected Lerian product. It is not a third Access Manager service. A product with its Auth client enabled and an Auth address calls Auth from its route-level code. The route-level code then lets the request continue or rejects it before the product handler runs. Identity defines the access data. Auth decides whether a subject can perform an action on a resource. Product-level enforcement applies those decisions to live traffic. ## What it does *** For each request on a route with enforcement configured, the product: * reads the bearer token from the `Authorization` header. * builds a permission check with the subject derived from token claims, the `resource` configured for the route, and the `action` configured for the route. * sends that check to Auth. * continues with the product handler when Auth returns an authorized decision. * rejects the request with the appropriate HTTP or gRPC error when Auth denies it. * uses token claims according to the product's own tenant-aware integration when required. > [!WARNING] > > You configure route protection separately in each product. Enable each product's Auth client and set an Auth address. Midaz, Matcher, Reporter, Streaming Hub, Pix Lerian, and Pix Indirect BTG read `AUTH_REQUIRED`: set it to `true` to make protected routes refuse with `503` if the Auth client is disabled or misconfigured. Without it, the middleware passes the request through by default. In Access Manager, `PLUGIN_AUTH_ENABLED` configures Identity's Auth client only. It does not enable enforcement in other Lerian products. For example, a Midaz route can protect `POST /transactions` with the resource `transactions` and action `post`. Auth decides whether the subject in the bearer token has that permission. > [!WARNING] > > Product-level enforcement does not manage users, issue tokens, or store policy data. It calls Auth and acts on the response. ## Permission model *** Access Manager evaluates the core permission decision with three values: | Value | Meaning | | - | - | | Subject | The authenticated user, the machine-to-machine application, or the derived `admin/-editor-role` subject in legacy M2M mode. | | Resource | The protected area of a product, such as `users`, `applications`, `accounts`, `reports`, or `templates`. | | Action | The operation being requested, such as `get`, `post`, `patch`, `delete`, `read`, or `write`. | Human authorization evaluates permissions embedded in identity data. Groups can organize permissions, and users can also receive direct user permissions. For a normal user token, the middleware derives the subject from its `owner` and `sub` claims. Unless the product configures local JWT verification, the middleware parses those claims without signature verification. The authorization round trip to Auth is the trust anchor. Machine-to-machine authorization depends on `AUTH_M2M_INVERSION_ENABLED`. With its default value of `false`, the middleware derives a product-scoped `admin/-editor-role` subject for any non-user token type and does not consult that token's `sub`. With `true`, it uses the application's token `sub` identity and rejects unknown token types. ### Tenant IP allowlist For the guide to this feature, read [IP allowlist](/en/platform/access-manager/features/ip-allowlist/overview). Identity stores each tenant's IP allowlist and the surfaces where it applies. A non-empty list applies only on explicitly selected scopes: `console` for human traffic and `api` for machine traffic. With no selected scope, the list remains stored but is inert. Configure `TRUSTED_PROXIES` in Auth and in each product process listed in [Deployment requirements (BYOC)](/en/platform/access-manager/features/ip-allowlist/deployment). Each process must use the CIDRs of the load balancer or ingress that fronts it. The product resolves the client IP and sends it to Auth as `clientIp`. The gate runs before Auth's permission cache. Tokens marked as internal bypass it. An empty list does not enforce the gate. A token is internal when it carries the `isInternal` claim with the value `true`. Caradhras issues that claim only for applications that the platform provisions as internal Lerian services. The client-facing application API cannot set it. The bypass skips only the allowlist gate. Auth still validates the token against Caradhras before it authorizes the request, so a forged internal marker on an invalid token does not grant access. > [!WARNING] > > If Caradhras is unavailable and Auth has a cached policy, Auth serves the last-known-good policy and can still deny. A lookup with no cached policy fails open. An unset `TRUSTED_PROXIES` in Auth also fails open and emits the untrusted-proxy signal. Auth denies a request with a missing client IP when a non-empty list applies. The exception is a request whose transport peer is in `PLATFORM_INTERNAL_CIDRS`. Auth allows that platform-internal forwarding failure, and you must monitor it. In deployments where platform services call Caradhras from cluster networks, configure the same `PLATFORM_INTERNAL_CIDRS` value in Identity, Auth, and Caradhras with those CIDRs. Each entry must include an explicit prefix. A catch-all prefix is rejected at boot because it would trust every caller and remove every tenant entry from enforcement. Caradhras admits platform callers from its own copy of the value. Auth subtracts them from the tenant policy before enforcement. ### Resource and action names They must match the values configured for the product route, and the route sends those configured values to Auth. Most API products use HTTP-method-style actions: | Example | Meaning | | - | - | | `users:get` | Read users. | | `applications:post` | Create machine-to-machine applications. | | `reports:patch` | Update reports. | | `templates:delete` | Delete report templates. | Some products use semantic actions when an HTTP method does not describe the route well: | Example | Meaning | | - | - | | `transfers:create` | Create a Bank Transfer transfer. | | `transfers:process` | Process a Bank Transfer transfer. | | `system_config:read` | Read Bank Transfer system configuration. | | `workflows:activate` | Activate a Flowker workflow. | Use [Retrieve User Permissions](/en/reference/platform/access-manager/retrieve-user-permissions) to inspect the effective resources and actions available to the authenticated user. > [!WARNING] > > Do not derive permission strings from endpoint paths by convention. Protected products enforce the resource and action values configured in Access Manager, not values inferred from the URL. ## Request flow *** 1. **Receive the request** * The product reads the bearer token from the `Authorization` header. * If the token is absent or malformed, the product rejects the request before any permission check. 2. **Build the permission check** * The product uses the resource and action configured for the route. * In tenant-aware deployments, the product applies its own configured tenant integration. Do not infer a universal `tenantId`-only authorization contract from this route-level flow. 3. **Ask Auth** * The product calls Auth with the subject, resource, and action. Depending on its integration, it can also forward product and client-IP context. * Auth evaluates the request against the configured Access Manager permissions and may serve the answer from cache. 4. **Apply the decision** * On authorized, the product continues to its handler. * On denied, the product returns the appropriate HTTP or gRPC error and does not invoke business logic. ## Where this fits *** When a product configures route-level enforcement, it sits between the network and the product handler. With an enabled and configured Auth client, Auth evaluates authorization before the handler runs. An authorized subject still needs the right resource-action permission to reach a specific operation. A tenant IP allowlist active for that request's scope can still deny it. Auth can serve authorization decisions from cache. On the management side of this picture, see the [Identity service](/en/platform/access-manager/identity-plugin). The [Auth service](/en/platform/access-manager/auth-plugin) handles the runtime decision side. For the day-to-day workflow against the APIs, see [Using Access Manager](/en/platform/access-manager/using-access-manager). # Access Manager via Lerian Console Source: https://docs.lerian.studio/en/platform/access-manager/using-access-manager-with-midaz-console Manage supported Access Manager operations visually through Lerian Console: users, groups, machine-to-machine applications, and permission assignments. Lerian Console offers a visual interface for the access tasks most administrators handle day to day. From Lerian Console you can create and manage users, assign permission groups, and set up machine-to-machine applications without touching the Identity APIs. > [!TIP] > > If you need the full API surface, including providers, application-provider links, token flows, or explicit permission checks, use the [Using Access Manager](/en/platform/access-manager/using-access-manager) guide. ## Prerequisites *** Before using Access Manager features in Lerian Console, make sure you have: * Access Manager plugin installed and running * Access Manager [enabled in your environment configuration](/en/platform/access-manager/enabling-access-manager) * Admin credentials to log into Lerian Console > [!CAUTION] > > After you enable Access Manager, protected API requests must include an `Authorization` header with a valid Bearer access token. > > Without this header, protected requests will be rejected, even for endpoints that were previously accessible without authentication. ## Where the Access Manager screens live *** Lerian Console holds the Access Manager screens in two areas. Settings holds Applications and Security. Authorization holds Users, Roles, Groups, and Permissions, with its own sidebar. **Step 1** Log into Lerian Console with your admin credentials. **Step 2** Click the gear icon () in the top navigation bar. **Step 3** Select **Applications** or **Security** to open Settings. Select **Authorization** to open the Authorization area. ## What you can do *** > **[Managing Users](/en/platform/console/managing-users)** > > Create, edit, and delete user accounts. Manage passwords and assign users to permission groups for human access. > **[Managing Applications](/en/platform/console/managing-applications)** > > Create and review machine-to-machine applications for service integrations. Use generated client credentials for secure service communication. ## What still uses APIs *** Console covers the common cases. For anything the Console does not cover, use the [Identity APIs](/en/reference/platform/access-manager/am-identity-apis) and [Auth APIs](/en/reference/platform/access-manager/am-auth-apis): | Area | Use APIs for | | - | - | | Providers | Create, update, delete, link, unlink, and set default communication providers for MFA delivery. | | Token flows | Request tokens, refresh tokens, retrieve user information, and end sessions. | | Permission checks | Validate a resource-action decision or retrieve the authenticated user's effective permissions. | > [!NOTE] > > Users enroll and manage their own multi-factor authentication methods in the Console, under Settings › Security. > [!NOTE] > > On Lerian Cloud, Console follows the same tenant boundary as the APIs. The logged-in administrator only sees and manages users and applications in their own tenant. > [!NOTE] > > You cannot delete your own user. Another administrator with the delete permission on users can delete a seeded administrator account. A later run of the install step can recreate it with its default password. Change that password before exposing the deployment. Make sure another administrator exists before removing one. You can update its details through the Console. ## Next steps *** * Learn about [best practices for Access Manager](/en/platform/access-manager/am-best-practices) * Understand the [technical components](/en/platform/access-manager/am-components) of Access Manager * Review [API reference](/en/reference/platform/access-manager/am-api-introduction) for programmatic access # What is Caradhras? Source: https://docs.lerian.studio/en/platform/access-manager/what-is-caradhras Caradhras is the identity provider behind Access Manager. It manages users, organizations, and applications, and supports the standard identity protocols. Caradhras is the identity provider that Access Manager runs on. It is a self-hostable identity and access-management server. Caradhras holds the objects behind every access decision: * **Organizations**, the boundary that owns every other object. * **Users**, the people who sign in. * **Groups** and **roles**, the two ways to collect subjects. * **Permissions**, which pair a resource with an action. * **Applications**, the OAuth clients that machine-to-machine callers use. * **Certificates**, the key material behind token signatures. You manage that data through the Identity APIs or Lerian Console. ## Protocols *** Caradhras supports these protocols and authentication methods: * OAuth 2.0 and OIDC * SAML 2.0 * CAS * SCIM 2.0 for user provisioning * LDAP: authenticates users against an external directory. Caradhras does not run a directory. * WebAuthn and passkeys * TOTP and MFA ## Authorization model *** Caradhras expresses authorization with Casbin, which supports ACL, RBAC, and ABAC models. A permission names a resource and an action. Access Manager evaluates that pair against the subject in the token before a product handler runs. Subjects nest. A group can belong to a parent group. A role can include other roles. ## Your identity provider *** Your identity provider connects to Caradhras as an upstream provider over OAuth 2.0 and OIDC, SAML 2.0, or LDAP. OIDC providers include Google, Microsoft Entra ID, Okta, or a custom OIDC provider. Your provider does not replace Caradhras. Your users then authenticate at your provider. Access Manager still reads their identity data from Caradhras, and it still decides what they can reach. ## How it ships *** Caradhras is part of the Access Manager deployment. You do not install it as a separate product. The [Access Manager Helm chart](https://github.com/LerianStudio/helm/tree/main/charts/plugin-access-manager) deploys Caradhras as the identity backend, and the Auth and Identity services call it. Caradhras persists its identity data in the PostgreSQL database that you configure for Access Manager. To see what each Access Manager service uses the backend for, read [Caradhras in Access Manager](/en/platform/access-manager/caradhras-in-access-manager). # What is Lerian Console? Source: https://docs.lerian.studio/en/platform/console/about-lerian-console Manage and operate Lerian products from a single web interface with unified authentication, navigation, and product administration. The **Lerian Console** is the single web interface for managing and operating Lerian products. You configure, monitor, and administer them from one place. The Console centralizes navigation, authentication, and product management. > [!NOTE] > > The Console is part of the **Enterprise plan**. It isn't sold separately. See [how Lerian is packaged](/en/products/products-and-plugins) or [talk to our team](https://lerian.studio/contact) for details. ## Architecture *** The Console has two layers: * **Platform layer**. Shared capabilities that apply across all products: centralized authentication, product catalog, global navigation, and language and theme preferences. * **Product layer**. Each Lerian product integrates as an independent module with its own navigation, workflows, and product-specific settings, while sharing the same design patterns. ## Key capabilities *** * **Single sign-on**: Log in once and access all enabled products without re-authenticating. * **Role-based access**: Control who can access which products and features based on roles and permissions. * **Product management**: Enable, disable, and configure products from a central dashboard. * **Unified experience**: Navigate between products with consistent UI patterns. ## Who uses the Console *** | Role | Primary activities | | - | - | | **Platform administrators** | Enable/disable products, manage users and roles, configure global settings | | **Product operators** | Access enabled products, perform day-to-day operations, configure product-specific settings | ## Console modules *** These products have a module in Lerian Console: > **[Midaz](/en/products/midaz/console/midaz-module)** > > Core ledger management: organizations, ledgers, accounts, assets, accounting, and transactions. > **[Reporter](/en/products/reporter/console/managing-reports)** > > Report generation, template management, and regulatory filing for financial and operational data. > **[Flowker](/en/products/flowker/console/flowker-module)** > > Design visual workflows, configure provider integrations, run automated processes, and monitor executions through the Flowker module. > **[Tracer](/en/products/tracer/console/tracer-module)** > > Configure real-time fraud detection rules, set spending limits, and review transaction validations. > **[Streaming Hub](/en/platform/streaming-hub/streaming-hub-in-lerian-console)** > > Browse the event catalog, create webhook and pull subscriptions, and follow delivery on the dashboard. > **[Matcher](/en/products/matcher/ui/matcher-module)** > > Configure reconciliation contexts and rules, run reconciliations, and resolve exceptions and disputes. ## Product versions *** Some Console screens need a minimum version of the product behind them. The Console reads the version that each product reports. When a product runs an earlier version, a notice replaces the part of the screen that needs that version. The notice names the product and the version, for example *Requires Bank Transfer 3.1.0 or later*. When the Console cannot read the version, it opens the screen. | Screen | Needs | | :- | :- | | The **Ledger activity** section of the Midaz home page | Midaz 4.1.0 or later | | The Tracer **Dashboard**. With an earlier Tracer, the module opens on a cover page instead. | Tracer 4.1.0 or later | | The Bank Transfer **Transfers** list, the **Recent Transfers** card, and the transfer detail page | Bank Transfer 3.1.0 or later | | The **Environments** page | Access Manager 3.11.0 or later | | In **Settings > Partners**: creating a partner, the **Permissions** and **Scope** tabs of a partner, and creating a partner application | Access Manager 3.11.0 or later | # About settings Source: https://docs.lerian.studio/en/platform/console/about-settings What the Settings area of Lerian Console contains, which sections need Access Manager, and where user and permission management lives. Open Settings with the gear icon () in the top navigation bar. ## Sections *** > **[Applications](/en/platform/console/managing-applications)** > > The Access Manager applications that authenticate external systems. Create them, view their details, and delete them. > **[Partners](/en/platform/access-manager/features/partners/console)** > > Your own customers that call your platform from their own systems. Create them, choose what each one can reach, and create their applications. > **[Authorization](/en/platform/console/managing-users)** > > Users, roles, groups, and permissions, in an area with its own sidebar. > **[Security](/en/platform/access-manager/features/ip-allowlist/console)** > > Your own multi-factor authentication, plus the single sign-on providers and the IP allowlist when your permissions include them. > **[Products](/en/platform/console/settings-products)** > > Turn Lerian products on or off for your tenant. > **[System](/en/platform/console/settings-system)** > > System preferences, such as the interface language and the theme. > **[Notification Layer](/en/platform/console/notification-layer)** > > Notification delivery by email, SMS, and webhook, in an area with its own sidebar. ## What each section needs *** Applications, Partners, Authorization, and Security need Access Manager. The Partners entry disappears when [`NEXT_PUBLIC_PARTNERS_ENABLED`](/en/platform/console/console-environment-variables) is `false`. When your account lacks the read permission on applications, the Console hides the Applications entry in the Settings sidebar. It does not show an error. Without the read permission on roles, it also hides the Authorization entry. Security always appears. Its **MFA** tab is open to every signed-in user. The **Single Sign-On** tab needs the read permission on providers. The **IP allowlist** tab needs the read permission on security. Products and System open without Access Manager and without a permission. With Access Manager, the **Notification Layer** entry needs the read permission on notifications. ## Where users live *** User, role, group, and permission management lives in a separate area called Authorization, with its own sidebar. Open it from the **Authorization** entry in the Settings sidebar, or from the gear menu in the top bar. > **[Managing users](/en/platform/console/managing-users)** > > Create, edit, and delete user accounts, reset passwords, and read how groups, roles, and permissions give a user access. # Console environment variables Source: https://docs.lerian.studio/en/platform/console/console-environment-variables The environment variables that connect Lerian Console to Midaz, Tracer, and Access Manager, and the ones that switch Console screens on or off. Console reads these variables when its container starts. After you change one, restart the Console container. You do not need to rebuild the image. To install or upgrade Console, see the [Console chart README](https://github.com/LerianStudio/helm/tree/main/charts/product-console). ## Product addresses | Variable | Required | What it does | | :- | :- | :- | | `MIDAZ_V2_BASE_PATH` | Yes | The address of the Ledger `/v2` API, ending in `/v2/`. Console reads Fees, Holders, and Alias Accounts from it. Without it, these pages fail. | | `TRACER_BASE_PATH` | Only with Tracer | The address of the Tracer service, without `/v1`. | ## Secrets Keep `NEXTAUTH_SECRET`, `MONGODB_PASS`, and `PLUGIN_AUTH_CLIENT_SECRET` in your secret vault. Never put them in a file in your repository or in an image. ## Screens | Variable | What it controls | | :- | :- | | `NEXT_PUBLIC_MIDAZ_CONSOLE_AVATAR_ALLOWED_FORMAT` | The image formats accepted for avatars, separated by commas, for example `jpeg,jpg,png`. | | `NEXT_PUBLIC_PARTNERS_ENABLED` | The **Partners** entry of Settings and the **Partners** card on the home page. They are on unless the value is `false`. | | `NEXT_PUBLIC_BANK_TRANSFER_ENABLED` | The Bank Transfer screens of the Midaz module. They are off unless the value is `true`. | # Environments Source: https://docs.lerian.studio/en/platform/console/environments See the environments where you have an account in Lerian Console, and choose your default environment. The **Environments** page lists the environments where you have an account. Use it to choose your default environment. When you have an account in more than one environment, you sign in to your default environment. The page needs Access Manager 3.11.0 or later. With an earlier version, a version notice replaces the list. See [Product versions](/en/platform/console/about-lerian-console#product-versions). ## Accessing the Environments page *** In the top navigation bar, click the name of your current environment. On a narrow screen, open your profile menu and select the environment name. If the Console cannot read your environments, the navigation bar shows no environment name. ## Your environments *** The **Your environments** table has one row for each environment: * **Environment name**: the name of the environment. * **Environment status**: the **Default** badge marks your default environment. The **Current** badge marks the environment that you use now. An **SSO** badge marks an environment that has single sign-on turned on. ## Changing your default environment *** **1. Click Make default** On the row of the environment that you want, click **Make default**. Your default environment has no **Make default** button. **2. Confirm the change** In the dialog, click **Make default and sign out**. Make default signs you out. If the change fails, the Console shows an error message and keeps the dialog open, so you can try again. # Managing Applications Source: https://docs.lerian.studio/en/platform/console/managing-applications Create, inspect, and delete the Access Manager applications that authenticate external systems, from the Applications page of Lerian Console. An application is an OAuth client: an external system or service that connects to your API through authentication with an identity provider. Each application has its own credentials (client ID and secret) that authorize and authenticate access to the APIs. On Lerian Cloud, the list shows only the applications of the logged-in administrator's tenant. Console does not expose applications from other tenants. ## The Applications page *** Click the gear icon () in the top navigation bar, then select **Applications**. The section needs Access Manager and the read permission on applications. Without that permission, Applications does not appear in the Settings sidebar. The table has five columns: | Column | What it shows | | - | - | | **Name** | The application identifier. | | **Description** | The description of the application. A dash (—) shows when it has none. | | **ClientId** | The client identifier for authentication. | | **Creation Date** | The date on which someone created the application. | | **Actions** | The row menu, which opens the details panel or deletes the application. | The default Lerian application is protected: its row shows a lock instead of the actions menu, so you cannot view or delete it here. The footer gives the number of applications in the list. The list does not paginate. A **Security Warning** banner stands above the table. It asks you to: * Keep the client ID and the client secret confidential. * Store the keys in a secure location. * Delete an application only when you are sure. * Rotate the credentials when you suspect a compromise. * Keep the keys out of frontend code and public repositories. ## Create an application *** **1. Open the form** Click **New Application**. The form opens on the right side of the screen. **2. Choose the application name** Pick a name in the **Application Name** list. The list comes from the platform. You cannot type a name that is not on it. **3. Describe the application** Write a short **Description** of what the application does. **4. Save** Click **Save**. This step needs the create permission on applications. The Console answers with the message "Application successfully created". The new application appears in the list. The Console shows the client secret once, when you create the application. Store it then. If you lose it, delete the application and create a new one. ## View an application *** Find the application, open the menu in the **Actions** column, and select **Details**. The **Application Details** panel opens on the right side of the screen with three read-only fields: **Name**, **Description**, and **ClientId**. A copy button sits next to the **ClientId**, in the panel and in the table. > [!NOTE] > > Application details cannot be edited. To change one, delete it and create it again. ## Delete an application *** > [!WARNING] > > Deleting an application **revokes access to all connected services.** It is irreversible and permanently removes the application. **1. Open the row menu** Find the application, open the menu in the **Actions** column, and select **Delete**. **2. Confirm** A dialog asks "Are you sure you want to delete?". Click **Yes, delete it**. To stop, click **I changed my mind**. The Console answers with the message "Application successfully deleted". ## What the Console does not expose *** To set up single sign-on providers, open **Settings** › **Security** › **Single Sign-On**. Use the Identity APIs for other application settings that Console does not expose. # Managing Users Source: https://docs.lerian.studio/en/platform/console/managing-users Create, edit, reset the password of, and delete user accounts, and manage roles, groups, and permissions in the Authorization area of Lerian Console. Users live in the Authorization area of Lerian Console, next to Roles, Groups, and Permissions. Open Authorization from the **Authorization** entry in the Settings sidebar, or from the gear menu in the top bar. Roles and permissions control what each user can do. The Users page needs Access Manager and the read permission on users. Without that permission, the Console shows a not-authorized page instead of the list. On Lerian Cloud, the list shows only the users of the logged-in administrator's tenant. Console does not expose users from other tenants. ## The Users page *** The table has six columns: | Column | What it shows | | - | - | | **Name** | The full name of the user, with the user identifier below it. | | **E-mail** | The e-mail address of the user. | | **Username** | The login identifier. | | **Groups** | The first group of the user, and a `+N` badge that lists the rest. | | **MFA** | The multi-factor authentication status of the user. | | **Actions** | The row menu. | You choose which columns the table shows. The list paginates, and the footer gives the range on screen and the total. The row menu holds three items: **View Details**, **Edit**, and **Delete**. The Console shows **Edit** only to an account with the update permission on users. It shows **Delete** only to an account with the delete permission on users. ## Create a user *** **1. Open the form** Click **New User**. The form opens on the right side of the screen. **2. Fill in the fields** The form asks for **Name**, **Last Name**, **Username**, **E-mail**, **Password**, **Confirm Password**, and **Groups**. **3. Save** Click **Save**. This step needs the create permission on users. The new user appears in the list. The Console applies these rules: * **Name** and **Last Name** are required, and **Username** holds at least 3 characters. * **Username** accepts letters, numbers, hyphens, and underscores. * **E-mail** holds a valid address. * **Password** holds at least 12 characters, with one uppercase letter, one lowercase letter, one number, and one special character. * **Password** rejects two identical characters next to each other, so `aa` and `11` both fail. * **Groups** accepts one or more groups. At least one is required. > [!NOTE] > > The Console checks these rules before it submits the form. The identity backend can apply further rules. ## Edit a user *** **1. Open the row menu** Find the user, open the menu in the **Actions** column, and select **Edit**. **2. Change the fields** The panel has two tabs: **Personal Information** and **Password**. On the **Personal Information** tab you change the **Name**, the **Last Name**, the **E-mail**, and the **Groups**. The tab holds no username field, so a user keeps the username they got at creation. **3. Save** Click **Save**. ## Reset a user's password *** **1. Open the Password tab** Select **Edit** in the row menu, then open the **Password** tab. **2. Type the new password** Fill in **New Password** and **Confirm Password**. The new password follows the rules in [Create a user](#create-a-user). **3. Save and confirm** Click **Save**. A dialog titled "Password Change" opens. Click **Yes, change password**. > [!NOTE] > > This is an administrator reset. It asks for no current password and sets no temporary password. The user signs in with the new one next time. > [!NOTE] > > To manage your own multi-factor authentication, use **Settings** › **Security**. To reset your own password, use **Forgot my password?** on the sign-in page. The link appears when the deployment sets `ENABLE_FORGOT_PASSWORD` to `true`. ## Delete a user *** > [!WARNING] > > Deleting a user **permanently removes the account.** This action cannot be undone. Find the user, open the menu in the **Actions** column, and select **Delete**. A dialog titled "Confirm Deletion" states: "You are about to permanently delete this user. This action cannot be undone. Do you wish to continue?" The Console then answers with the message "User successfully deleted". The Console does not offer **Delete** on your own account. ## Roles, groups, and permissions *** The Authorization sidebar has five entries: Overview, Users, Roles, Groups, and Permissions. A permission names a resource and an action. You grant it to a role, or directly to a user. A user gets a role directly or through a group. **View Details** in the row menu opens a page for one user. That page has six tabs: **Effective Access**, **Roles**, **Groups**, **Direct Permissions**, **Profile**, and **Security**. ## The Roles page *** The Roles table has six columns: **Name**, **Type**, **Users**, **Groups**, **Status**, and **Actions**. **Type** shows **System** or **Custom**. **Status** shows **Enabled** or **Disabled**. The platform manages the system roles. A system role has no row menu. Its **Actions** cell reads "This role is managed by the platform and is not editable from the console". The row menu of a custom role holds **View Details**, **Edit**, and **Delete**. The Console shows **Edit** only to an account with the update permission on roles. It shows **Delete** only to an account with the delete permission on roles. ## Create a role *** **1. Open the form** On the Roles page, click **New Role**. The form opens on the right side of the screen. **2. Fill in the fields** The form asks for **Name**, **Display Name**, and **Description**. **3. Save** Click **Save**. This step needs the create permission on roles. The new role appears in the list. The Console applies these rules: * **Name** and **Display Name** are required. * **Name** holds 2 to 100 characters: letters, numbers, hyphens, and underscores. * **Display Name** holds at most 100 characters. **Description** holds at most 500. ## Edit a role *** **1. Open the row menu** Find the role, open the menu in the **Actions** column, and select **Edit**. **2. Change the fields** Change the **Display Name**, the **Description**, or the **Enabled** switch. The **Name** field is read-only, so a role keeps the name it got at creation. **3. Save** Click **Save**. ## Assign permissions, users, and groups to a role *** **View Details** in the row menu opens a page for one role. That page has three tabs: **Permissions**, **Users**, and **Groups**. Each tab lists what the role holds. **1. Open the tab** On the role page, open the **Permissions**, **Users**, or **Groups** tab. **2. Open the panel** Click **Add Permissions**, **Add Users**, or **Add Groups**. A panel opens with a search box. **3. Select the items** Select the items to add. An item that the role already holds shows "Already assigned". **4. Assign** Click the button at the bottom of the panel, for example **Assign 2 Permissions**. The **Add Permissions** panel also filters the list by **Resource**. To remove one item, use the remove button on its row. To remove more than one, select the rows and click **Remove Selected**. The Console asks you to confirm. A system role shows the notice "System Role". Its tabs offer no add or remove. ## Create a group *** **1. Open the form** On the Groups page, click **New Group**. The form opens on the right side of the screen. **2. Fill in the fields** The form asks for **Name**, **Display Name**, and **Contact Email**. **3. Save** Click **Save**. This step needs the create permission on groups. The new group appears in the list. **Name** and **Display Name** follow the same rules as for a role. **Contact Email** is optional. When you fill it in, it holds a valid address. The Groups table shows **Name**, **Members**, and **Actions**. The row menu holds **View Details**, **Edit**, and **Delete**. The Console shows **Edit** only to an account with the update permission on groups. It shows **Delete** only to an account with the delete permission on groups. ## Edit a group *** **1. Open the row menu** Find the group, open the menu in the **Actions** column, and select **Edit**. **2. Change the fields** Change the **Display Name** or the **Contact Email**. The **Name** field is read-only. **3. Save** Click **Save**. ## Add members and roles to a group *** **View Details** in the row menu of a group opens a page with two tabs: **Members** and **Roles**. **1. Open the tab** On the group page, open the **Members** or **Roles** tab. **2. Open the panel** Click **Add Members** or **Add Roles**. A panel opens with a search box. **3. Select and add** Select the users or roles. Click the button at the bottom of the panel, for example **Add 2 Members** or **Assign 2 Roles**. To remove a member or a role from the group, use the same buttons as on a role page. ## Browse permissions *** The Permissions page shows the permissions grouped by resource. Search by name, or filter by **Resource**. Click a resource name to open or close its table. Each resource table has six columns: | Column | What it shows | | - | - | | **Name** | The permission name, with the permission identifier below it. | | **Actions** | The actions that the permission covers. | | **Effect** | **Allow** or **Deny**. | | **Roles** | The roles that hold the permission, as links. | | **Users** | The number of users with the permission. | | **Groups** | The number of groups with the permission. | # Plugins in Midaz Module Source: https://docs.lerian.studio/en/platform/console/mc-console-plugins The cards the Plugins screen of the Midaz Module shows: Pix Indirect (BTG), Fees Engine, Bank Transfer (TED JD), and Payments. Plugins are optional capabilities that add features to the Midaz Module on top of Midaz Core. The Plugins screen also holds the Fees Engine card. Fees Engine is part of Midaz, not a plugin. ## What the Plugins screen lists *** Open the screen from the Midaz Module sidebar, at **Settings** › **Plugins**. This is the Midaz Module's own Settings, not the Console Settings area behind the gear icon. It shows up to four cards. Each card carries a name, a short description, and a toggle. > **[Pix Indirect (BTG)](/en/interfaces/pix-btg/indirect-pix-overview)** > > Connects your institution to Brazil's Pix instant payment system through BTG Pactual, the direct participant. > **[Fees Engine](/en/products/midaz/fees/fees-engine-overview)** > > Adds the Fee Packages, Calculator, Billing Packages, and Billing Runs screens to the Midaz Module sidebar. Fees runs inside Midaz and needs no separate service. > **Bank Transfer (TED JD)** > > Sends and receives TED bank transfers through the JD integration. > **[Payments](/en/interfaces/payments-btg/boleto-and-bill-payment)** > > Issues boletos and pays bills, including DARF tax documents. > [!NOTE] > > To show the Bank Transfer (TED JD) card, set `NEXT_PUBLIC_BANK_TRANSFER_ENABLED` to `true` and restart the Console container. You do not need to rebuild the image. ## How plugins work *** * **Settings controls availability.** Enable each plugin in [Plugin settings](/en/platform/console/plugin-settings) before its menu item appears in the Midaz Module sidebar. * **Disabling preserves data.** Turning a plugin off hides its UI. It does not delete configurations you created with it. Enabling Fees Engine checks that Midaz answers at the `MIDAZ_V2_BASE_PATH` address. Enabling Pix Indirect (BTG) checks that the Pix Indirect service answers when the Console has an address for it. If the check fails, the Console keeps the card off and shows an error. Payments and Bank Transfer (TED JD) turn on without that check. # Notification Layer Source: https://docs.lerian.studio/en/platform/console/notification-layer Follow notification delivery, inspect each notification and its delivery attempts, configure the email, SMS, and webhook delivery profiles, and change the delivery settings in Lerian Console. The Notification Layer is the Console area for the notifications that the platform sends by email, SMS, and webhook. Use it to: * Follow the delivery figures on the **Overview** screen. * Find a notification, read its delivery attempts, and retry or cancel it. * Configure the delivery profiles that send each channel. * Change the runtime settings of the delivery service. ## Open the Notification Layer *** Click the gear icon () in the top navigation bar, then select **Notification Layer**. The same entry is also in the Settings sidebar. The area has its own sidebar with four screens: **Overview**, **Notifications**, **Delivery Profiles**, and **Settings**. With Access Manager, the entry needs the read permission on notifications. Without that permission, **Notification Layer** does not appear in the gear menu or in the Settings sidebar. Each screen and action needs its own permission: | Screen or action | Permission with Access Manager | | - | - | | **Overview**, **Notifications**, and the detail of a notification | Read on notifications | | **Retry delivery** and **Cancel notification** | Create on notifications | | **Delivery Profiles** and the detail of a profile | Read on delivery profiles | | **New delivery profile** | Create on delivery profiles | | **Save changes** and **Edit credentials** on a profile | Update on delivery profiles | | **Delete** a profile | Delete on delivery profiles | | **Settings** | Read on systemplane, the resource that holds the runtime settings | | Save a change on **Settings** | Update on systemplane | ## Overview *** The **Overview** screen sums up notification delivery across all channels. Six cards stand at the top: | Card | What it shows | | - | - | | **Total** | The number of notifications. | | **Delivered** | The notifications with the status **Delivered**. | | **Delivery Rate** | The delivered notifications as a percentage of the delivered and failed ones together. | | **Failed** | The notifications with the status **Failed** or **Exhausted**. | | **Pending** | The notifications with the status **Pending** or **Processing**. | | **p95 Latency** | The 95th percentile of the delivery attempt latency. The 99th percentile shows below it. | Below the cards: * **Total Attempts** and **Avg Attempts per Notification** count the delivery attempts. * **Top Failure Reasons** shows the failures by reason, for example **Invalid recipient** or **Rejected by Twilio**. A reason without a label shows its failure code. * **Volume by Channel** shows the number of notifications on each channel. * **Recent Notifications** lists the latest notifications with their **Status**, **Channel**, **Recipient**, **Subject**, **Priority**, and **Created** date. If the Console cannot read the figures, the screen shows **Could not load notification overview** in place of all of them. Click **Retry** to read them again. ## Notifications *** The **Notifications** screen lists the notifications of the platform. The table has the columns **Status**, **Channel**, **Subject**, **Recipient**, **Priority**, and **Created**. To open a notification, open the menu in the **Actions** column and select **View**. ### Notification statuses | Status | Meaning | | - | - | | **Pending** | The notification waits for its next delivery attempt. | | **Processing** | A delivery attempt is in progress. | | **Delivered** | The provider accepted the message. This status is final. | | **Failed** | The last attempt failed. | | **Exhausted** | The service makes no more attempts, because the attempts ran out or the failure is permanent. | | **Canceled** | Someone canceled the notification. This status is final. | ### Filter the list Open the **Filters** panel to narrow the list: * **Status**: click one or more status chips. * **Channel**: **Email**, **SMS**, or **Webhook**. * **Recipient**: the exact recipient. * **Correlation ID**: the correlation identifier of the notification. * **Failure code**: the failure codes from the overview, the most frequent first. * **Created from** and **Created to**: the creation date range. A field with a value shows **Clear** next to its label. **Clear filters** empties every field. **Items per page** sets the page size. ### Notification detail The **Notification Detail** screen shows one notification in cards: * The first card shows the status, the channel, **ID**, **Subject**, **Recipient**, **Destination**, **Priority**, and **Correlation ID**. * **Delivery** shows **Attempts** (used and maximum), **Scheduled at**, **Next attempt at**, **Delivered at**, **Failed at**, **Canceled at**, **Canceled by**, **Provider message ID**, **Failure code**, **Created at**, and **Updated at**. * **Metadata** and **Failure detail** show as JSON when the notification has them. **Back to notifications** returns to the list. ### Delivery attempts The **Delivery attempts** card lists the attempts, oldest first, as **Attempt 1**, **Attempt 2**, and so on. Each attempt shows: * Its result and the time of the attempt. * **Provider**: the delivery profile that made the attempt. * **Response**: the response code, when there is one. * **Latency**: the duration of the attempt, in milliseconds. * The error message, when the attempt failed. * **Response body**: click it to expand the body that the provider returned. When the notification has no attempt yet, the card shows **No delivery attempts recorded yet.** ### Retry or cancel a notification **Retry delivery** is available when the status is **Failed** or **Exhausted**. Confirm in the **Retry delivery?** dialog. The notification goes back to **Pending**, and the service attempts delivery again. **Cancel notification** is available when the status is **Pending**, **Processing**, or **Failed**. Confirm in the **Cancel notification?** dialog. You cannot undo a cancellation. The service makes no new delivery attempt. ## Delivery Profiles *** A delivery profile connects a channel to a provider and holds the credentials for that provider. The **Delivery Profiles** screen lists the profiles with the columns **Name**, **Channel**, **Provider**, **Status**, **Priority**, **Default**, and **Updated**. The row menu in **Actions** has **View** and **Delete**. When a channel has more than one active profile, the service uses the profile with the highest priority first. A tie goes to the default profile, then to the oldest profile. The service skips inactive profiles. ### Create a delivery profile **1. Open the form** Click **New delivery profile**. **2. Name the profile** Type a **Name**. **3. Choose the channel and the provider** Select the **Channel**, then the **Provider**. You cannot change either one after you create the profile. **4. Set the selection fields** Set the **Status**, the **Priority**, and the **Default for this channel** switch. **5. Set the limits** Optionally, type a **Rate limit (per second)** and a **Daily limit**. An empty limit means no limit. **6. Fill in the provider configuration** Fill in the fields of the **Provider configuration** section. The fields change with the provider, as the next sections show. **7. Save** Click **Save**. The Console returns to the list. ### Email and SMS providers Each channel has its own providers. An asterisk (\*) marks a required field. | Channel | Provider | Configuration fields | | - | - | - | | **Email** | **SMTP** | **Host**\*, **Port**\*, **From**\*, **Username**, **Password** | | **Email** | **AWS\_SES** | **AWS region**\*, **Access key ID**\*, **Secret access key**\*, **Session token**, **From**\* | | **Email** | **SENDGRID** | **API key**\*, **From**\*, **From name** | | **SMS** | **TWILIO** | **Account SID**\*, **Auth token**\*, **From**\* | | **SMS** | **ZENVIA** | **API token**\*, **From**\* | | **SMS** | **INFOBIP** | **API key**\*, **Base URL**\*, **From**\* | **From** is the sender that recipients see, such as an email address or an approved SMS sender ID. The SMTP **Port** accepts a value from 1 to 65535. ### Webhook provider The **Webhook** channel has one provider, **HTTP\_WEBHOOK**. The profile does not hold the endpoint address: each webhook notification carries its own address in **Destination**. The webhook profile has these fields: * **Signing secret (HMAC)**: optional. The service signs the request body with this secret, so that the destination can verify the sender. * **Destination auth**: how the service authenticates to the destination. Select **none** for no authentication. * **Custom headers**: optional name and value pairs that the service sends on every request. The **Destination auth** value adds its own fields: | Destination auth | Fields | | - | - | | **basic** | **Username**\*, **Password**\* | | **bearer\_static** | **Token**\* | | **oauth2\_client\_credentials** | **Auth URL**\*, **Client ID**\*, **Client secret**\*, **Scope**, **Audience** | | **jwt\_exchange** | **Auth URL**\*, **Token path**\*, **Expires-in path**\*, **HTTP method**, **Custom headers**, **Request body (JSON)** | For **oauth2\_client\_credentials** and **jwt\_exchange**, **Auth URL** is the token endpoint that the service calls before it delivers the webhook. For **jwt\_exchange**, **Token path** and **Expires-in path** are the JSON paths of the access token and of its lifetime in the response. ### Delivery profile detail The **Delivery Profile** screen shows the status, the channel, and the provider of the profile at the top. You can change these fields: * **Name** * **Status** * **Priority** * **Rate limit** * **Daily limit** * **Default profile for this channel** Click **Save changes** to save them. After the priority or a limit has a value, you can change that value, but you cannot clear it. The **Lifecycle** section shows **ID**, **Channel**, **Provider type**, **Consecutive failures**, **Last success**, **Last failure**, **Created at**, and **Updated at**. **Consecutive failures** goes back to zero after the next successful delivery. ### Replace the credentials The **Provider configuration & credentials** section does not show the stored credentials. To replace them: **1. Open the editor** Click **Edit credentials**. **2. Fill in every field** Fill in all the fields you want to keep. Secret fields, such as passwords, API keys, and tokens, open empty. **3. Save** Click **Save credentials**. The new values replace the full provider configuration. To stop, click **Cancel**. ### Delete a delivery profile Click **Delete** on the profile screen, or select **Delete** in the row menu of the list. Then click **Confirm** in the dialog. ## Settings *** The **Settings** screen shows the runtime settings of the delivery service, by group. Changes apply without a restart. Each row shows the setting, its key, a description, and the allowed values. | Group | Setting | Allowed values | | - | - | - | | **Delivery & Retry** | **Max delivery attempts** | 1 to 20 | | **Delivery & Retry** | **Retry base delay (s)** | 5 to 300 | | **Delivery & Retry** | **Retry max delay (s)** | 60 to 86400 | | **Delivery & Retry** | **Email concurrency** | 1 to 100 | | **Delivery & Retry** | **SMS concurrency** | 1 to 50 | | **Delivery & Retry** | **Webhook concurrency** | 1 to 100 | | **Delivery & Retry** | **Webhook timeout (s)** | 5 to 120 | | **Circuit Breaker** | **Failure threshold** | 1 to 50 | | **Circuit Breaker** | **Recovery timeout (s)** | 10 to 600 | | **Rate Limiting** | **Default max requests**, **Aggressive max requests**, **Relaxed max requests** | 1 to 100000 | | **Rate Limiting** | **Default window (s)**, **Aggressive window (s)**, **Relaxed window (s)** | 1 to 3600 | | **M2M Credentials Cache** | **Credential cache TTL (s)** | 30 to 86400 | | **Logging** | **Log level** | debug, info, warn, error | To change settings: **1. Find the setting** Type in the search box, or select a group in the list next to it. **All groups** shows every group. **2. Change the value** Change the value. The screen marks each changed row. **3. Save** Click **Save** with the number of changes, for example **Save 2 changes**. If a value is out of range, the screen saves nothing and asks you to fix the marked fields. If some changes fail, the screen keeps them marked, so you can save them again. When runtime configuration is off on the delivery service, the screen shows **Runtime configuration plane is disabled**, and you cannot change the settings. # Plugin settings Source: https://docs.lerian.studio/en/platform/console/plugin-settings Enable or disable Midaz Module plugins from the Plugins screen of Midaz Settings. The **Plugins** screen of **Midaz Settings** is where you enable and disable plugins for the Midaz Module. > [!NOTE] > > Enabling a plugin adds its menu item to the Midaz Module sidebar. Disabling it hides the menu item without deleting any configuration you have already created. ## Plugins *** The page shows three cards: **Pix Indirect (BTG)**, **Fees Engine**, and **Payments**. A fourth card, **Bank Transfer (TED JD)**, appears only when the deployment turns it on. [Plugins in Midaz Module](/en/platform/console/mc-console-plugins) explains the setting. Each card shows the plugin name, a short description, and a toggle. ### Fees Engine card Fees Engine is part of Midaz, not a plugin. Its card on this screen turns the Fees screens of the Midaz Module on and off. > **Fees Engine** > > Configure fee packages and billing packages, and estimate the fees charged on a transaction. > > **Key features**: > > * Create and manage Fee Packages > > * Define fee calculation rules (flat, percentage, or maximum between both) > > * Run fee estimations before applying to real transactions > > * Associate fees with specific ledgers, segments, or transaction routes > > * Create billing packages and review billing runs > > > **[Learn more about Fees Engine](/en/products/midaz/fees/console/fees-engine-overview)** > > > > Complete guide to managing fees through Lerian Console. ## Enabling or disabling a plugin *** **1. Open Settings** From the Midaz Module sidebar, click **Settings** > **Plugins**. The entry is available after you select a ledger. **2. Locate the plugin** Find the plugin in the list. **3. Toggle the switch** Use the toggle on the right side of the plugin card to enable or disable it. # Products Source: https://docs.lerian.studio/en/platform/console/settings-products Enable or disable Lerian product modules from Console Settings to control which capabilities and screens are available to your Console users and operators. Use the Products page to enable or disable Lerian products for your tenant. This controls which modules are available to users in the Lerian Console. You can also enable a product from the Console home page. The card of each product that is off has an **Enable** button. With Access Manager, the button needs the update permission on products. ## Accessing the Products page *** To access the **Products** section: **Step 1** Click the gear icon () in the top navigation bar. **Step 2** Select **Products** from the sidebar. **Step 3** The **Products** page displays all available products with toggle switches to enable or disable each one. ## Available products *** | Product | Description | | - | - | | **Midaz** | Core ledger management module: organizations, ledgers, assets, accounts, portfolios, and transactions. | | **Reporter** | Report generation module: templates, reports, data sources, and regulatory filing. | | **Tracer** | Transaction validation module. Provides rule-based validation for financial transactions before they are committed to the ledger. | | **Flowker** | Workflow orchestration module with visual automation and execution in real time. | | **Streaming Hub** | Event delivery module. Delivers platform events to your own webhooks and queues. | | **Matcher** | Reconciliation module: contexts, rules, reconciliations, and exceptions. | ## Enabling or disabling a product *** To change a product's status with Access Manager, you need the update permission on products. Without it, the toggle switches are disabled. To change a product's status: **Step 1** From the **Products** page, locate the product you want to enable or disable. **Step 2** Click the toggle switch next to the product. **Step 3** To turn a product on, the Console checks that its service answers and shows "Product enabled". If the service does not answer, the product stays off. To turn a product off, confirm with **Turn off** in the dialog. The Console shows "Product disabled". # System Source: https://docs.lerian.studio/en/platform/console/settings-system Set the language and the theme of the Lerian Console interface. The System page holds two display preferences: the interface language and the theme. ## Accessing the System page *** To access the **System** section: **Step 1** Click the gear icon () in the top navigation bar. **Step 2** Select **System** from the sidebar. **Step 3** The page shows the **Language** and **Theme** fields. ## Available settings *** ### Language Select the display language for the Lerian Console interface. It sets the language of the Console labels and messages. **Languages available:** * English - EN-US * Português - PT-BR ### Theme Select the appearance theme of the Lerian Console interface. **Themes available:** * Light: Uses light backgrounds and dark text. * Dark: Uses dark backgrounds and light text to reduce visual strain in low-light environments. * System: Automatically matches the appearance settings defined by your device or operating system. ## Altering system settings *** To change the settings: **Step 1** On the **System** page, open the drop-down list that you wish to change (**Language** or **Theme**). **Step 2** Select the option you want. **Step 3** Click **Save** in the bar at the bottom of the page. The bar appears after you change a field. **Step 4** The interface updates immediately to display the content in the selected option. > [!TIP] > > The Console keeps these preferences in your browser, not in your user account. Another browser or device keeps its own. > > If you change the language by mistake, open the **Language** list again. It shows each language in its own name. # License Manager Source: https://docs.lerian.studio/en/platform/licensing/license-manager How a licensed Lerian product validates its license key, what each result allows, and how it behaves when the license service does not answer. Lerian issues a license key for each Lerian product that requires one. The product validates the key when it starts, and again at a fixed interval. This page describes what you configure, which host the product calls, and what each result allows. ## What a license key is *** A license key covers one product and the organization identifiers that you declare for it. A product accepts the key only for those organizations, and only for itself. Store the key as a secret, and keep it out of source control. ## What the product reads from the environment *** | Variable | Content | | :- | :- | | `LICENSE_KEY` | The key that Lerian issued for this product. | | `ORGANIZATION_IDS` | The organization identifiers that the key covers, separated by commas, or `global` when Lerian licenses the whole deployment. Some products, such as Lerian SISBAJUD, accept only `global`. | | `IS_DEVELOPMENT` | Optional. Set it to `true` to validate against `https://license.dev.lerian.io` instead of the production host. | In production, most products do not start without `ORGANIZATION_IDS`. Some products, such as the Lerian SISBAJUD executor, do not start without it in any environment. ## How the check runs *** The product sends its own name, the license key and one organization identifier to `https://license.lerian.io`. Lerian operates that service. Open outbound HTTPS to that host from every workload that runs a licensed product, or every check fails. A workload that sets `IS_DEVELOPMENT` to `true` needs the same rule for `https://license.dev.lerian.io`. Lerian STA can read a signed license file instead of a key. When `LICENSE_FILE_PATH` is set, Lerian STA verifies the file locally and does not call `https://license.lerian.io`. See the [Lerian STA environment variables](/en/rails/sta/sta-environment-variables). The product validates at startup, then repeats the check in the background at a fixed interval. One attempt waits up to five seconds for an answer. ## What each result allows *** | Result | What the product does | | :- | :- | | Active | The product runs. It logs a warning 30 days before expiry, and a stronger warning in the last 7 days. | | Trial | The product runs and logs the days that remain. It logs a warning in the last 2 days. | | Expired, grace period open | The service grants 14 days of grace from the day it records the expiry. The product runs and logs a warning. | | Grace period closed | The service suspends the key. The product stops. A Bank Transfer deployment that serves several organizations stops serving only that organization. | | No valid license at startup | The product stops and writes the reason to the log. | ## When the license service does not answer *** > [!WARNING] > > If the license service stays unreachable, the product can stop serving. A product that starts while it cannot reach the license service stops, at once or within six hours. Monitor the outbound rule and the validation logs. ## Error codes *** Each failure carries a stable code. The [License Manager error list](/en/platform/licensing/license-manager-error-list) gives the codes and the messages that come with them. # License Manager error list Source: https://docs.lerian.studio/en/platform/licensing/license-manager-error-list The License Manager error codes and their messages, so you can diagnose a license failure on a Lerian product. A licensed product can return one of these codes when it cannot confirm a valid license. Quote the code when you contact support. [License Manager](/en/platform/licensing/license-manager) describes the environment that the check reads. ```json theme={null} { "code": "", "title": "", "message": "" } ``` Bank Transfer answers with its own problem document instead, and carries the LCS code in `refusalCode`. When the license service refused the key, the body can also carry `gatewayCode`, the code that the service returned. ## Errors that the product returns *** `\` is the identifier that the product reports back to you. During shutdown, a product can answer LCS-0003 or LCS-0013 with a message that reports the shutdown. | `code` | `message` | | :- | :- | | LCS-0001 | The server encountered an unexpected error. Please try again later or contact support. | | LCS-0003 | 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 | The X-Organization-Id header is missing. Please ensure the header is included in the request. | | LCS-0011 | The organization ID '\' was not found in the list of configured organizations. Please verify you are using a valid organization ID. | | LCS-0012 | License validation failed for organization ID \. 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 | The license for organization ID '\' is not valid and has no grace period active. Please renew your license or contact support for assistance. | ## Errors that the license service returns *** The license service returns these codes. A product can pass the code on to the caller. | `code` | `message` | | :- | :- | | LCS-1001 | Could not validate license key. Please try again later or contact your system administrator. | | LCS-1002 | There is no license for this license key. Please ensure the license key is correct and try again. | | LCS-1003 | The license key has expired. Please contact your system administrator to renew your license. | | LCS-1004 | The license key has been suspended due to payment issues or administrative action. Please contact your system administrator. | | LCS-1005 | The license key or organization IDs are not valid for this license. Please ensure the license key and organization IDs are correct and try again. | | LCS-1006 | There is no license for this product. Please ensure the license key is correct and try again. | | LCS-1007 | The license key cannot be empty. Please set the license key in the environment variables. | | LCS-1008 | The organization IDs cannot be empty. Please set the organization IDs in the environment variables. | | LCS-1009 | The application name cannot be empty. Please set the application name in the environment variables. | # Overview Source: https://docs.lerian.studio/en/platform/lifecycle-management Lerian installs and operates Lerian products in your Kubernetes cluster through the Lerian BYOC agent, which opens every connection to Lerian from your cluster. **Lifecycle Management** runs through the Lerian BYOC agent. You install the agent in your Kubernetes cluster. Lerian then installs, upgrades, rolls back, and removes Lerian products in your cluster through the agent. The agent installs and operates applications. It does not create cloud infrastructure, and it holds no cloud credential. > [!NOTE] > > Lifecycle Management is available to **Enterprise** clients on the [BYOC deployment model](/en/start-here/evaluate-and-deploy/deployment-models). In Lerian Cloud, Lerian runs the deployment and lifecycle operations for you, and you do not need the agent. ## How the agent connects to Lerian *** * You install one agent in each cluster. * The agent opens every connection to Lerian. Your cluster needs no inbound access from Lerian. * The agent connects to Lerian over HTTPS. * Each agent has its own credential. Lerian identifies your company from that credential, not from data that the agent sends. * Inside the cluster, the agent serves a health and metrics endpoint. ## What Lerian does through the agent *** At your request, Lerian runs these operations in your cluster through the agent: * Install a Lerian product. * Upgrade a product. * Roll back a product to an earlier version. * Remove a product. * Check that the cluster meets the needs of a product before the install. If an upgrade fails, the agent rolls the product back to its last working version. The agent does not roll back after a database schema change ran. In that case, the product stays on the failed upgrade. The agent pulls a product's chart from an allowed registry, by the exact content digest that Lerian names. It refuses a chart that has no digest. The agent also reports to Lerian the health of the products that Lerian asks it to observe. Lerian also updates the agent itself. The agent first runs the new build once in your cluster as a test. It replaces itself only when that test passes. ## What you control *** * **Namespaces**: You create the namespaces where the agent can install products, and you list them when you install the agent. To add a namespace later, upgrade the agent chart. * **Registries**: You set the registries from which the agent can pull product charts and its own image. * **Private registry credential**: If your registry needs a credential, the agent sends it only to that registry. * **Your secret manager**: The agent can use your own secret manager through External Secrets. * **Network**: You can limit the outbound traffic of the agent, and set a proxy and your own certificate authorities. * **Removal**: You can uninstall the agent. For each of these settings, see the [agent chart README](https://github.com/LerianStudio/helm/tree/main/charts/agent). ## What Lerian sees *** The agent sends information about your cluster and your products to Lerian. For example: * **At regular intervals**: The status of the agent, and the health of each product that it observes. * **When a product looks unwell**: The last output of its stopped containers, and the warning events about the product. * **On request from a Lerian operator**: Product status, an upgrade preview, the computed configuration values, live resource usage, and the cluster check result. For the full inventory, with when each item leaves and where it lands, see "What leaves your cluster" in the [agent chart README](https://github.com/LerianStudio/helm/tree/main/charts/agent). ## Security *** * The agent runs as a non-root user, with a read-only file system and no added privileges. * Across the cluster, the agent reads information such as nodes, pods, and services. It can also create namespaces and manage storage classes. * In the namespaces that you list, the agent installs products and writes their Secrets. * Install the agent in a dedicated namespace. ## Get started *** **1. Ask Lerian support** Ask Lerian support for the agent credential and the address of the Lerian control plane. **2. Create the namespaces** Create the namespaces where Lerian installs products. Each namespace must exist before you install the agent. **3. Install the agent** Install the agent with the agent chart, as the [agent chart README](https://github.com/LerianStudio/helm/tree/main/charts/agent) describes. Use the credential, the address, and the namespaces from the previous steps. ## Manage installed products *** To install, upgrade, roll back, or remove a product, send a request through the [Lerian Support Portal](/en/products/midaz/lerian-support-portal). Lerian runs the operation in your cluster through the agent. To remove the agent itself, uninstall its chart. See the [agent chart README](https://github.com/LerianStudio/helm/tree/main/charts/agent). ## Related *** > **[Agent chart README](https://github.com/LerianStudio/helm/tree/main/charts/agent)** > > Install, configure, and uninstall the Lerian BYOC agent. > **[Deployment models](/en/start-here/evaluate-and-deploy/deployment-models)** > > Compare Lerian Cloud and BYOC. > **[Lerian Support Portal](/en/products/midaz/lerian-support-portal)** > > Send requests to Lerian support. # Multi-tenancy Source: https://docs.lerian.studio/en/platform/multi-tenancy How Lerian Cloud and supported BYOC products isolate tenants, and how to choose dedicated or shared storage. Multi-tenancy lets one deployment serve independent institution contexts (**tenants**) while keeping each request and its data within the authenticated tenant's scope. Lerian operates Lerian Cloud as a multi-tenant environment. In BYOC, multi-tenancy is available only for products and entitlements that support it. A BYOC Single-Tenant deployment is a separate configuration. Tenant scoping and configuration are product-specific. Use the authentication and configuration guide for the product that you deploy. Do not assume an environment variable, JWT claim, or API behavior documented for one product applies to another. ## Authentication and request scoping *** Each product validates the caller's identity and derives its tenant context according to that product's authentication contract. The platform does not define one universal `tenantId` claim or one universal tenant header for every product. A product integration must follow that product's documented claim and routing behavior. Do not add a tenant identifier to a request unless that product explicitly requires it. > [!NOTE] > > Multi-tenancy is an operational capability. It does not mean that every Lerian product has the same authentication middleware or configuration surface. ## Storage isolation *** * **`dedicated`**: a tenant receives a dedicated PostgreSQL database. * **`shared`**: tenants share a PostgreSQL database and each receives a dedicated schema. The database-versus-schema split is specific to PostgreSQL. Other datastores use their own mode-specific routing and provisioning rules. Do not infer PostgreSQL schema behavior for MongoDB or RabbitMQ. | Dimension | `dedicated` | `shared` | | :- | :- | :- | | **PostgreSQL isolation** | Separate database per tenant. | Separate schema in a shared database. | | **Operational blast radius** | A database-level incident is limited to that tenant's database. | A database-level incident can affect tenants sharing the instance. | | **Cost and density** | Higher isolation, more infrastructure per tenant. | Higher density, less infrastructure per tenant. | | **Restore design** | Plan and test restores for the dedicated database. | Plan and test restores against the shared database and its schemas. | Choose the mode per service according to the service's supported configuration, regulatory obligations, expected workload, and recovery requirements. ## Moving a tenant between modes *** Moving a tenant from shared to dedicated storage is an operator-planned migration. The platform has no generic automatic workflow for it. Validate the target product's migration path, data-consistency requirements, maintenance window, backup/restore plan, and rollback procedure before changing a tenant's storage mode. The tenant identity can remain stable, but do not assume every product can move data between modes without an implementation-specific migration. ## Operating each deployment model *** ### Lerian Cloud Lerian operates the multi-tenant infrastructure. Follow the product's API and authentication documentation. Your token and the product's routing contract determine the request scope. ### BYOC Multi-Tenant The operator configures the supported products, tenant services, storage mode, backing resources, and authentication. Treat each product's configuration reference as authoritative. ### BYOC Single-Tenant or local development Multi-tenancy is not automatic. Authentication requirements depend on the product and environment. For Midaz, production and multi-tenant deployments require authentication. A permitted non-production single-tenant deployment can disable it. ## Related pages *** > **[Use cases](/en/platform/multi-tenancy/use-cases)** > > How to choose dedicated or shared PostgreSQL isolation. > **[Deployment models](/en/start-here/evaluate-and-deploy/deployment-models)** > > Compare Lerian Cloud, BYOC Single-Tenant, and supported BYOC Multi-Tenant configurations. > **[Access Manager](/en/platform/access-manager)** > > Understand the supported authentication flows for the products you deploy. # Multi-tenancy use cases Source: https://docs.lerian.studio/en/platform/multi-tenancy/use-cases Choose dedicated or shared PostgreSQL isolation for a supported BYOC tenant service. Choose an isolation mode per tenant service after you have confirmed that the product supports multi-tenancy. This page uses the isolation modes `dedicated` and `shared` as they apply to PostgreSQL. ## Choose `dedicated` when isolation dominates *** Use a dedicated PostgreSQL database per tenant when the tenant needs a strong infrastructure boundary. The same applies when the tenant needs independent database maintenance windows, or a recovery plan designed around a dedicated database. Typical examples include a highly regulated institution, a high-volume tenant, or a tenant whose workload must not share a PostgreSQL instance with other tenants. This choice increases infrastructure and operational cost, so validate the product's connection, backup, and migration design first. ## Choose `shared` when density dominates *** Use a dedicated schema per tenant in a shared PostgreSQL database when your operating model values density and lower per-tenant infrastructure cost. The supported product must also be able to operate in `shared` mode. A database-level incident or maintenance action can affect tenants on the shared instance. Plan capacity, backups, recovery, and maintenance around that shared blast radius. ## Treat migration as a product operation *** Do not rely on a generic automatic promotion from `shared` to `dedicated`. Moving a tenant to dedicated storage requires an operator-planned migration that the target product supports. Before changing a registration, establish: 1. the target product's supported migration procedure 2. a tested backup, consistency, and rollback plan 3. a maintenance window and communication plan 4. post-migration checks for authentication, routing, and tenant data. ## Decision checklist *** | Question | Favors `dedicated` | Favors `shared` | | :- | :- | :- | | Does the tenant require a dedicated database boundary? | Yes | No | | Can the tenant share a database-level maintenance and incident domain? | No | Yes | | Is per-tenant infrastructure cost acceptable? | Yes | No | | Does the product explicitly support the selected mode? | Required | Required | ## Related pages *** > **[Multi-tenancy](/en/platform/multi-tenancy)** > > Product support, request scoping, and storage concepts. # Automation and CI Source: https://docs.lerian.studio/en/platform/narya/automation-and-ci Run Narya from a pipeline with one command and an exit code, put work on a clock, and orchestrate several agents from a script. A pipeline drives Narya the way you do: the binary, a prompt, a credential in the environment, and an exit code. ## One turn from a command line *** `narya -p ""` runs one turn and answers on standard output. Its flags: * `--repo ` sets the working repository, and defaults to the working directory. * `--model ` chooses the model. * `--agent ` runs the turn under a named agent. * `--session ` continues an existing session. * `--style ` names a response style you declared as `[styles.]` in `config.toml`. It cannot join `--session`. * `--instructions ` adds an instruction file to the system prompt. Repeat it for more. A run that goes to a running host refuses it. * `--json` switches the output to a machine shape. ## Exit codes *** | Code | Meaning | | - | - | | `0` | The turn ran to its end. The answer is on standard output. | | `1` | The turn failed. A key the provider rejected lands here. | | `2` | Usage, configuration, or a store that another Narya process holds. No credential for the resolved provider lands here. | | `130` | Interrupted. The transcript survives, and `--session` resumes it. | The two credential failures carry different codes on purpose. An absent key exits `2`, and a rejected key exits `1`. Retry on `1`. On `2`, read the message first: a held store clears on its own, and a missing credential does not. > [!WARNING] > > A turn that stops early still exits `0`. Two endings do this: an output ceiling and the provider's content filter. The exit code alone cannot tell a cut answer from a complete one. Narya names both on standard error, and `--json` carries each as `stopReason` on the `turn-finished` event. ## The two shapes of `--json` *** `narya -p --json` streams one event envelope per line on standard output. Each line carries `id`, `type` and `timestamp`. A line adds `sessionId` and `laneId` when the event belongs to a session or a lane. `payload` is `null` when the event carries no body. Plain output carries only the answer to your prompt. The stream carries every lane, so select on `laneId`. These are the same event envelopes the host streams on [its API](/en/platform/narya/the-host-api), so a pipeline and a client read one format. Every other command answers with one document. The actions share one envelope: ```json theme={null} {"command":"narya retention run","result":"swept","changed":{},"exitCode":0} ``` `result` is one word to branch on. `changed` appears only on a run that changed something. A non-zero exit carries `reason` instead. Some commands publish their own document on success and print the envelope only on failure, among them `narya export` and the `narya schedule` verbs. Branch on the exit code for those, never on `result`. Under `--json`, standard output carries the document or the stream and nothing else. Sentences for a person go to standard error. ## The credential on a runner *** Put the provider key in your CI secret store and hand it to the step as `_API_KEY`. ```yaml theme={null} name: narya review on: pull_request permissions: contents: read jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false # Add a step here that places your licensed narya binary on PATH. - name: Ask Narya env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # Pass the ref through env, never into the script text. A ref with # shell metacharacters would run as script on this runner. BASE_REF: ${{ github.base_ref }} run: | narya -p "Read the changes on this branch against origin/$BASE_REF. Name every change that breaks a caller, with the file and why." ``` The step fails when Narya exits non-zero, and the answer lands in the job log. > [!NOTE] > > Narya reads the branch's own history, so check out the full branch. ## Schedules *** Put work on the host's clock. * `narya schedule add "" --every ` repeats on an interval, and `--at ` fires once. * Add `--repo `, `--agent ` or `--budget ` to either form. * `narya schedule list` lists what exists, and `narya schedule rm ` cancels one. * `narya schedule budget on ` changes what one fire of an existing schedule may spend. `off` stops the schedule from firing until you set a number again. Narya refuses to create a schedule in a directory nobody trusts. All four verbs talk to the running host. The spend ceiling is on by default. If you set no `--budget`, the schedule takes the operator's default, `$5.00` per fire, as `[schedules] ceiling_usd`. If you set a number, Narya keeps it. If you set `0`, Narya stores the schedule and refuses to fire it until you set a budget. ## Workflows *** A workflow is a JavaScript program that orchestrates agents. Start one by path or as inline source, over the host API. Narya refuses an invalid program before anything runs. A workflow program calls four capabilities: * `agent(step, agent, task)` runs one agent. * `agents([...])` runs one wave, read in the order you asked. * `step(name, value)` records a step. * `args` hands the program its own run's arguments. Three ceilings are constants, and no setting changes them: 72 hours per run, 4,096 host calls, and 4 hours per step. Narya snapshots the source at submission, so a resumed run uses the source that was submitted. # Extending Narya Source: https://docs.lerian.studio/en/platform/narya/extending-narya Add tool servers, OpenAPI tool trees, packages, registered extensions, confined scripts, agents, skills, commands and hooks, or drive Narya over its local API. You extend Narya with resources you declare in files, packages you install, extensions you register, and confined scripts. A client you write drives the host API. ## MCP tool servers *** Declare a server as `[tool_servers.]` in `config.toml`. Give it exactly one transport. `command` spawns a process and speaks stdio. `url` speaks streamable HTTP over https, or over http on loopback. A declaration with both, or with neither, refuses to load. ```toml theme={null} [tool_servers.filesystem] command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/repo"] env = { PATH = "/usr/local/bin:/usr/bin:/bin", HOME = "/home/you" } ``` > [!WARNING] > > `env` replaces the child environment. It does not add to it. The child inherits nothing, not even `PATH` and `HOME`. Declare both for any `npx` or `uvx` server, or the child exits before it serves a tool. The other keys are `dir`, `headers`, `oauth`, `timeout_ms` and `enabled`. Every declared server connects at startup unless you set `enabled = false`. Narya keeps one long-lived session per server and reconnects with backoff. When a server disconnects, Narya withdraws its tools from the registry, so the model never sees a departed server's tools. Set `oauth = true` for a remote server that signs in. Narya holds the connection open, prints the authorization URL, and listens on `127.0.0.1:7666` for the callback while the sign-in is open. A sign-in nobody answers costs that one server and nothing else. A one-shot run cannot complete a sign-in, and it says so. > [!WARNING] > > A declared tool server runs under the `tool` rule, which the shipped set allows. Narya's write and secrets rules do not apply to what the server itself reads or writes. Narrow it with `ask tool ` or `deny tool `. ## OpenAPI tool trees *** Declare `[tool_trees.]` to turn an OpenAPI document into a callable tool tree. The keys are `spec`, `base_url`, `headers`, `timeout_ms` and `enabled`. A relative `spec` path resolves against your Narya home. Set `base_url` when the document declares no server of its own. Narya derives the tools once at startup. No server runs, and no session reconnects. ## Confined scripts *** Narya runs ECMAScript in a confined engine. The engine reaches no filesystem, no network, no environment and no module loader. It has no dynamic evaluation. Call depth stops at 4,096, and source stops at 256 KiB. Three callers use it: the model's `run_program` tool, workflows, and tool discovery. ## The host API *** The host serves one HTTP API over a Unix socket, and every client drives it. [The host API](/en/platform/narya/the-host-api) describes the transport, the event stream and the envelopes. ## Packages *** `narya ext add ` installs a package from a local path or a Go module reference. `narya ext list`, `remove`, `enable` and `disable` manage what you installed. A package that declares only resources adds agents, skills, commands and hooks, and installs by copy. A package with Go code compiles into your Narya binary, so it needs a local Go toolchain. Narya asks you to trust a package that carries code or hooks before it installs it. Add `--yes` to skip that question in a script. ## Registered extensions *** To add an extension that runs as its own HTTPS service, register it with `POST /v1/extensions`. That needs the `extensions:manage` permission. The body needs two fields: `name`, in the form `publisher/extension`, and `endpoint`. The other fields are optional: `description`, `points`, `slots`, `position`, `grants`, `models` and `enabled`. The endpoint must be an `https://` URL, and this applies to loopback addresses too. The host resolves the address before it stores the registration. It refuses a loopback or private address unless `allow_private_endpoints` in the `[extensions]` table of `config.toml` admits that range. No setting admits any other address outside the public internet, such as a link-local or multicast address. An exclusive slot holds one extension. A registration that claims an exclusive slot that another extension holds gets `409` with `NRY-0006`. To admit both, name the winner of that slot under `prefer` in the `[extensions]` table. The response carries the extension's secret, once. No read returns it again. To replace the secret, call `POST /v1/extensions/{publisher}/{name}/secret`. The response carries the new secret and the time when the old one stops working. The host signs each call to the extension under the secret. The extension signs its own calls the same way, and it can call only the event stream. The extension serves its own operations at its endpoint. The host calls `/transform` at the decision points that the extension sits on. It calls `/models` and `/supply` when the extension supplies models, `/tools` when it publishes tools, and `/invoke` to run a named operation. The host does not follow a redirect from the extension. A client reaches an extension through the host with `POST /v1/extensions/{name}/invoke`. The token of an ordinary member is enough. The host signs the forwarded call, so the client does not need the secret. ## Agents, skills, commands and hooks *** Narya reads Claude Code's own formats without any change. **Agents** are markdown files with YAML frontmatter, and the body is the prompt. The keys are `name`, `description`, `tools` and `model`, and a file with no `name` takes its file name. An agent in your Narya home or in `.narya/agents` also reads `mode`, `hidden` and `permission-policy`. `tools` is an allowlist and never a grant: Narya intersects a child's list with its parent's, so a child agent can lose tools, never gain them. Narya reads agents from your Narya home, then `~/.claude/agents`, then the `.claude/agents` and `.narya/agents` of a repository you trust. Four agents ship with the binary: `explore`, `general`, `plan` and `review`. **Skills** follow the Agent Skills standard: a directory that holds a `SKILL.md`. Narya reads the same ladder, `~/.claude/skills` included, and re-reads it on each request. A skill you add mid-session reaches the next turn. **Commands** come from your Narya home, from `~/.claude/commands`, and from the `.claude/commands` and `.narya/commands` of a repository you trust. **Hooks** come from the `hooks` table of `~/.claude/settings.json`, and from the same file in a repository you trust. Narya reads them with the stdin keys and exit codes those scripts already use. Four events map: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `SessionStart`. Other events, such as `Stop`, `SubagentStop`, `Notification`, `PreCompact` and `SessionEnd`, have no equivalent. Narya skips them and says so in its log. > [!WARNING] > > These differences have no workaround. Narya reads hook output for a verdict only, so `additionalContext` never reaches the model's context. `systemMessage` goes nowhere. A hook read from `settings.json` always fails open: a hook that times out, exits with a code other than `0` or `2`, or prints output Narya cannot read lets the call through, and this format has no field to change that. Its `tool_response` arrives as a string, not an object. ## Narya's own hooks *** Declare `[[hooks]]` in `config.toml` with `point`, `command`, `timeout_ms`, `fail` and `match`. `fail` carries no default, so state it. `match` filters tool names at `before-tool-call` and `after-tool-call`. Use a tool name or a Go regular expression that matches the whole name, such as `read|write`. An empty value or `*` matches every tool. Other hook points do not accept a nonempty `match`. Four points decide, and a hook there can stop what it fronts: `before-tool-call`, `before-model-request`, `after-tool-call`, `at-permission-ask`. Eight points observe, and a hook there learns what happened without changing it: `session-started`, `lane-started`, `lane-finished`, `delegation-created`, `child-asked`, `skill-invoked`, `permission-denied`, `session-compacted`. A repository's own file cannot declare `at-permission-ask`. ## Delegation *** Delegation is a tool call. Set its bounds under `[delegation]` in `config.toml`. The `max_depth` key counts the levels below your conversation, and `0` turns delegation off. Set `max_concurrent` to bound how many lanes one conversation runs at any instant. # Models and credentials Source: https://docs.lerian.studio/en/platform/narya/models-and-credentials Choose a model supplier, give Narya your own key, and see exactly what leaves your machine and what it costs. Narya calls the model supplier you configure, with your own credential. Three suppliers ship with the binary. ## The three suppliers *** | Provider | Endpoint | | - | - | | `anthropic` | `https://api.anthropic.com` | | `openai` | `https://api.openai.com` | | `openrouter` | `https://openrouter.ai/api` | `openrouter` is a gateway to other suppliers. A fresh installation with no `config.toml` runs on the default model of the provider you signed in to. Override the wire family, the base URL and the default model per provider under `[providers.]` in `config.toml`. Your file wins on any collision with the catalog in the binary. Run `narya providers` to list the providers, and `narya models` to list the models. ## Where your key comes from *** Narya spends your organization's key first, when one is stored, and this machine's own key otherwise. A key typed into a client is your organization's: Narya stores it encrypted, and every member's turns spend it. Storing one needs the `keys:manage` permission. `narya auth login` stores this machine's own key. Narya derives the environment variable name from the provider name, uppercased, plus `_API_KEY`. So `anthropic` reads `ANTHROPIC_API_KEY`, `openai` reads `OPENAI_API_KEY`, and `openrouter` reads `OPENROUTER_API_KEY`. Narya reads this machine's key from three tiers, in order: 1. The environment variable, which wins on every read. 2. The operating system keychain: macOS Keychain or Linux Secret Service. 3. A per-provider file with `0600` permissions under `credentials` in your Narya home. Narya uses this tier only on a machine with no keychain backend at all. If the keychain exists but refuses the request, Narya reports the error and does not use the file. > [!WARNING] > > `narya auth login` writes to the keychain even when the environment variable already holds a key for that provider. The environment variable then shadows the stored one on every read, without a message. Unset the variable to use what you stored. ## Sign in *** Run `narya auth login ` to store a key, and `narya auth logout ` to remove it. Narya probes a typed key against the provider before it stores anything. Add `--method key` to type a key, or `--method subscription` for a browser sign-in. Browser sign-in works for `anthropic` only, against `claude.ai` and `platform.claude.com`. ## What leaves your machine *** Narya sends your prompts, the files it reads, and tool results to the model supplier you configure, with the key it spends. Lerian operates no endpoint in that path. Narya also reaches these hosts: * `models.dev`, for a model catalog refresh every 12 hours, and each provider you configured, for a `GET /v1/models` listing. Set `NARYA_MODELS_URL=off` to stop both. The refresh never blocks a turn, and a failure goes to the host log alone. * The two browser sign-in hosts above, during a sign-in. * The identity provider in your `[identity]` settings. * The address in `[identity] plane_url`, when you set one. The host reads your organization's settings there every 15 minutes, and a share finds your organization's home there. A session you share goes to that home. * A release host, on an explicit upgrade command. * Any host the model names through its `fetch` tool. That call runs under the network rule, which asks first. Narya reads at most 5 MiB and waits at most 30 seconds. * The supplier's own web search, which runs on the supplier's side under the same key. Narya offers it to the `anthropic` and `openai` providers, and only against the supplier's own endpoint. The network ask does not reach it. `deny network **` turns it off, and a per-host rule narrows `fetch`, never the search. An unattended run searches only where the rules allow every host. * The tool servers, OpenAPI tool trees and PDF converter you declare. See [Extending Narya](/en/platform/narya/extending-narya). * Registered extensions and packages. `narya ext add` downloads the package's Go module from the module source. * The OpenTelemetry collector you name under `[telemetry]` or in `OTEL_EXPORTER_OTLP_ENDPOINT`. Telemetry is off until you name one. Hooks can receive session data according to their point: `before-model-request` receives a prompt, `before-tool-call` receives tool arguments, and `after-tool-call` receives tool arguments and results. Review an extension's hooks before you enable it. Shell commands the model runs reach the network under the shell rules, not the network rule. ## Cost *** Prices come from a `models.dev` snapshot inside the binary, in US dollars per million tokens. A model with no published price reports no cost. It never reports a zero. Run `narya records usage --from